whatap 0.5.19 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/conf/config-default.js +7 -0
- package/lib/control/control-handler.js +21 -12
- package/lib/core/agent.js +2 -0
- package/lib/error/error-handler.js +437 -0
- package/lib/observers/apollo-server-observer.js +10 -2
- package/lib/observers/global-observer.js +17 -7
- package/lib/observers/grpc-observer.js +9 -9
- package/lib/observers/http-observer.js +145 -15
- package/lib/observers/maria-observer.js +0 -1
- package/lib/observers/mongodb-observer.js +4 -1
- package/lib/observers/mongoose-observer.js +414 -114
- package/lib/observers/mssql-observer.js +2 -0
- package/lib/observers/mysql-observer.js +5 -2
- package/lib/observers/mysql2-observer.js +703 -0
- package/lib/observers/oracle-observer.js +4 -1
- package/lib/observers/pgsql-observer.js +2 -0
- package/lib/observers/prisma-observer.js +3 -2
- package/lib/service/tx-record.js +92 -54
- package/lib/trace/trace-context.js +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2016 the WHATAP project authors. All rights reserved.
|
|
3
|
+
* Use of this source code is governed by a license that
|
|
4
|
+
* can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var TraceContextManager = require('../trace/trace-context-manager'),
|
|
8
|
+
ParsedSql = require('../trace/parsed-sql'),
|
|
9
|
+
SqlStepX = require('../step/sql-stepx'),
|
|
10
|
+
DBCStep = require('../step/dbc-step'),
|
|
11
|
+
ResultSetStep = require('../step/resultset-step'),
|
|
12
|
+
DataTextAgent = require('../data/datatext-agent'),
|
|
13
|
+
StatSql = require('../stat/stat-sql'),
|
|
14
|
+
MeterSql = require('../counter/meter/meter-sql'),
|
|
15
|
+
conf = require('../conf/configure'),
|
|
16
|
+
IntKeyMap = require('../util/intkey-map'),
|
|
17
|
+
EscapeLiteralSQL = require('../util/escape-literal-sql'),
|
|
18
|
+
HashUtil = require('../util/hashutil'),
|
|
19
|
+
StatError = require('../stat/stat-error'),
|
|
20
|
+
TextTypes = require('../lang/text-types'),
|
|
21
|
+
ParamSecurity = require('../util/paramsecurity'),
|
|
22
|
+
Logger = require('../logger'),
|
|
23
|
+
DateUtil = require('../util/dateutil'),
|
|
24
|
+
Buffer = require('buffer').Buffer,
|
|
25
|
+
TraceSQL = require('../trace/trace-sql'),
|
|
26
|
+
shimmer = require('../core/shimmer');
|
|
27
|
+
|
|
28
|
+
var Mysql2Observer = function (agent) {
|
|
29
|
+
this.agent = agent;
|
|
30
|
+
this.packages = ['mysql2', 'mysql2/promise'];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
var dbc_hash = 0;
|
|
34
|
+
var dbc = 'mysql://';
|
|
35
|
+
|
|
36
|
+
// 전역으로 후킹된 함수들을 추적
|
|
37
|
+
var globalHookedFunctions = new WeakSet();
|
|
38
|
+
// 후킹된 객체 인스턴스들을 추적 (중복 방지 강화)
|
|
39
|
+
var hookedInstances = new WeakSet();
|
|
40
|
+
|
|
41
|
+
var createQueryWrapper = function(isPromise = false) {
|
|
42
|
+
return function(original) {
|
|
43
|
+
return function wrappedQuery() {
|
|
44
|
+
var args = Array.prototype.slice.call(arguments);
|
|
45
|
+
var ctx = TraceContextManager.getCurrentContext();
|
|
46
|
+
|
|
47
|
+
// ctx가 없으면 추적하지 않고 원본 함수만 실행
|
|
48
|
+
if (ctx == null || args[0] == null) {
|
|
49
|
+
return original.apply(this, args);
|
|
50
|
+
}
|
|
51
|
+
if (args[0].sql == null && typeof args[0] != 'string') {
|
|
52
|
+
return original.apply(this, args);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 동시 쿼리 처리를 위한 고유 ID 생성
|
|
56
|
+
var queryId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
|
57
|
+
|
|
58
|
+
// 현재 처리 중인 쿼리 목록 관리
|
|
59
|
+
if (!ctx._processing_queries) {
|
|
60
|
+
ctx._processing_queries = new Set();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 같은 SQL이 동시에 처리되고 있는지 확인
|
|
64
|
+
var sqlKey = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].sql);
|
|
65
|
+
if (sqlKey && ctx._processing_queries.has(sqlKey)) {
|
|
66
|
+
return original.apply(this, args);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (sqlKey) {
|
|
70
|
+
ctx._processing_queries.add(sqlKey);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
var dbc_step = new DBCStep();
|
|
74
|
+
DataTextAgent.DBC.add(dbc_hash, dbc);
|
|
75
|
+
DataTextAgent.METHOD.add(dbc_hash, dbc);
|
|
76
|
+
DataTextAgent.ERROR.add(dbc_hash, dbc);
|
|
77
|
+
|
|
78
|
+
dbc_step.hash = dbc_hash;
|
|
79
|
+
dbc_step.start_time = ctx.getElapsedTime();
|
|
80
|
+
ctx.profile.push(dbc_step);
|
|
81
|
+
|
|
82
|
+
var sql_step = new SqlStepX();
|
|
83
|
+
sql_step.queryId = queryId;
|
|
84
|
+
sql_step.start_time = ctx.getElapsedTime();
|
|
85
|
+
ctx.profile.push(sql_step);
|
|
86
|
+
|
|
87
|
+
ctx.footprint('MySql2 Query Start [' + queryId + ']');
|
|
88
|
+
ctx.sql_count++;
|
|
89
|
+
|
|
90
|
+
var sql = args.length > 0 ? args[0] : undefined,
|
|
91
|
+
psql = null;
|
|
92
|
+
|
|
93
|
+
if (typeof sql !== 'string') {
|
|
94
|
+
sql = args[0].sql || undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof sql === 'string' && sql.length > 0) {
|
|
98
|
+
try {
|
|
99
|
+
psql = escapeLiteral(sql);
|
|
100
|
+
Logger.print('WHATAP-SQL-DEBUG', 'Processing SQL [' + queryId + ']: ' + sql.substring(0, 100), false);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
Logger.printError('WHATAP-191', 'Mysql2Observer escapeliteral error', e, false);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
sql = '';
|
|
106
|
+
psql = escapeLiteral(sql);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (psql != null) {
|
|
110
|
+
sql_step.hash = psql.sql;
|
|
111
|
+
}
|
|
112
|
+
sql_step.dbc = dbc_hash;
|
|
113
|
+
|
|
114
|
+
var els = new EscapeLiteralSQL(sql);
|
|
115
|
+
els.process();
|
|
116
|
+
|
|
117
|
+
// Context 활성 상태를 배열로 관리 (여러 쿼리 동시 처리)
|
|
118
|
+
if (!ctx.active_queries) {
|
|
119
|
+
ctx.active_queries = [];
|
|
120
|
+
}
|
|
121
|
+
ctx.active_queries.push({
|
|
122
|
+
sqlhash: sql_step.hash,
|
|
123
|
+
dbc: sql_step.dbc,
|
|
124
|
+
queryId: queryId
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 백워드 호환성을 위해 마지막 쿼리 정보 유지
|
|
128
|
+
ctx.active_sqlhash = sql_step.hash;
|
|
129
|
+
ctx.active_dbc = sql_step.dbc;
|
|
130
|
+
|
|
131
|
+
if (conf.profile_sql_param_enabled) {
|
|
132
|
+
var params = args.length > 1 && Array.isArray(args[1]) ? args[1] : undefined;
|
|
133
|
+
sql_step.setTrue(1);
|
|
134
|
+
var crc = {value: 0};
|
|
135
|
+
sql_step.p1 = toParamBytes(psql.param, crc);
|
|
136
|
+
if (params != undefined) {
|
|
137
|
+
const result = params.map((param) => {
|
|
138
|
+
if (typeof param === 'string') {
|
|
139
|
+
return `'${param}'`
|
|
140
|
+
}
|
|
141
|
+
return param
|
|
142
|
+
}).toString()
|
|
143
|
+
sql_step.p2 = toParamBytes(result, crc);
|
|
144
|
+
}
|
|
145
|
+
sql_step.pcrc = crc.value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 원본 MySQL Observer의 queryCallback 함수를 참고한 결과 처리 함수
|
|
149
|
+
function queryCallback(obj, args) {
|
|
150
|
+
if (ctx == null) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 처리 완료된 쿼리를 목록에서 제거
|
|
155
|
+
if (sqlKey && ctx._processing_queries) {
|
|
156
|
+
ctx._processing_queries.delete(sqlKey);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 활성 쿼리 목록에서 현재 쿼리 제거
|
|
160
|
+
if (ctx.active_queries) {
|
|
161
|
+
ctx.active_queries = ctx.active_queries.filter(q => q.queryId !== queryId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
TraceContextManager.resume(ctx._id);
|
|
165
|
+
|
|
166
|
+
// 에러 처리 (args[0]이 에러 객체)
|
|
167
|
+
if (args[0]) {
|
|
168
|
+
try {
|
|
169
|
+
if (conf.trace_sql_error_stack && conf.trace_sql_error_depth) {
|
|
170
|
+
var traceDepth = conf.trace_sql_error_depth;
|
|
171
|
+
var errorStack = args[0].stack ? args[0].stack.split("\n") : [];
|
|
172
|
+
if (errorStack.length > traceDepth) {
|
|
173
|
+
errorStack = errorStack.slice(0, traceDepth + 1);
|
|
174
|
+
}
|
|
175
|
+
ctx.error_message = errorStack.join("\n");
|
|
176
|
+
sql_step.error = ctx.error = StatError.addError('mysql2-' + (args[0].code || 'UNKNOWN'),
|
|
177
|
+
args[0].sqlMessage || args[0].message, ctx.service_hash, TextTypes.SQL, sql_step.hash);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 에러 무시 설정 확인
|
|
181
|
+
var shouldAddError = false;
|
|
182
|
+
if (conf._is_trace_ignore_err_cls_contains === true && args[0].code &&
|
|
183
|
+
args[0].code.indexOf(conf.trace_ignore_err_cls_contains) < 0) {
|
|
184
|
+
shouldAddError = true;
|
|
185
|
+
} else if (conf._is_trace_ignore_err_msg_contains === true && args[0].message &&
|
|
186
|
+
args[0].message.indexOf(conf.trace_ignore_err_msg_contains) < 0) {
|
|
187
|
+
shouldAddError = true;
|
|
188
|
+
} else if (conf._is_trace_ignore_err_cls_contains === false && conf._is_trace_ignore_err_msg_contains === false) {
|
|
189
|
+
shouldAddError = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (shouldAddError) {
|
|
193
|
+
sql_step.error = StatError.addError('mysql2-' + (args[0].code || 'UNKNOWN'),
|
|
194
|
+
args[0].message || 'mysql2 error', ctx.service_hash, TextTypes.SQL, sql_step.hash);
|
|
195
|
+
if (ctx.error.isZero()) {
|
|
196
|
+
ctx.error = sql_step.error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (e) {
|
|
200
|
+
Logger.printError('WHATAP-192', 'Error handling MySQL2 error', e, false);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
sql_step.elapsed = ctx.getElapsedTime() - sql_step.start_time;
|
|
205
|
+
ctx.sql_time += sql_step.elapsed;
|
|
206
|
+
|
|
207
|
+
// 핵심 통계 수집 로직
|
|
208
|
+
TraceSQL.isSlowSQL(ctx);
|
|
209
|
+
ctx.footprint('MySql2 Query Done [' + queryId + ']');
|
|
210
|
+
|
|
211
|
+
MeterSql.add(dbc_hash, sql_step.elapsed, args[0] != null);
|
|
212
|
+
StatSql.addSqlTime(ctx.service_hash, sql_step.dbc,
|
|
213
|
+
sql_step.hash, sql_step.elapsed, args[0] != null, 0);
|
|
214
|
+
|
|
215
|
+
// 결과 집합 처리 (SELECT 쿼리) - args[1]이 결과 배열
|
|
216
|
+
if (Array.isArray(args[1]) && psql != null && psql.type === 'S') {
|
|
217
|
+
var result_step = new ResultSetStep();
|
|
218
|
+
result_step.start_time = ctx.getElapsedTime();
|
|
219
|
+
result_step.elapsed = 0;
|
|
220
|
+
result_step.fetch = args[1].length;
|
|
221
|
+
result_step.sqlhash = psql.sql;
|
|
222
|
+
result_step.dbc = dbc_hash;
|
|
223
|
+
ctx.profile.push(result_step);
|
|
224
|
+
|
|
225
|
+
ctx.rs_count = ctx.rs_count ? ctx.rs_count + args[1].length : args[1].length;
|
|
226
|
+
ctx.rs_time = ctx.rs_time ? ctx.rs_time + sql_step.elapsed : sql_step.elapsed;
|
|
227
|
+
|
|
228
|
+
// 핵심 통계 수집 로직
|
|
229
|
+
MeterSql.addFetch(result_step.dbc, result_step.fetch, 0);
|
|
230
|
+
StatSql.addFetch(result_step.dbc, result_step.sqlhash, result_step.fetch, 0);
|
|
231
|
+
TraceSQL.isTooManyRecords(sql_step, result_step.fetch, ctx);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// UPDATE/INSERT 쿼리 처리
|
|
235
|
+
if (args[1] != null && args[1].affectedRows != null && psql != null && psql.type === 'U') {
|
|
236
|
+
sql_step.updated = args[1].affectedRows || 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Promise 기반 처리 (mysql2/promise)
|
|
241
|
+
if (isPromise) {
|
|
242
|
+
var result = original.apply(this, args);
|
|
243
|
+
|
|
244
|
+
// Promise 체인 처리
|
|
245
|
+
if (result && typeof result.then === 'function') {
|
|
246
|
+
return result.then(function(queryResult) {
|
|
247
|
+
// MySQL2 Promise 결과는 [rows, fields] 형태
|
|
248
|
+
var rows = Array.isArray(queryResult) ? queryResult[0] : queryResult;
|
|
249
|
+
var fields = Array.isArray(queryResult) ? queryResult[1] : null;
|
|
250
|
+
|
|
251
|
+
// queryCallback 형태로 변환하여 호출
|
|
252
|
+
queryCallback(null, [null, rows, fields]);
|
|
253
|
+
return queryResult;
|
|
254
|
+
}).catch(function(error) {
|
|
255
|
+
// 에러 시 처리
|
|
256
|
+
queryCallback(null, [error, null]);
|
|
257
|
+
throw error;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 콜백 기반 처리 (일반 mysql2) - 원본 MysqlObserver 방식 적용
|
|
264
|
+
var lastCallback = false;
|
|
265
|
+
|
|
266
|
+
// 원본 MysqlObserver의 functionHook 방식을 모방
|
|
267
|
+
// 마지막 인자에서 함수를 찾아서 후킹
|
|
268
|
+
lastCallback = hookLastFunction(args, queryCallback);
|
|
269
|
+
|
|
270
|
+
// functionHook이 실패한 경우 _callback 속성 확인 (원본 코드 참조)
|
|
271
|
+
if (lastCallback === false && args.length > 0 && args[0]._callback) {
|
|
272
|
+
try {
|
|
273
|
+
// _callback 속성이 있는 경우 직접 후킹
|
|
274
|
+
var originalCallback = args[0]._callback;
|
|
275
|
+
args[0]._callback = function() {
|
|
276
|
+
var callbackArgs = Array.prototype.slice.call(arguments);
|
|
277
|
+
try {
|
|
278
|
+
queryCallback(this, callbackArgs);
|
|
279
|
+
} catch (e) {
|
|
280
|
+
Logger.printError('WHATAP-CALLBACK', 'Error in _callback hook', e, false);
|
|
281
|
+
}
|
|
282
|
+
// 원본 콜백 실행
|
|
283
|
+
if (originalCallback && typeof originalCallback === 'function') {
|
|
284
|
+
return originalCallback.apply(this, callbackArgs);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
lastCallback = true;
|
|
288
|
+
} catch (e) {
|
|
289
|
+
Logger.printError('WHATAP-CALLBACK-HOOK', 'Error hooking _callback', e, false);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 수정된 args로 원본 함수 실행
|
|
294
|
+
try {
|
|
295
|
+
return original.apply(this, args);
|
|
296
|
+
} catch (queryError) {
|
|
297
|
+
// 쿼리 실행 중 에러 발생 시 즉시 처리
|
|
298
|
+
queryCallback(null, [queryError, null]);
|
|
299
|
+
throw queryError;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 원본 MySQL Observer의 functionHook을 모방한 함수
|
|
307
|
+
* 마지막 함수 인자를 찾아서 콜백으로 후킹
|
|
308
|
+
*/
|
|
309
|
+
function hookLastFunction(args, callback) {
|
|
310
|
+
if (!args || args.length === 0) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 뒤에서부터 함수를 찾음
|
|
315
|
+
for (var i = args.length - 1; i >= 0; i--) {
|
|
316
|
+
if (typeof args[i] === 'function') {
|
|
317
|
+
var originalFunction = args[i];
|
|
318
|
+
|
|
319
|
+
// 함수를 래핑
|
|
320
|
+
args[i] = function() {
|
|
321
|
+
var functionArgs = Array.prototype.slice.call(arguments);
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
// WHATAP 콜백 먼저 실행
|
|
325
|
+
callback(this, functionArgs);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
Logger.printError('WHATAP-FUNCTION-HOOK', 'Error in function hook callback', e, false);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 원본 함수 실행
|
|
331
|
+
if (originalFunction && typeof originalFunction === 'function') {
|
|
332
|
+
try {
|
|
333
|
+
return originalFunction.apply(this, functionArgs);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
Logger.printError('WHATAP-ORIGINAL-FUNCTION', 'Error in original function', e, false);
|
|
336
|
+
throw e;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// 후킹된 함수임을 표시
|
|
342
|
+
args[i].__whatap_hooked__ = true;
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 개선된 Connection 래퍼 - 원본 패턴 적용
|
|
352
|
+
*/
|
|
353
|
+
var createConnectionWrapper = function(isPromise = false) {
|
|
354
|
+
return function(original) {
|
|
355
|
+
return function wrappedCreateConnection() {
|
|
356
|
+
var args = Array.prototype.slice.call(arguments);
|
|
357
|
+
var ctx = TraceContextManager.getCurrentContext();
|
|
358
|
+
|
|
359
|
+
// DB 연결 정보 구성
|
|
360
|
+
if (dbc_hash === 0 && args.length > 0) {
|
|
361
|
+
var info = (args[0] || {});
|
|
362
|
+
dbc = 'mysql://';
|
|
363
|
+
dbc += info.user || '';
|
|
364
|
+
dbc += "@";
|
|
365
|
+
dbc += info.host || '';
|
|
366
|
+
dbc += '/';
|
|
367
|
+
dbc += info.database || '';
|
|
368
|
+
dbc_hash = HashUtil.hashFromString(dbc);
|
|
369
|
+
DataTextAgent.DBC.add(dbc_hash, dbc);
|
|
370
|
+
DataTextAgent.METHOD.add(dbc_hash, dbc);
|
|
371
|
+
DataTextAgent.ERROR.add(dbc_hash, dbc);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
var dbc_step = null;
|
|
375
|
+
if (ctx && !ctx.db_opening) {
|
|
376
|
+
ctx.db_opening = true;
|
|
377
|
+
ctx.footprint('MySql2 Connecting Start');
|
|
378
|
+
dbc_step = new DBCStep();
|
|
379
|
+
dbc_step.start_time = ctx.getElapsedTime();
|
|
380
|
+
dbc_step.hash = dbc_hash;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
var result = original.apply(this, args);
|
|
384
|
+
|
|
385
|
+
// Connection 객체 후킹 함수
|
|
386
|
+
function hookConnection(connection) {
|
|
387
|
+
if (connection && !hookedInstances.has(connection)) {
|
|
388
|
+
hookedInstances.add(connection);
|
|
389
|
+
Logger.print('WHATAP-MYSQL2-CONNECTION', 'Hooking new connection object', false);
|
|
390
|
+
|
|
391
|
+
// Connection의 query와 execute 메서드 후킹
|
|
392
|
+
if (connection.query && !connection.query.__whatap_wrapped__) {
|
|
393
|
+
shimmer.wrap(connection, 'query', createQueryWrapper(isPromise));
|
|
394
|
+
connection.query.__whatap_wrapped__ = true;
|
|
395
|
+
}
|
|
396
|
+
if (connection.execute && !connection.execute.__whatap_wrapped__) {
|
|
397
|
+
shimmer.wrap(connection, 'execute', createQueryWrapper(isPromise));
|
|
398
|
+
connection.execute.__whatap_wrapped__ = true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 에러 델리게이트 후킹 (원본 패턴)
|
|
402
|
+
if (connection._protocol && connection._protocol._delegateError) {
|
|
403
|
+
try {
|
|
404
|
+
var originalDelegateError = connection._protocol._delegateError;
|
|
405
|
+
connection._protocol._delegateError = function() {
|
|
406
|
+
var args = Array.prototype.slice.call(arguments);
|
|
407
|
+
try {
|
|
408
|
+
// WHATAP 에러 처리
|
|
409
|
+
handleProtocolError(args);
|
|
410
|
+
} catch (e) {
|
|
411
|
+
Logger.printError('WHATAP-PROTOCOL-ERROR', 'Error in protocol error handler', e, false);
|
|
412
|
+
}
|
|
413
|
+
// 원본 함수 실행
|
|
414
|
+
return originalDelegateError.apply(this, args);
|
|
415
|
+
};
|
|
416
|
+
} catch (e) {
|
|
417
|
+
Logger.printError('WHATAP-PROTOCOL-HOOK', 'Error hooking _delegateError', e, false);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return connection;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 프로토콜 에러 처리 함수
|
|
425
|
+
function handleProtocolError(args) {
|
|
426
|
+
var ctx = TraceContextManager.getCurrentContext();
|
|
427
|
+
if (ctx == null || !args[0]) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
var laststep = ctx.profile.getLastSteps(1);
|
|
432
|
+
if (laststep == null || laststep.length === 0) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
var step = laststep[0];
|
|
437
|
+
if (!args[0].fatal) {
|
|
438
|
+
MeterSql.add(step.dbc, step.elapsed, true);
|
|
439
|
+
StatSql.addSqlTime(ctx.service_hash, step.dbc, step.hash, step.elapsed, true, 0);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
var errorCode = args[0].code || 'UNKNOWN';
|
|
444
|
+
var errorMessage = ctx.error_message = args[0].message || 'mysql2 error';
|
|
445
|
+
ctx.error_class = args[0].name || args[0].constructor?.name || 'MySQLError';
|
|
446
|
+
|
|
447
|
+
if (args[0].fatal) {
|
|
448
|
+
step.error = StatError.addError('mysql2-' + errorCode, errorMessage, ctx.service_hash);
|
|
449
|
+
if (ctx.error.isZero()) {
|
|
450
|
+
ctx.error = step.error;
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
var shouldAddError = false;
|
|
454
|
+
if (conf._is_trace_ignore_err_cls_contains === true && errorCode.indexOf(conf.trace_ignore_err_cls_contains) < 0) {
|
|
455
|
+
shouldAddError = true;
|
|
456
|
+
} else if (conf._is_trace_ignore_err_msg_contains === true && errorMessage.indexOf(conf.trace_ignore_err_msg_contains) < 0) {
|
|
457
|
+
shouldAddError = true;
|
|
458
|
+
} else if (conf._is_trace_ignore_err_cls_contains === false && conf._is_trace_ignore_err_msg_contains === false) {
|
|
459
|
+
shouldAddError = true;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (shouldAddError) {
|
|
463
|
+
step.error = StatError.addError('mysql2-' + errorCode, errorMessage, ctx.service_hash, TextTypes.SQL, step.hash);
|
|
464
|
+
if (ctx.error.isZero()) {
|
|
465
|
+
ctx.error = step.error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch (e) {
|
|
470
|
+
Logger.printError('WHATAP-PROTOCOL-ERROR-HANDLING', 'Error processing protocol error', e, false);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Promise 기반 연결 처리
|
|
475
|
+
if (isPromise && result && typeof result.then === 'function') {
|
|
476
|
+
return result.then(function(connection) {
|
|
477
|
+
hookConnection(connection);
|
|
478
|
+
|
|
479
|
+
if (ctx && dbc_step) {
|
|
480
|
+
ctx.footprint('MySql2 Connecting Done');
|
|
481
|
+
ctx.db_opening = false;
|
|
482
|
+
dbc_step.elapsed = ctx.getElapsedTime() - dbc_step.start_time;
|
|
483
|
+
ctx.profile.push(dbc_step);
|
|
484
|
+
}
|
|
485
|
+
return connection;
|
|
486
|
+
}).catch(function(error) {
|
|
487
|
+
if (ctx && dbc_step) {
|
|
488
|
+
ctx.footprint('MySql2 Connecting Error');
|
|
489
|
+
ctx.db_opening = false;
|
|
490
|
+
dbc_step.elapsed = ctx.getElapsedTime() - dbc_step.start_time;
|
|
491
|
+
ctx.profile.push(dbc_step);
|
|
492
|
+
}
|
|
493
|
+
throw error;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// 동기/콜백 기반 연결 처리
|
|
497
|
+
else {
|
|
498
|
+
if (result && typeof result === 'object') {
|
|
499
|
+
hookConnection(result);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (ctx && dbc_step) {
|
|
503
|
+
ctx.footprint('MySql2 Connecting Done');
|
|
504
|
+
ctx.db_opening = false;
|
|
505
|
+
dbc_step.elapsed = ctx.getElapsedTime() - dbc_step.start_time;
|
|
506
|
+
ctx.profile.push(dbc_step);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return result;
|
|
511
|
+
};
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* 개선된 Pool 래퍼 - 원본 패턴 적용
|
|
517
|
+
*/
|
|
518
|
+
var createPoolWrapper = function(isPromise = false) {
|
|
519
|
+
return function(original) {
|
|
520
|
+
return function wrappedCreatePool() {
|
|
521
|
+
var args = Array.prototype.slice.call(arguments);
|
|
522
|
+
|
|
523
|
+
// DB 연결 정보 구성
|
|
524
|
+
if (dbc_hash === 0 && args.length > 0) {
|
|
525
|
+
var info = args[0];
|
|
526
|
+
dbc = 'mysql://';
|
|
527
|
+
dbc += info.user || '';
|
|
528
|
+
dbc += "@";
|
|
529
|
+
dbc += info.host || '';
|
|
530
|
+
dbc += '/';
|
|
531
|
+
dbc += info.database || '';
|
|
532
|
+
dbc_hash = HashUtil.hashFromString(dbc);
|
|
533
|
+
DataTextAgent.DBC.add(dbc_hash, dbc);
|
|
534
|
+
DataTextAgent.METHOD.add(dbc_hash, dbc);
|
|
535
|
+
DataTextAgent.ERROR.add(dbc_hash, dbc);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
var pool = original.apply(this, args);
|
|
539
|
+
|
|
540
|
+
// Pool 객체 후킹
|
|
541
|
+
if (pool && !hookedInstances.has(pool)) {
|
|
542
|
+
hookedInstances.add(pool);
|
|
543
|
+
|
|
544
|
+
// Pool의 query와 execute 메서드 후킹
|
|
545
|
+
if (pool.query && !pool.query.__whatap_wrapped__) {
|
|
546
|
+
shimmer.wrap(pool, 'query', createQueryWrapper(isPromise));
|
|
547
|
+
pool.query.__whatap_wrapped__ = true;
|
|
548
|
+
}
|
|
549
|
+
if (pool.execute && !pool.execute.__whatap_wrapped__) {
|
|
550
|
+
shimmer.wrap(pool, 'execute', createQueryWrapper(isPromise));
|
|
551
|
+
pool.execute.__whatap_wrapped__ = true;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// getConnection 메서드 후킹 - 원본 패턴 적용
|
|
555
|
+
if (pool.getConnection && !pool.getConnection.__whatap_wrapped__) {
|
|
556
|
+
shimmer.wrap(pool, 'getConnection', function(original) {
|
|
557
|
+
return function wrappedGetConnection() {
|
|
558
|
+
var args = Array.prototype.slice.call(arguments);
|
|
559
|
+
var ctx = TraceContextManager.getCurrentContext();
|
|
560
|
+
|
|
561
|
+
if (ctx == null) {
|
|
562
|
+
return original.apply(this, args);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 원본 패턴: functionHook을 사용하여 콜백 후킹
|
|
566
|
+
hookLastFunction(args, function(obj, callbackArgs) {
|
|
567
|
+
TraceContextManager.resume(ctx._id);
|
|
568
|
+
DataTextAgent.DBC.add(dbc_hash, dbc);
|
|
569
|
+
|
|
570
|
+
if (callbackArgs[0] != null) {
|
|
571
|
+
return; // 에러가 있으면 처리하지 않음
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
var conn = callbackArgs[1];
|
|
575
|
+
if (conn && !conn.__query_hook__) {
|
|
576
|
+
conn.__query_hook__ = true;
|
|
577
|
+
|
|
578
|
+
// Connection 객체의 메서드들 후킹
|
|
579
|
+
if (conn.query && !conn.query.__whatap_wrapped__) {
|
|
580
|
+
shimmer.wrap(conn, 'query', createQueryWrapper(isPromise));
|
|
581
|
+
conn.query.__whatap_wrapped__ = true;
|
|
582
|
+
}
|
|
583
|
+
if (conn.execute && !conn.execute.__whatap_wrapped__) {
|
|
584
|
+
shimmer.wrap(conn, 'execute', createQueryWrapper(isPromise));
|
|
585
|
+
conn.execute.__whatap_wrapped__ = true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
var result = original.apply(this, args);
|
|
591
|
+
|
|
592
|
+
// 연결 통계 업데이트 (원본 패턴)
|
|
593
|
+
try {
|
|
594
|
+
if (this._allConnections != undefined) {
|
|
595
|
+
var all = this._allConnections.length;
|
|
596
|
+
var idle = this._freeConnections.length;
|
|
597
|
+
MeterSql.setConnection((all - idle), idle, dbc);
|
|
598
|
+
}
|
|
599
|
+
} catch (e) {
|
|
600
|
+
Logger.printError('WHATAP-CONNECTION-STATS', 'Error updating connection stats', e, false);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return result;
|
|
604
|
+
};
|
|
605
|
+
});
|
|
606
|
+
pool.getConnection.__whatap_wrapped__ = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return pool;
|
|
611
|
+
};
|
|
612
|
+
};
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
var toParamBytes = function (p, crc) {
|
|
616
|
+
if (p == null || p.length === 0) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
return ParamSecurity.encrypt(Buffer.from(p, 'utf8'), crc);
|
|
621
|
+
} catch (e) {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
Mysql2Observer.prototype.inject = function (mod, moduleName) {
|
|
627
|
+
if (mod.__whatap_observe__) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
mod.__whatap_observe__ = true;
|
|
631
|
+
Logger.initPrint("Mysql2Observer");
|
|
632
|
+
|
|
633
|
+
if (conf.sql_enabled === false) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// mysql2/promise인지 일반 mysql2인지 구분
|
|
638
|
+
var isPromise = moduleName && moduleName.includes('promise');
|
|
639
|
+
|
|
640
|
+
Logger.print('WHATAP-MYSQL2-INJECT', 'Injecting MySQL2 module: ' + moduleName + ', Promise mode: ' + isPromise, false);
|
|
641
|
+
|
|
642
|
+
// 전역 함수 추적으로 중복 후킹 방지
|
|
643
|
+
if (mod.createConnection && !globalHookedFunctions.has(mod.createConnection)) {
|
|
644
|
+
globalHookedFunctions.add(mod.createConnection);
|
|
645
|
+
shimmer.wrap(mod, 'createConnection', createConnectionWrapper(isPromise));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (mod.createPool && !globalHookedFunctions.has(mod.createPool)) {
|
|
649
|
+
globalHookedFunctions.add(mod.createPool);
|
|
650
|
+
shimmer.wrap(mod, 'createPool', createPoolWrapper(isPromise));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (mod.createPoolCluster && !globalHookedFunctions.has(mod.createPoolCluster)) {
|
|
654
|
+
globalHookedFunctions.add(mod.createPoolCluster);
|
|
655
|
+
shimmer.wrap(mod, 'createPoolCluster', createPoolWrapper(isPromise));
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
var checkedSql = new IntKeyMap(2000).setMax(2000);
|
|
660
|
+
var nonLiteSql = new IntKeyMap(5000).setMax(5000);
|
|
661
|
+
var date = DateUtil.yyyymmdd();
|
|
662
|
+
|
|
663
|
+
function escapeLiteral(sql) {
|
|
664
|
+
if (sql == null) {
|
|
665
|
+
sql = '';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (date !== DateUtil.yyyymmdd()) {
|
|
669
|
+
checkedSql.clear();
|
|
670
|
+
nonLiteSql.clear();
|
|
671
|
+
date = DateUtil.yyyymmdd();
|
|
672
|
+
Logger.print('WHATAP-SQL-CLEAR', 'Mysql2Observer CLEAR OK!!!!!!!!!!!!!!!!', false);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
var sqlHash = HashUtil.hashFromString(sql);
|
|
676
|
+
var psql = nonLiteSql.get(sqlHash);
|
|
677
|
+
|
|
678
|
+
if (psql != null) {
|
|
679
|
+
return psql;
|
|
680
|
+
}
|
|
681
|
+
psql = checkedSql.get(sqlHash);
|
|
682
|
+
|
|
683
|
+
if (psql != null) {
|
|
684
|
+
return psql;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
var els = new EscapeLiteralSQL(sql);
|
|
688
|
+
els.process();
|
|
689
|
+
|
|
690
|
+
var hash = HashUtil.hashFromString(els.getParsedSql());
|
|
691
|
+
DataTextAgent.SQL.add(hash, els.getParsedSql());
|
|
692
|
+
|
|
693
|
+
if (hash === sqlHash) {
|
|
694
|
+
psql = new ParsedSql(els.sqlType, hash, null);
|
|
695
|
+
nonLiteSql.put(sqlHash, psql);
|
|
696
|
+
} else {
|
|
697
|
+
psql = new ParsedSql(els.sqlType, hash, els.getParameter());
|
|
698
|
+
checkedSql.put(sqlHash, psql);
|
|
699
|
+
}
|
|
700
|
+
return psql;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
exports.Mysql2Observer = Mysql2Observer;
|