whatap 0.5.7 → 0.5.9
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 +5 -1
- package/lib/core/agent.js +12 -1
- package/lib/logger.js +2 -3
- package/lib/observers/apollo-server-observer.js +153 -0
- package/lib/observers/prisma-observer.js +1045 -0
- package/package.json +2 -2
|
@@ -267,7 +267,11 @@ var ConfigDefault = {
|
|
|
267
267
|
"grpc_profile_ignore_method": bool("grpc_profile_ignore_method", true),
|
|
268
268
|
|
|
269
269
|
"oname_port_postfix_enabled": bool("oname_port_postfix_enabled", false),
|
|
270
|
-
"ignore_http_lost_connection": bool("ignore_http_lost_connection", false)
|
|
270
|
+
"ignore_http_lost_connection": bool("ignore_http_lost_connection", false),
|
|
271
|
+
|
|
272
|
+
"profile_graphql_enabled": bool("profile_graphql_enabled", true),
|
|
273
|
+
"profile_graphql_variable_enabled": bool("profile_graphql_variable_enabled", false),
|
|
274
|
+
"ignore_graphql_operation": str("ignore_graphql_operation", '')
|
|
271
275
|
};
|
|
272
276
|
|
|
273
277
|
ConfigDefault._hook_method_ignore_prefix = ConfigDefault.hook_method_ignore_prefixes.split(',');
|
package/lib/core/agent.js
CHANGED
|
@@ -30,7 +30,10 @@ var Interceptor = require('./interceptor').Interceptor,
|
|
|
30
30
|
PromiseObserver = require('../observers/promise-observer').PromiseObserver,
|
|
31
31
|
PgSqlObserver = require('../observers/pgsql-observer').PgSqlObserver,
|
|
32
32
|
ScheduleObserver = require('../observers/schedule-observer').ScheduleObserver,
|
|
33
|
-
GRpcObserver = require('../observers/grpc-observer').GRpcObserver
|
|
33
|
+
// GRpcObserver = require('../observers/grpc-observer').GRpcObserver,
|
|
34
|
+
ApolloObserver = require('../observers/apollo-server-observer').ApolloServerObserver,
|
|
35
|
+
PrismaObserver = require('../observers/prisma-observer').PrismaObserver;
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
var Configuration = require('./../conf/configure'),
|
|
36
39
|
SecurityMaster = require('./../net/security-master'),
|
|
@@ -112,6 +115,12 @@ NodeAgent.prototype.init = function(cb) {
|
|
|
112
115
|
self.findRoot();
|
|
113
116
|
|
|
114
117
|
Logger.initializer.process();
|
|
118
|
+
|
|
119
|
+
// 최초 1회 실행 후 1시간마다 실행
|
|
120
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
121
|
+
Logger.clearOldLog();
|
|
122
|
+
setInterval(Logger.clearOldLog, ONE_HOUR);
|
|
123
|
+
|
|
115
124
|
Logger.print('WHATAP-001', 'Start initialize WhaTap Agent... Root[' + self._conf['app.root'] + ']', true);
|
|
116
125
|
|
|
117
126
|
if(self._conf['app.root'] == null || self._conf['app.root'].length == 0) {
|
|
@@ -268,6 +277,8 @@ NodeAgent.prototype.loadObserves = function() {
|
|
|
268
277
|
observes.push(PgSqlObserver);
|
|
269
278
|
observes.push(ScheduleObserver);
|
|
270
279
|
// observes.push(GRpcObserver);
|
|
280
|
+
observes.push(ApolloObserver);
|
|
281
|
+
observes.push(PrismaObserver);
|
|
271
282
|
|
|
272
283
|
var packageToObserve = {};
|
|
273
284
|
observes.forEach(function(observeObj) {
|
package/lib/logger.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
var now = DateUtil.currentTime();
|
|
28
28
|
if(now > this.last + DateUtil.MILLIS_PER_HOUR) {
|
|
29
29
|
this.last = now;
|
|
30
|
-
Logger.clearOldLog();
|
|
30
|
+
// Logger.clearOldLog();
|
|
31
31
|
}
|
|
32
32
|
if(this.lastFileRotation !== conf.log_rotation_enabled
|
|
33
33
|
|| this.lastDateUnit !== DateUtil.getDateUnit()
|
|
@@ -210,7 +210,6 @@
|
|
|
210
210
|
log_prefix = WHATAP_CONF + "-";
|
|
211
211
|
|
|
212
212
|
fs.readdir(dir, function (err, files) {
|
|
213
|
-
|
|
214
213
|
for (var i = 0; i < files.length; i++) {
|
|
215
214
|
var stat = fs.statSync(path.join(dir, files[i]));
|
|
216
215
|
if (stat.isDirectory()) {
|
|
@@ -232,12 +231,12 @@
|
|
|
232
231
|
var fileUnit = DateUtil.getDateUnit(d);
|
|
233
232
|
try {
|
|
234
233
|
if (nowUnit - fileUnit > conf.log_keep_days) {
|
|
234
|
+
let filePath = `${dir}/${files[i]}`;
|
|
235
235
|
fs.rmSync(filePath);
|
|
236
236
|
}
|
|
237
237
|
} catch (e) {
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
|
-
|
|
241
240
|
});
|
|
242
241
|
},
|
|
243
242
|
read : function( file, endpos, length , callback) {
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
conf = require('../conf/configure'),
|
|
9
|
+
Logger = require('../logger');
|
|
10
|
+
const HashUtil = require('../util/hashutil');
|
|
11
|
+
const DataTextAgent = require('../data/datatext-agent');
|
|
12
|
+
const MessageStep = require('../step/message-step');
|
|
13
|
+
const shimmer = require('../core/shimmer');
|
|
14
|
+
|
|
15
|
+
var profile_graphql_enabled = conf.getProperty('profile_graphql_enabled', true);
|
|
16
|
+
var profile_graphql_variable_enabled = conf.getProperty('profile_graphql_variable_enabled', false);
|
|
17
|
+
var ignore_graphql_operation = conf.getProperty('ignore_graphql_operation', '');
|
|
18
|
+
|
|
19
|
+
conf.on('profile_graphql_enabled', function(newProperty) {
|
|
20
|
+
profile_graphql_enabled = newProperty;
|
|
21
|
+
});
|
|
22
|
+
conf.on('profile_graphql_variable_enabled', function(newProperty) {
|
|
23
|
+
profile_graphql_variable_enabled = newProperty;
|
|
24
|
+
});
|
|
25
|
+
conf.on('ignore_graphql_operation', function(newProperty) {
|
|
26
|
+
ignore_graphql_operation = newProperty;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
var ApolloServerObserver = function(agent) {
|
|
30
|
+
this.agent = agent;
|
|
31
|
+
this.packages = ['@apollo/server'];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function checkIgnoreOperation(ignore_operation, operation_name) {
|
|
35
|
+
try {
|
|
36
|
+
let ignore_operation_set = null;
|
|
37
|
+
if (ignore_operation) {
|
|
38
|
+
ignore_operation_set = new Set(ignore_operation.split(','));
|
|
39
|
+
} else {
|
|
40
|
+
ignore_operation_set = null;
|
|
41
|
+
}
|
|
42
|
+
if (ignore_operation_set && ignore_operation_set.has(operation_name)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
Logger.printError('WHATAP-803', 'GraphQL checkIgnoreOperation error: ' + e, false);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function wrapExecuteHTTPGraphQLRequest(original) {
|
|
52
|
+
return async function executeHTTPGraphQLRequest() {
|
|
53
|
+
if (!profile_graphql_enabled) {
|
|
54
|
+
return original.apply(this, arguments);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
var ctx = TraceContextManager.getCurrentContext();
|
|
59
|
+
if (!ctx) {
|
|
60
|
+
return original.apply(this, arguments);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const [requestContext] = arguments;
|
|
64
|
+
const { httpGraphQLRequest } = requestContext;
|
|
65
|
+
const { body } = httpGraphQLRequest;
|
|
66
|
+
|
|
67
|
+
const operationName = body.operationName;
|
|
68
|
+
if (checkIgnoreOperation(ignore_graphql_operation, operationName)) {
|
|
69
|
+
TraceContextManager.end(ctx._id)
|
|
70
|
+
return original.apply(this, arguments);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const originalServiceName = ctx.service_name || '';
|
|
74
|
+
if(operationName){
|
|
75
|
+
ctx.service_name = `${originalServiceName}?operationName=${operationName}`;
|
|
76
|
+
ctx.service_hash = HashUtil.hashFromString(ctx.service_name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Operation Type Step (query)
|
|
80
|
+
if(body.query && body.query.trim()){
|
|
81
|
+
var step_type = new MessageStep();
|
|
82
|
+
step_type.hash = HashUtil.hashFromString('Type');
|
|
83
|
+
step_type.start_time = ctx.getElapsedTime();
|
|
84
|
+
step_type.desc = body.query.trim().startsWith('mutation') ? 'mutation' : 'query';
|
|
85
|
+
DataTextAgent.MESSAGE.add(step_type.hash, "Type");
|
|
86
|
+
ctx.profile.push(step_type);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Operation Name Step
|
|
90
|
+
if(operationName){
|
|
91
|
+
var step_operation = new MessageStep();
|
|
92
|
+
step_operation.hash = HashUtil.hashFromString('Operation');
|
|
93
|
+
step_operation.start_time = ctx.getElapsedTime();
|
|
94
|
+
step_operation.desc = operationName || 'anonymous';
|
|
95
|
+
DataTextAgent.MESSAGE.add(step_operation.hash, "Operation");
|
|
96
|
+
ctx.profile.push(step_operation);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Variables Step
|
|
100
|
+
if (profile_graphql_variable_enabled && body.variables && Object.keys(body.variables).length > 0) {
|
|
101
|
+
var step_variables = new MessageStep();
|
|
102
|
+
step_variables.hash = HashUtil.hashFromString('Variables');
|
|
103
|
+
step_variables.start_time = ctx.getElapsedTime();
|
|
104
|
+
step_variables.desc = JSON.stringify(Object.keys(body.variables));
|
|
105
|
+
DataTextAgent.MESSAGE.add(step_variables.hash, "Variables");
|
|
106
|
+
ctx.profile.push(step_variables);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const response = await original.apply(this, arguments);
|
|
110
|
+
|
|
111
|
+
// 에러 처리
|
|
112
|
+
if (response.body.kind === 'complete' && response.body.string.includes('"errors"')) {
|
|
113
|
+
try {
|
|
114
|
+
const result = JSON.parse(response.body.string);
|
|
115
|
+
if (result.errors && result.errors.length > 0) {
|
|
116
|
+
const errorMessages = result.errors.map(error => error.message).join('\n');
|
|
117
|
+
ctx.statusCode = 500;
|
|
118
|
+
ctx.error_message = errorMessages;
|
|
119
|
+
}
|
|
120
|
+
} catch (e) {
|
|
121
|
+
Logger.printError('WHATAP-802', 'GraphQL error parsing failed', e, false);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return response;
|
|
126
|
+
|
|
127
|
+
} catch (err) {
|
|
128
|
+
Logger.printError('WHATAP-801', 'GraphQL executeHTTPGraphQLRequest error: ' + err, false);
|
|
129
|
+
if (ctx) {
|
|
130
|
+
ctx.statusCode = 500;
|
|
131
|
+
ctx.error_message = err.stack;
|
|
132
|
+
}
|
|
133
|
+
return original.apply(this, arguments);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ApolloServerObserver.prototype.inject = function(mod, moduleName) {
|
|
139
|
+
if (mod.__whatap_observe__) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
mod.__whatap_observe__ = true;
|
|
143
|
+
Logger.initPrint("ApolloServerObserver");
|
|
144
|
+
|
|
145
|
+
if (profile_graphql_enabled) {
|
|
146
|
+
// Hook ApolloServer
|
|
147
|
+
if (mod.ApolloServer && mod.ApolloServer.prototype) {
|
|
148
|
+
shimmer.wrap(mod.ApolloServer.prototype, 'executeHTTPGraphQLRequest', wrapExecuteHTTPGraphQLRequest);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
exports.ApolloServerObserver = ApolloServerObserver;
|
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2025 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
|
+
const TraceContextManager = require("../trace/trace-context-manager");
|
|
8
|
+
const ParsedSql = require("../trace/parsed-sql");
|
|
9
|
+
const SqlStepX = require("../step/sql-stepx");
|
|
10
|
+
const DBCStep = require("../step/dbc-step");
|
|
11
|
+
const ResultSetStep = require("../step/resultset-step");
|
|
12
|
+
const DataTextAgent = require("../data/datatext-agent");
|
|
13
|
+
const StatSql = require("../stat/stat-sql");
|
|
14
|
+
const MeterSql = require("../counter/meter/meter-sql");
|
|
15
|
+
const conf = require("../conf/configure");
|
|
16
|
+
const IntKeyMap = require("../util/intkey-map");
|
|
17
|
+
const EscapeLiteralSQL = require("../util/escape-literal-sql");
|
|
18
|
+
const HashUtil = require("../util/hashutil");
|
|
19
|
+
const StatError = require("../stat/stat-error");
|
|
20
|
+
const TextTypes = require("../lang/text-types");
|
|
21
|
+
const ParamSecurity = require("../util/paramsecurity");
|
|
22
|
+
const Logger = require("../logger");
|
|
23
|
+
const Buffer = require("buffer").Buffer;
|
|
24
|
+
const DateUtil = require("../util/dateutil");
|
|
25
|
+
const TraceSQL = require("../trace/trace-sql");
|
|
26
|
+
const shimmer = require("../core/shimmer");
|
|
27
|
+
|
|
28
|
+
var PrismaObserver = function(agent) {
|
|
29
|
+
this.agent = agent;
|
|
30
|
+
this.packages = ["@prisma/client"];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
var dbc_hash = 0;
|
|
34
|
+
var dbc = "";
|
|
35
|
+
|
|
36
|
+
PrismaObserver.prototype.inject = function(mod, moduleName) {
|
|
37
|
+
if (mod.__whatap_observe__) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
mod.__whatap_observe__ = true;
|
|
42
|
+
Logger.initPrint("PrismaObserver");
|
|
43
|
+
|
|
44
|
+
const self = this;
|
|
45
|
+
|
|
46
|
+
if (conf.sql_enabled === false) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Prisma Client 초기화 메서드 후킹
|
|
51
|
+
if (mod.PrismaClient) {
|
|
52
|
+
// 직접 PrismaClient 생성자 후킹
|
|
53
|
+
shimmer.wrap(mod, 'PrismaClient', function(originalConstructor) {
|
|
54
|
+
return function() {
|
|
55
|
+
// 원래 생성자 호출
|
|
56
|
+
const instance = new originalConstructor(...arguments);
|
|
57
|
+
|
|
58
|
+
// 이 시점에서 instance가 생성된 Prisma 클라이언트 인스턴스
|
|
59
|
+
self.patchPrismaInstance(instance);
|
|
60
|
+
|
|
61
|
+
return instance;
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// 각 Prisma 인스턴스에 패치 적용
|
|
68
|
+
PrismaObserver.prototype.patchPrismaInstance = function(prismaInstance) {
|
|
69
|
+
if (prismaInstance.__whatap_observe__) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
prismaInstance.__whatap_observe__ = true;
|
|
73
|
+
|
|
74
|
+
// 모든 DB 연결 정보 추출 및 초기화
|
|
75
|
+
this.setupConnectionInfo(prismaInstance);
|
|
76
|
+
|
|
77
|
+
// $connect 후킹
|
|
78
|
+
this.hookConnect(prismaInstance);
|
|
79
|
+
|
|
80
|
+
// Raw 쿼리 메서드 후킹
|
|
81
|
+
// this.hookRawQueryMethods(prismaInstance);
|
|
82
|
+
|
|
83
|
+
// $use 미들웨어 후킹 (모델 메서드 추적)
|
|
84
|
+
this.hookUseMiddleware(prismaInstance);
|
|
85
|
+
|
|
86
|
+
// 각 모델에 대한 직접 메서드 후킹 (더 안정적인 추적을 위해)
|
|
87
|
+
// this.hookModelMethods(prismaInstance);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// 연결 정보 설정
|
|
91
|
+
PrismaObserver.prototype.setupConnectionInfo = function(prismaInstance) {
|
|
92
|
+
try {
|
|
93
|
+
// 연결 정보 가져오기 시도
|
|
94
|
+
const url = prismaInstance._engineConfig?.datasources?.db?.url ||
|
|
95
|
+
prismaInstance._baseDmmf?.datamodel?.datasources?.[0]?.url?.value ||
|
|
96
|
+
process.env.DATABASE_URL ||
|
|
97
|
+
'prisma:unknown';
|
|
98
|
+
|
|
99
|
+
if (url && url !== "prisma:unknown") {
|
|
100
|
+
const dbUrl = new URL(url);
|
|
101
|
+
const protocol = dbUrl.protocol.replace(':', '');
|
|
102
|
+
|
|
103
|
+
// 프로토콜에 따라 접두사 설정 (postgresql -> pgsql로 변환할 수도 있음)
|
|
104
|
+
const dbProtocol = protocol === 'pgsql' ? 'postgresql' : protocol;
|
|
105
|
+
|
|
106
|
+
// MySQL 관찰자와 동일한 형식으로 구성
|
|
107
|
+
dbc = `${dbProtocol}://`;
|
|
108
|
+
dbc += dbUrl.username || '';
|
|
109
|
+
dbc += "@";
|
|
110
|
+
dbc += dbUrl.hostname || '';
|
|
111
|
+
dbc += '/';
|
|
112
|
+
dbc += dbUrl.pathname.replace('/', '') || '';
|
|
113
|
+
dbc_hash = HashUtil.hashFromString(dbc);
|
|
114
|
+
|
|
115
|
+
DataTextAgent.DBC.add(dbc_hash, dbc);
|
|
116
|
+
DataTextAgent.METHOD.add(dbc_hash, dbc);
|
|
117
|
+
DataTextAgent.ERROR.add(dbc_hash, dbc);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
} catch (e) {
|
|
121
|
+
Logger.printError("WHATAP-301", "Failed to extract connection info", e, false);
|
|
122
|
+
dbc = "prisma:unknown";
|
|
123
|
+
dbc_hash = HashUtil.hashFromString(dbc);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Connect 메서드 후킹
|
|
128
|
+
PrismaObserver.prototype.hookConnect = function(prismaInstance) {
|
|
129
|
+
const self = this;
|
|
130
|
+
|
|
131
|
+
shimmer.wrap(prismaInstance, "$connect", function(original) {
|
|
132
|
+
return async function() {
|
|
133
|
+
const ctx = TraceContextManager.getCurrentContext();
|
|
134
|
+
if (!ctx || ctx.db_opening) {
|
|
135
|
+
return original.apply(this, arguments);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ctx.db_opening = true;
|
|
139
|
+
ctx.footprint("Prisma Connecting Start");
|
|
140
|
+
|
|
141
|
+
const dbc_step = new DBCStep();
|
|
142
|
+
dbc_step.start_time = ctx.getElapsedTime();
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const result = await original.apply(this, arguments);
|
|
146
|
+
|
|
147
|
+
dbc_step.hash = dbc_hash;
|
|
148
|
+
dbc_step.elapsed = ctx.getElapsedTime() - dbc_step.start_time;
|
|
149
|
+
|
|
150
|
+
ctx.db_opening = false;
|
|
151
|
+
ctx.footprint("Prisma Connecting Done");
|
|
152
|
+
ctx.profile.push(dbc_step);
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
ctx.db_opening = false;
|
|
157
|
+
|
|
158
|
+
dbc_step.hash = dbc_hash;
|
|
159
|
+
dbc_step.elapsed = ctx.getElapsedTime() - dbc_step.start_time;
|
|
160
|
+
dbc_step.error = StatError.addError("prisma-connection", err.message, ctx.service_hash);
|
|
161
|
+
|
|
162
|
+
if (ctx.error.isZero()) {
|
|
163
|
+
ctx.error = dbc_step.error;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
ctx.profile.push(dbc_step);
|
|
167
|
+
Logger.printError("WHATAP-302", "Connection Error", err);
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Raw 쿼리 메서드 후킹
|
|
175
|
+
PrismaObserver.prototype.hookRawQueryMethods = function(prismaInstance) {
|
|
176
|
+
const self = this;
|
|
177
|
+
const queryMethods = ["$queryRaw", "$executeRaw", "$queryRawUnsafe", "$executeRawUnsafe"];
|
|
178
|
+
|
|
179
|
+
queryMethods.forEach(method => {
|
|
180
|
+
if (typeof prismaInstance[method] === 'function') {
|
|
181
|
+
shimmer.wrap(prismaInstance, method, function(original) {
|
|
182
|
+
return async function() {
|
|
183
|
+
const ctx = TraceContextManager.getCurrentContext();
|
|
184
|
+
if (!ctx) {
|
|
185
|
+
return original.apply(this, arguments);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const sql_step = new SqlStepX();
|
|
189
|
+
sql_step.start_time = ctx.getElapsedTime();
|
|
190
|
+
ctx.profile.push(sql_step);
|
|
191
|
+
ctx.footprint(`Prisma ${method} Start`);
|
|
192
|
+
|
|
193
|
+
ctx.sql_count = (ctx.sql_count || 0) + 1;
|
|
194
|
+
|
|
195
|
+
// SQL 쿼리 추출
|
|
196
|
+
let sql = "";
|
|
197
|
+
let psql = null;
|
|
198
|
+
|
|
199
|
+
if (arguments.length > 0) {
|
|
200
|
+
if (typeof arguments[0] === "object" && arguments[0].sql) {
|
|
201
|
+
// Tagged template으로 전달된 경우
|
|
202
|
+
sql = arguments[0].sql;
|
|
203
|
+
} else if (typeof arguments[0] === "string") {
|
|
204
|
+
// Raw string으로 전달된 경우
|
|
205
|
+
sql = arguments[0];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// SQL 파싱
|
|
210
|
+
if (sql && sql.length > 0) {
|
|
211
|
+
try {
|
|
212
|
+
psql = escapeLiteral(sql);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
Logger.printError("WHATAP-303", "escapeliteral error", e, false);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
sql = "";
|
|
218
|
+
psql = escapeLiteral(sql);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (psql != null) {
|
|
222
|
+
sql_step.hash = psql.sql;
|
|
223
|
+
}
|
|
224
|
+
sql_step.dbc = dbc_hash;
|
|
225
|
+
|
|
226
|
+
var els = new EscapeLiteralSQL(sql);
|
|
227
|
+
els.process();
|
|
228
|
+
|
|
229
|
+
ctx.active_sqlhash = sql_step.hash;
|
|
230
|
+
ctx.active_dbc = sql_step.dbc;
|
|
231
|
+
|
|
232
|
+
// 파라미터 정보 추출
|
|
233
|
+
if (conf.profile_sql_param_enabled) {
|
|
234
|
+
var params = Array.from(arguments).slice(1);
|
|
235
|
+
sql_step.setTrue(1);
|
|
236
|
+
var crc = { value: 0 };
|
|
237
|
+
sql_step.p1 = toParamBytes(psql.param, crc);
|
|
238
|
+
|
|
239
|
+
if (params.length > 0) {
|
|
240
|
+
const result = params.map((param) => {
|
|
241
|
+
if (typeof param === "string") {
|
|
242
|
+
return `'${param}'`;
|
|
243
|
+
}
|
|
244
|
+
return param;
|
|
245
|
+
}).toString();
|
|
246
|
+
sql_step.p2 = toParamBytes(result, crc);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
sql_step.pcrc = crc.value;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const result = await original.apply(this, arguments);
|
|
254
|
+
|
|
255
|
+
// SELECT 쿼리의 결과셋 처리
|
|
256
|
+
if (method === "$queryRaw" || method === "$queryRawUnsafe") {
|
|
257
|
+
if (Array.isArray(result)) {
|
|
258
|
+
var result_step = new ResultSetStep();
|
|
259
|
+
result_step.start_time = ctx.getElapsedTime();
|
|
260
|
+
result_step.elapsed = 0;
|
|
261
|
+
result_step.fetch = result.length;
|
|
262
|
+
result_step.sqlhash = psql.sql;
|
|
263
|
+
result_step.dbc = dbc_hash;
|
|
264
|
+
ctx.profile.push(result_step);
|
|
265
|
+
|
|
266
|
+
ctx.rs_count = ctx.rs_count ? ctx.rs_count + result.length : result.length;
|
|
267
|
+
ctx.rs_time = ctx.rs_time ? ctx.rs_time + sql_step.elapsed : sql_step.elapsed;
|
|
268
|
+
|
|
269
|
+
MeterSql.addFetch(result_step.dbc, result_step.fetch, 0);
|
|
270
|
+
StatSql.addFetch(result_step.dbc, result_step.sqlhash, result_step.fetch, 0);
|
|
271
|
+
|
|
272
|
+
TraceSQL.isTooManyRecords(sql_step, result_step.fetch, ctx);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// UPDATE/INSERT/DELETE 쿼리 결과 처리
|
|
277
|
+
if ((method === "$executeRaw" || method === "$executeRawUnsafe") && typeof result === "number") {
|
|
278
|
+
sql_step.updated = result;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
self._finishQuery(ctx, sql_step);
|
|
282
|
+
return result;
|
|
283
|
+
} catch (err) {
|
|
284
|
+
Logger.printError("WHATAP-304", `${method} error: ${err.message}`, err);
|
|
285
|
+
|
|
286
|
+
self._handleError(ctx, sql_step, err);
|
|
287
|
+
|
|
288
|
+
if (conf.trace_sql_error_stack && conf.trace_sql_error_depth) {
|
|
289
|
+
var traceDepth = conf.trace_sql_error_depth;
|
|
290
|
+
var errorStack = err.stack.split("\n");
|
|
291
|
+
|
|
292
|
+
if (errorStack.length > traceDepth) {
|
|
293
|
+
errorStack = errorStack.slice(0, traceDepth + 1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
ctx.error_message = errorStack.join("\n");
|
|
297
|
+
sql_step.error = ctx.error = StatError.addError("prisma-" + (err.code || "unknown"), err.message, ctx.service_hash, TextTypes.SQL, null);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
PrismaObserver.prototype.hookUseMiddleware = function(prismaInstance) {
|
|
309
|
+
const self = this;
|
|
310
|
+
|
|
311
|
+
// 원본 $use 함수를 저장
|
|
312
|
+
const originalUse = prismaInstance.$use;
|
|
313
|
+
|
|
314
|
+
// 우리만의 미들웨어 추가
|
|
315
|
+
if (typeof originalUse === 'function') {
|
|
316
|
+
// 간단한 미들웨어 추가 (모든 쿼리를 추적)
|
|
317
|
+
prismaInstance.$use(async (params, next) => {
|
|
318
|
+
var result;
|
|
319
|
+
const ctx = TraceContextManager.getCurrentContext();
|
|
320
|
+
if (!ctx) {
|
|
321
|
+
return next(params);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
var dbc_step = new DBCStep();
|
|
325
|
+
dbc_hash = HashUtil.hashFromString(dbc);
|
|
326
|
+
DataTextAgent.DBC.add(dbc_hash, dbc);
|
|
327
|
+
DataTextAgent.METHOD.add(dbc_hash, dbc);
|
|
328
|
+
DataTextAgent.ERROR.add(dbc_hash, dbc)
|
|
329
|
+
|
|
330
|
+
dbc_step.hash = dbc_hash;
|
|
331
|
+
dbc_step.start_time = ctx.getElapsedTime();
|
|
332
|
+
ctx.profile.push(dbc_step);
|
|
333
|
+
|
|
334
|
+
const sql_step = new SqlStepX();
|
|
335
|
+
sql_step.start_time = ctx.getElapsedTime();
|
|
336
|
+
ctx.profile.push(sql_step);
|
|
337
|
+
|
|
338
|
+
const modelName = params.model || 'unknown';
|
|
339
|
+
const action = params.action || 'unknown';
|
|
340
|
+
|
|
341
|
+
ctx.footprint(`Prisma ${modelName}.${action} Start`);
|
|
342
|
+
ctx.sql_count = (ctx.sql_count || 0) + 1;
|
|
343
|
+
|
|
344
|
+
// 사용된 모델과 액션으로 SQL 문자열 구성
|
|
345
|
+
const queryInfo = `Prisma ${action.toUpperCase()} ${modelName}`;
|
|
346
|
+
const queryHash = HashUtil.hashFromString(queryInfo);
|
|
347
|
+
|
|
348
|
+
DataTextAgent.SQL.add(queryHash, queryInfo);
|
|
349
|
+
sql_step.hash = queryHash;
|
|
350
|
+
sql_step.dbc = dbc_hash;
|
|
351
|
+
|
|
352
|
+
ctx.active_sqlhash = sql_step.hash;
|
|
353
|
+
ctx.active_dbc = sql_step.dbc;
|
|
354
|
+
|
|
355
|
+
// 쿼리 파라미터 정보 추출
|
|
356
|
+
if (conf.profile_sql_param_enabled) {
|
|
357
|
+
const paramsString = JSON.stringify(params.args || {});
|
|
358
|
+
sql_step.setTrue(1);
|
|
359
|
+
var crc = { value: 0 };
|
|
360
|
+
sql_step.p1 = toParamBytes(paramsString, crc);
|
|
361
|
+
sql_step.pcrc = crc.value;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
result = await next(params);
|
|
366
|
+
|
|
367
|
+
if (action === "queryRaw" || action === "executeRaw" || action === "queryRawUnsafe" || action === "executeRawUnsafe") {
|
|
368
|
+
// SQL 문자열 추출
|
|
369
|
+
let sqlString = "";
|
|
370
|
+
if (params.args && params.args.length > 0) {
|
|
371
|
+
// 첫 번째 인자가 문자열 배열인 경우 (tagged template)
|
|
372
|
+
if (params.args[0] && params.args[0].strings && Array.isArray(params.args[0].strings)) {
|
|
373
|
+
sqlString = params.args[0].strings.join('');
|
|
374
|
+
}
|
|
375
|
+
// 첫 번째 인자가 직접 문자열인 경우
|
|
376
|
+
else if (typeof params.args[0] === 'string') {
|
|
377
|
+
sqlString = params.args[0];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// SQL 문자열이 있으면 처리
|
|
382
|
+
if (sqlString && sqlString.length > 0) {
|
|
383
|
+
try {
|
|
384
|
+
var psql = escapeLiteral(sqlString);
|
|
385
|
+
if (psql != null) {
|
|
386
|
+
sql_step.hash = psql.sql;
|
|
387
|
+
// CRUD 타입 설정 (S: Select, U: Update 등)
|
|
388
|
+
// if (psql.type) {
|
|
389
|
+
// sql_step.crud = psql.type.charCodeAt(0);
|
|
390
|
+
// }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 추가 SQL 정보 처리
|
|
394
|
+
var els = new EscapeLiteralSQL(sqlString);
|
|
395
|
+
els.process();
|
|
396
|
+
|
|
397
|
+
// SQL 파라미터 처리
|
|
398
|
+
if (conf.profile_sql_param_enabled) {
|
|
399
|
+
var params = params.args.slice(1);
|
|
400
|
+
sql_step.setTrue(1);
|
|
401
|
+
var crc = {value: 0};
|
|
402
|
+
sql_step.p1 = toParamBytes(psql.param, crc);
|
|
403
|
+
|
|
404
|
+
if (params && params.length > 0) {
|
|
405
|
+
const result = params.map((param) => {
|
|
406
|
+
if (typeof param === 'string') {
|
|
407
|
+
return `'${param}'`;
|
|
408
|
+
}
|
|
409
|
+
return param;
|
|
410
|
+
}).toString();
|
|
411
|
+
sql_step.p2 = toParamBytes(result, crc);
|
|
412
|
+
}
|
|
413
|
+
sql_step.pcrc = crc.value;
|
|
414
|
+
}
|
|
415
|
+
} catch (e) {
|
|
416
|
+
Logger.printError("WHATAP-305", "escapeLiteral error", e, false);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Raw 쿼리 결과셋 처리
|
|
421
|
+
if ((action === "queryRaw" || action === "queryRawUnsafe") && result) {
|
|
422
|
+
let recordCount = 0;
|
|
423
|
+
|
|
424
|
+
if (Array.isArray(result)) {
|
|
425
|
+
recordCount = result.length;
|
|
426
|
+
} else if (result && typeof result === "object") {
|
|
427
|
+
recordCount = 1;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (recordCount > 0) {
|
|
431
|
+
// 가장 최근에 처리된 SQL 해시 사용 (Raw 쿼리에서 업데이트된 경우)
|
|
432
|
+
const sqlHashToUse = psql ? psql.sql : queryHash;
|
|
433
|
+
|
|
434
|
+
var result_step = new ResultSetStep();
|
|
435
|
+
result_step.start_time = ctx.getElapsedTime();
|
|
436
|
+
result_step.elapsed = 0;
|
|
437
|
+
result_step.fetch = recordCount;
|
|
438
|
+
result_step.sqlhash = sqlHashToUse;
|
|
439
|
+
result_step.dbc = dbc_hash;
|
|
440
|
+
ctx.profile.push(result_step);
|
|
441
|
+
|
|
442
|
+
ctx.rs_count = ctx.rs_count ? ctx.rs_count + recordCount : recordCount;
|
|
443
|
+
ctx.rs_time = ctx.rs_time ? ctx.rs_time + sql_step.elapsed : sql_step.elapsed;
|
|
444
|
+
|
|
445
|
+
MeterSql.addFetch(result_step.dbc, result_step.fetch, 0);
|
|
446
|
+
StatSql.addFetch(result_step.dbc, result_step.sqlhash, result_step.fetch, 0);
|
|
447
|
+
|
|
448
|
+
TraceSQL.isTooManyRecords(sql_step, result_step.fetch, ctx);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 결과셋 처리 (findMany, findFirst, findUnique 등)
|
|
454
|
+
if (["findMany", "findFirst", "findUnique"].includes(action)) {
|
|
455
|
+
let recordCount = 0;
|
|
456
|
+
|
|
457
|
+
if (Array.isArray(result)) {
|
|
458
|
+
recordCount = result.length;
|
|
459
|
+
} else if (result && typeof result === "object") {
|
|
460
|
+
recordCount = 1;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (recordCount > 0) {
|
|
464
|
+
var result_step = new ResultSetStep();
|
|
465
|
+
result_step.start_time = ctx.getElapsedTime();
|
|
466
|
+
result_step.elapsed = 0;
|
|
467
|
+
result_step.fetch = recordCount;
|
|
468
|
+
result_step.sqlhash = queryHash;
|
|
469
|
+
result_step.dbc = dbc_hash;
|
|
470
|
+
ctx.profile.push(result_step);
|
|
471
|
+
|
|
472
|
+
ctx.rs_count = ctx.rs_count ? ctx.rs_count + recordCount : recordCount;
|
|
473
|
+
ctx.rs_time = ctx.rs_time ? ctx.rs_time + sql_step.elapsed : sql_step.elapsed;
|
|
474
|
+
|
|
475
|
+
MeterSql.addFetch(result_step.dbc, result_step.fetch, 0);
|
|
476
|
+
StatSql.addFetch(result_step.dbc, result_step.sqlhash, result_step.fetch, 0);
|
|
477
|
+
|
|
478
|
+
TraceSQL.isTooManyRecords(sql_step, result_step.fetch, ctx);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 수정된 레코드 수 처리 (create, update, delete 등)
|
|
483
|
+
if (["create", "createMany", "update", "updateMany", "delete", "deleteMany"].includes(action)) {
|
|
484
|
+
if (result && result.count !== undefined) {
|
|
485
|
+
sql_step.updated = result.count;
|
|
486
|
+
} else if (result && typeof result === "object") {
|
|
487
|
+
sql_step.updated = 1;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// findUnique, create 등의 메서드에 대한 SQL 형식의 정보 구성
|
|
492
|
+
if (modelName !== 'unknown' && action !== 'queryRaw' && action !== 'executeRaw' &&
|
|
493
|
+
action !== 'queryRawUnsafe' && action !== 'executeRawUnsafe') {
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
// 모델 메서드를 SQL 스타일로 변환
|
|
497
|
+
let sqlStyleQuery = "";
|
|
498
|
+
|
|
499
|
+
// 액션에 따라 SQL 명령 결정
|
|
500
|
+
if (action.startsWith('find')) {
|
|
501
|
+
sqlStyleQuery = "SELECT ";
|
|
502
|
+
|
|
503
|
+
// select 필드가 있는 경우 해당 필드 사용
|
|
504
|
+
if (params.args && params.args.select) {
|
|
505
|
+
const fields = Object.keys(params.args.select)
|
|
506
|
+
.filter(key => params.args.select[key] === true);
|
|
507
|
+
|
|
508
|
+
if (fields.length > 0) {
|
|
509
|
+
sqlStyleQuery += fields.join(", ");
|
|
510
|
+
} else {
|
|
511
|
+
sqlStyleQuery += "*";
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
sqlStyleQuery += "*";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
sqlStyleQuery += ` FROM ${modelName}`;
|
|
518
|
+
|
|
519
|
+
// where 조건 추가
|
|
520
|
+
if (params.args && params.args.where) {
|
|
521
|
+
const conditions = [];
|
|
522
|
+
for (const [key, value] of Object.entries(params.args.where)) {
|
|
523
|
+
if (typeof value === 'object' && value !== null) {
|
|
524
|
+
// 복합 조건(contains, startsWith 등)
|
|
525
|
+
for (const [op, val] of Object.entries(value)) {
|
|
526
|
+
let sqlOp = "";
|
|
527
|
+
|
|
528
|
+
// Prisma 연산자를 SQL 연산자로 변환
|
|
529
|
+
switch (op) {
|
|
530
|
+
case 'equals': sqlOp = '='; break;
|
|
531
|
+
case 'not': sqlOp = '!='; break;
|
|
532
|
+
case 'in': sqlOp = 'IN'; break;
|
|
533
|
+
case 'notIn': sqlOp = 'NOT IN'; break;
|
|
534
|
+
case 'lt': sqlOp = '<'; break;
|
|
535
|
+
case 'lte': sqlOp = '<='; break;
|
|
536
|
+
case 'gt': sqlOp = '>'; break;
|
|
537
|
+
case 'gte': sqlOp = '>='; break;
|
|
538
|
+
case 'contains': sqlOp = 'LIKE'; break;
|
|
539
|
+
case 'startsWith': sqlOp = 'LIKE'; break;
|
|
540
|
+
case 'endsWith': sqlOp = 'LIKE'; break;
|
|
541
|
+
default: sqlOp = op;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
let sqlVal = val;
|
|
545
|
+
if (op === 'contains') {
|
|
546
|
+
sqlVal = `'%${val}%'`;
|
|
547
|
+
} else if (op === 'startsWith') {
|
|
548
|
+
sqlVal = `'${val}%'`;
|
|
549
|
+
} else if (op === 'endsWith') {
|
|
550
|
+
sqlVal = `'%${val}'`;
|
|
551
|
+
} else if (typeof val === 'string') {
|
|
552
|
+
sqlVal = `'${val}'`;
|
|
553
|
+
} else if (Array.isArray(val)) {
|
|
554
|
+
sqlVal = `(${val.map(v => typeof v === 'string' ? `'${v}'` : v).join(', ')})`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
conditions.push(`${key} ${sqlOp} ${sqlVal}`);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
// 단순 조건
|
|
561
|
+
let formattedValue = typeof value === 'string' ? `'${value}'` : value;
|
|
562
|
+
conditions.push(`${key} = ${formattedValue}`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (conditions.length > 0) {
|
|
567
|
+
sqlStyleQuery += ` WHERE ${conditions.join(' AND ')}`;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 정렬 조건 추가
|
|
572
|
+
if (params.args && params.args.orderBy) {
|
|
573
|
+
const orderClauses = [];
|
|
574
|
+
|
|
575
|
+
if (Array.isArray(params.args.orderBy)) {
|
|
576
|
+
for (const item of params.args.orderBy) {
|
|
577
|
+
for (const [field, dir] of Object.entries(item)) {
|
|
578
|
+
orderClauses.push(`${field} ${dir}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
for (const [field, dir] of Object.entries(params.args.orderBy)) {
|
|
583
|
+
orderClauses.push(`${field} ${dir}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (orderClauses.length > 0) {
|
|
588
|
+
sqlStyleQuery += ` ORDER BY ${orderClauses.join(', ')}`;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// 페이징 정보 추가
|
|
593
|
+
if (params.args) {
|
|
594
|
+
if (params.args.skip !== undefined) {
|
|
595
|
+
sqlStyleQuery += ` OFFSET ${params.args.skip}`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (params.args.take !== undefined) {
|
|
599
|
+
sqlStyleQuery += ` LIMIT ${params.args.take}`;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
} else if (action.startsWith('create')) {
|
|
604
|
+
sqlStyleQuery = `INSERT INTO ${modelName}`;
|
|
605
|
+
|
|
606
|
+
if (params.args && params.args.data) {
|
|
607
|
+
const columns = [];
|
|
608
|
+
const values = [];
|
|
609
|
+
|
|
610
|
+
for (const [key, val] of Object.entries(params.args.data)) {
|
|
611
|
+
columns.push(key);
|
|
612
|
+
if (typeof val === 'string') {
|
|
613
|
+
values.push(`'${val}'`);
|
|
614
|
+
} else if (val === null) {
|
|
615
|
+
values.push('NULL');
|
|
616
|
+
} else if (typeof val === 'object') {
|
|
617
|
+
values.push(`'${JSON.stringify(val)}'`);
|
|
618
|
+
} else {
|
|
619
|
+
values.push(val);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
sqlStyleQuery += ` (${columns.join(', ')}) VALUES (${values.join(', ')})`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
} else if (action.startsWith('update')) {
|
|
627
|
+
sqlStyleQuery = `UPDATE ${modelName}`;
|
|
628
|
+
|
|
629
|
+
if (params.args && params.args.data) {
|
|
630
|
+
const setExpressions = [];
|
|
631
|
+
|
|
632
|
+
for (const [key, val] of Object.entries(params.args.data)) {
|
|
633
|
+
let formattedValue;
|
|
634
|
+
if (typeof val === 'string') {
|
|
635
|
+
formattedValue = `'${val}'`;
|
|
636
|
+
} else if (val === null) {
|
|
637
|
+
formattedValue = 'NULL';
|
|
638
|
+
} else if (typeof val === 'object') {
|
|
639
|
+
formattedValue = `'${JSON.stringify(val)}'`;
|
|
640
|
+
} else {
|
|
641
|
+
formattedValue = val;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
setExpressions.push(`${key} = ${formattedValue}`);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (setExpressions.length > 0) {
|
|
648
|
+
sqlStyleQuery += ` SET ${setExpressions.join(', ')}`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// where 조건 추가
|
|
653
|
+
if (params.args && params.args.where) {
|
|
654
|
+
const conditions = [];
|
|
655
|
+
for (const [key, value] of Object.entries(params.args.where)) {
|
|
656
|
+
if (typeof value === 'object' && value !== null) {
|
|
657
|
+
for (const [op, val] of Object.entries(value)) {
|
|
658
|
+
let sqlOp = op === 'equals' ? '=' : op;
|
|
659
|
+
let sqlVal = typeof val === 'string' ? `'${val}'` : val;
|
|
660
|
+
conditions.push(`${key} ${sqlOp} ${sqlVal}`);
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
let formattedValue = typeof value === 'string' ? `'${value}'` : value;
|
|
664
|
+
conditions.push(`${key} = ${formattedValue}`);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (conditions.length > 0) {
|
|
669
|
+
sqlStyleQuery += ` WHERE ${conditions.join(' AND ')}`;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
} else if (action.startsWith('delete')) {
|
|
674
|
+
sqlStyleQuery = `DELETE FROM ${modelName}`;
|
|
675
|
+
|
|
676
|
+
// where 조건 추가
|
|
677
|
+
if (params.args && params.args.where) {
|
|
678
|
+
const conditions = [];
|
|
679
|
+
for (const [key, value] of Object.entries(params.args.where)) {
|
|
680
|
+
if (typeof value === 'object' && value !== null) {
|
|
681
|
+
for (const [op, val] of Object.entries(value)) {
|
|
682
|
+
let sqlOp = op === 'equals' ? '=' : op;
|
|
683
|
+
let sqlVal = typeof val === 'string' ? `'${val}'` : val;
|
|
684
|
+
conditions.push(`${key} ${sqlOp} ${sqlVal}`);
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
let formattedValue = typeof value === 'string' ? `'${value}'` : value;
|
|
688
|
+
conditions.push(`${key} = ${formattedValue}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (conditions.length > 0) {
|
|
693
|
+
sqlStyleQuery += ` WHERE ${conditions.join(' AND ')}`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
} else {
|
|
697
|
+
// 기타 액션은 기본 형식으로
|
|
698
|
+
sqlStyleQuery = `${action.toUpperCase()} ${modelName}`;
|
|
699
|
+
|
|
700
|
+
if (params.args) {
|
|
701
|
+
sqlStyleQuery += ` ${JSON.stringify(params.args)}`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// 생성된 SQL 쿼리에 escapeLiteral 적용하여 처리
|
|
706
|
+
try {
|
|
707
|
+
var psql = escapeLiteral(sqlStyleQuery);
|
|
708
|
+
if (psql != null) {
|
|
709
|
+
sql_step.hash = psql.sql;
|
|
710
|
+
// CRUD 타입 설정
|
|
711
|
+
if (psql.type) {
|
|
712
|
+
sql_step.crud = psql.type.charCodeAt(0);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// 추가 SQL 정보 처리
|
|
717
|
+
var els = new EscapeLiteralSQL(sqlStyleQuery);
|
|
718
|
+
els.process();
|
|
719
|
+
|
|
720
|
+
// SQL 파라미터 처리
|
|
721
|
+
if (conf.profile_sql_param_enabled) {
|
|
722
|
+
sql_step.setTrue(1);
|
|
723
|
+
var crc = {value: 0};
|
|
724
|
+
sql_step.p1 = toParamBytes(psql.param, crc);
|
|
725
|
+
|
|
726
|
+
// 원래 인자를 파라미터로 추가
|
|
727
|
+
const paramsString = JSON.stringify(params.args || {});
|
|
728
|
+
sql_step.p2 = toParamBytes(paramsString, crc);
|
|
729
|
+
sql_step.pcrc = crc.value;
|
|
730
|
+
}
|
|
731
|
+
} catch (e) {
|
|
732
|
+
Logger.printError("WHATAP-306", "escapeLiteral error for model method", e, false);
|
|
733
|
+
}
|
|
734
|
+
} catch (e) {
|
|
735
|
+
Logger.printError("WHATAP-307", "Error creating SQL-style query", e, false);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
sql_step.elapsed = ctx.getElapsedTime() - sql_step.start_time;
|
|
740
|
+
ctx.sql_time = (ctx.sql_time || 0) + sql_step.elapsed;
|
|
741
|
+
|
|
742
|
+
TraceSQL.isSlowSQL(ctx);
|
|
743
|
+
|
|
744
|
+
MeterSql.add(dbc_hash, sql_step.elapsed, false);
|
|
745
|
+
StatSql.addSqlTime(ctx.service_hash, sql_step.dbc, sql_step.hash, sql_step.elapsed, false, 0);
|
|
746
|
+
|
|
747
|
+
ctx.footprint(`Prisma ${modelName}.${action} Done`);
|
|
748
|
+
|
|
749
|
+
return result;
|
|
750
|
+
} catch (err) {
|
|
751
|
+
Logger.printError("WHATAP-308", `Middleware error in ${modelName}.${action}: ${err.message}`, err, false);
|
|
752
|
+
|
|
753
|
+
sql_step.elapsed = ctx.getElapsedTime() - sql_step.start_time;
|
|
754
|
+
ctx.sql_time = (ctx.sql_time || 0) + sql_step.elapsed;
|
|
755
|
+
|
|
756
|
+
TraceSQL.isSlowSQL(ctx);
|
|
757
|
+
|
|
758
|
+
MeterSql.add(dbc_hash, sql_step.elapsed, true);
|
|
759
|
+
StatSql.addSqlTime(ctx.service_hash, sql_step.dbc, sql_step.hash, sql_step.elapsed, true, 0);
|
|
760
|
+
|
|
761
|
+
// 에러 처리
|
|
762
|
+
try {
|
|
763
|
+
sql_step.error = StatError.addError("prisma-" + (err.code || "unknown"), err.message, ctx.service_hash, TextTypes.SQL, sql_step.hash);
|
|
764
|
+
|
|
765
|
+
if (ctx.error.isZero()) {
|
|
766
|
+
ctx.error = sql_step.error;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (conf.trace_sql_error_stack && conf.trace_sql_error_depth) {
|
|
770
|
+
var traceDepth = conf.trace_sql_error_depth;
|
|
771
|
+
var errorStack = err.stack.split("\n");
|
|
772
|
+
|
|
773
|
+
if (errorStack.length > traceDepth) {
|
|
774
|
+
errorStack = errorStack.slice(0, traceDepth + 1);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
ctx.error_message = errorStack.join("\n");
|
|
778
|
+
}
|
|
779
|
+
} catch (e) {
|
|
780
|
+
Logger.printError("WHATAP-309", "Error handling failed", e, false);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
ctx.footprint(`Prisma ${modelName}.${action} Error`);
|
|
784
|
+
throw err;
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// 모델 메서드 직접 후킹
|
|
791
|
+
// PrismaObserver.prototype.hookModelMethods = function(prismaInstance) {
|
|
792
|
+
// const self = this;
|
|
793
|
+
//
|
|
794
|
+
// // Prisma에서 모든 모델 가져오기
|
|
795
|
+
// let models = [];
|
|
796
|
+
// try {
|
|
797
|
+
// // DMMF를 통해 모델 이름 얻기
|
|
798
|
+
// if (prismaInstance._baseDmmf && prismaInstance._baseDmmf.modelMap) {
|
|
799
|
+
// models = Object.keys(prismaInstance._baseDmmf.modelMap);
|
|
800
|
+
// } else if (prismaInstance._dmmf && prismaInstance._dmmf.modelMap) {
|
|
801
|
+
// models = Object.keys(prismaInstance._dmmf.modelMap);
|
|
802
|
+
// }
|
|
803
|
+
// } catch (e) {
|
|
804
|
+
// Logger.printError("WHATAP-PRISMA", "Failed to get models", e, false);
|
|
805
|
+
// }
|
|
806
|
+
//
|
|
807
|
+
// // 모델 메서드 목록
|
|
808
|
+
// const methods = [
|
|
809
|
+
// "findUnique", "findFirst", "findMany",
|
|
810
|
+
// "create", "createMany",
|
|
811
|
+
// "update", "updateMany",
|
|
812
|
+
// "upsert",
|
|
813
|
+
// "delete", "deleteMany",
|
|
814
|
+
// "count", "aggregate", "groupBy"
|
|
815
|
+
// ];
|
|
816
|
+
//
|
|
817
|
+
// Logger.print("WHATAP-PRISMA", `Found models: ${models.join(', ')}`, false);
|
|
818
|
+
//
|
|
819
|
+
// // 각 모델에 대해 메서드 후킹
|
|
820
|
+
// models.forEach(model => {
|
|
821
|
+
// if (prismaInstance[model]) {
|
|
822
|
+
// methods.forEach(method => {
|
|
823
|
+
// if (typeof prismaInstance[model][method] === 'function') {
|
|
824
|
+
// shimmer.wrap(prismaInstance[model], method, function(original) {
|
|
825
|
+
// return async function() {
|
|
826
|
+
// const ctx = TraceContextManager.getCurrentContext();
|
|
827
|
+
// if (!ctx) {
|
|
828
|
+
// return original.apply(this, arguments);
|
|
829
|
+
// }
|
|
830
|
+
//
|
|
831
|
+
// Logger.print("WHATAP-PRISMA", `Direct model method called: ${model}.${method}`, false);
|
|
832
|
+
//
|
|
833
|
+
// const sql_step = new SqlStepX();
|
|
834
|
+
// sql_step.start_time = ctx.getElapsedTime();
|
|
835
|
+
// ctx.profile.push(sql_step);
|
|
836
|
+
// ctx.footprint(`Prisma ${model}.${method} Start (Direct)`);
|
|
837
|
+
//
|
|
838
|
+
// ctx.sql_count = (ctx.sql_count || 0) + 1;
|
|
839
|
+
//
|
|
840
|
+
// // 쿼리 정보
|
|
841
|
+
// const queryInfo = `${method.toUpperCase()} ${model}`;
|
|
842
|
+
// const queryHash = HashUtil.hashFromString(queryInfo);
|
|
843
|
+
//
|
|
844
|
+
// DataTextAgent.SQL.add(queryHash, queryInfo);
|
|
845
|
+
// sql_step.hash = queryHash;
|
|
846
|
+
// sql_step.dbc = dbc_hash;
|
|
847
|
+
//
|
|
848
|
+
// ctx.active_sqlhash = sql_step.hash;
|
|
849
|
+
// ctx.active_dbc = sql_step.dbc;
|
|
850
|
+
//
|
|
851
|
+
// // 쿼리 파라미터
|
|
852
|
+
// if (conf.profile_sql_param_enabled) {
|
|
853
|
+
// const argsString = JSON.stringify(arguments[0] || {});
|
|
854
|
+
// sql_step.setTrue(1);
|
|
855
|
+
// var crc = { value: 0 };
|
|
856
|
+
// sql_step.p1 = toParamBytes(argsString, crc);
|
|
857
|
+
// sql_step.pcrc = crc.value;
|
|
858
|
+
// }
|
|
859
|
+
//
|
|
860
|
+
// try {
|
|
861
|
+
// const result = await original.apply(this, arguments);
|
|
862
|
+
//
|
|
863
|
+
// // 결과셋 처리
|
|
864
|
+
// if (["findMany", "findFirst", "findUnique"].includes(method)) {
|
|
865
|
+
// let recordCount = 0;
|
|
866
|
+
//
|
|
867
|
+
// if (Array.isArray(result)) {
|
|
868
|
+
// recordCount = result.length;
|
|
869
|
+
// } else if (result && typeof result === "object") {
|
|
870
|
+
// recordCount = 1;
|
|
871
|
+
// }
|
|
872
|
+
//
|
|
873
|
+
// if (recordCount > 0) {
|
|
874
|
+
// var result_step = new ResultSetStep();
|
|
875
|
+
// result_step.start_time = ctx.getElapsedTime();
|
|
876
|
+
// result_step.elapsed = 0;
|
|
877
|
+
// result_step.fetch = recordCount;
|
|
878
|
+
// result_step.sqlhash = queryHash;
|
|
879
|
+
// result_step.dbc = dbc_hash;
|
|
880
|
+
// ctx.profile.push(result_step);
|
|
881
|
+
//
|
|
882
|
+
// ctx.rs_count = ctx.rs_count ? ctx.rs_count + recordCount : recordCount;
|
|
883
|
+
// ctx.rs_time = ctx.rs_time ? ctx.rs_time + sql_step.elapsed : sql_step.elapsed;
|
|
884
|
+
//
|
|
885
|
+
// MeterSql.addFetch(result_step.dbc, result_step.fetch, 0);
|
|
886
|
+
// StatSql.addFetch(result_step.dbc, result_step.sqlhash, result_step.fetch, 0);
|
|
887
|
+
//
|
|
888
|
+
// TraceSQL.isTooManyRecords(sql_step, result_step.fetch, ctx);
|
|
889
|
+
// }
|
|
890
|
+
// }
|
|
891
|
+
//
|
|
892
|
+
// // 수정된 레코드 처리
|
|
893
|
+
// if (["create", "createMany", "update", "updateMany", "delete", "deleteMany"].includes(method)) {
|
|
894
|
+
// if (result && result.count !== undefined) {
|
|
895
|
+
// sql_step.updated = result.count;
|
|
896
|
+
// } else if (result && typeof result === "object") {
|
|
897
|
+
// sql_step.updated = 1;
|
|
898
|
+
// }
|
|
899
|
+
// }
|
|
900
|
+
//
|
|
901
|
+
// sql_step.elapsed = ctx.getElapsedTime() - sql_step.start_time;
|
|
902
|
+
// ctx.sql_time = (ctx.sql_time || 0) + sql_step.elapsed;
|
|
903
|
+
//
|
|
904
|
+
// TraceSQL.isSlowSQL(ctx);
|
|
905
|
+
//
|
|
906
|
+
// MeterSql.add(dbc_hash, sql_step.elapsed, false);
|
|
907
|
+
// StatSql.addSqlTime(ctx.service_hash, sql_step.dbc, sql_step.hash, sql_step.elapsed, false, 0);
|
|
908
|
+
//
|
|
909
|
+
// ctx.footprint(`Prisma ${model}.${method} Done (Direct)`);
|
|
910
|
+
// Logger.print("WHATAP-PRISMA", `Direct model method completed: ${model}.${method}`, false);
|
|
911
|
+
//
|
|
912
|
+
// return result;
|
|
913
|
+
// } catch (err) {
|
|
914
|
+
// Logger.printError("WHATAP-PRISMA", `Direct model method error in ${model}.${method}: ${err.message}`, err);
|
|
915
|
+
//
|
|
916
|
+
// sql_step.elapsed = ctx.getElapsedTime() - sql_step.start_time;
|
|
917
|
+
// ctx.sql_time = (ctx.sql_time || 0) + sql_step.elapsed;
|
|
918
|
+
//
|
|
919
|
+
// TraceSQL.isSlowSQL(ctx);
|
|
920
|
+
//
|
|
921
|
+
// MeterSql.add(dbc_hash, sql_step.elapsed, true);
|
|
922
|
+
// StatSql.addSqlTime(ctx.service_hash, sql_step.dbc, sql_step.hash, sql_step.elapsed, true, 0);
|
|
923
|
+
//
|
|
924
|
+
// try {
|
|
925
|
+
// sql_step.error = StatError.addError("prisma-" + (err.code || "unknown"), err.message, ctx.service_hash, TextTypes.SQL, sql_step.hash);
|
|
926
|
+
//
|
|
927
|
+
// if (ctx.error.isZero()) {
|
|
928
|
+
// ctx.error = sql_step.error;
|
|
929
|
+
// }
|
|
930
|
+
//
|
|
931
|
+
// if (conf.trace_sql_error_stack && conf.trace_sql_error_depth) {
|
|
932
|
+
// var traceDepth = conf.trace_sql_error_depth;
|
|
933
|
+
// var errorStack = err.stack.split("\n");
|
|
934
|
+
//
|
|
935
|
+
// if (errorStack.length > traceDepth) {
|
|
936
|
+
// errorStack = errorStack.slice(0, traceDepth + 1);
|
|
937
|
+
// }
|
|
938
|
+
//
|
|
939
|
+
// ctx.error_message = errorStack.join("\n");
|
|
940
|
+
// }
|
|
941
|
+
// } catch (e) {
|
|
942
|
+
// Logger.printError("WHATAP-PRISMA", "Error handling failed", e, false);
|
|
943
|
+
// }
|
|
944
|
+
//
|
|
945
|
+
// ctx.footprint(`Prisma ${model}.${method} Error (Direct)`);
|
|
946
|
+
// throw err;
|
|
947
|
+
// }
|
|
948
|
+
// };
|
|
949
|
+
// });
|
|
950
|
+
// }
|
|
951
|
+
// });
|
|
952
|
+
// }
|
|
953
|
+
// });
|
|
954
|
+
// };
|
|
955
|
+
|
|
956
|
+
PrismaObserver.prototype._finishQuery = function(ctx, sql_step) {
|
|
957
|
+
sql_step.elapsed = ctx.getElapsedTime() - sql_step.start_time;
|
|
958
|
+
ctx.sql_time = (ctx.sql_time || 0) + sql_step.elapsed;
|
|
959
|
+
|
|
960
|
+
TraceSQL.isSlowSQL(ctx);
|
|
961
|
+
|
|
962
|
+
MeterSql.add(dbc_hash, sql_step.elapsed, false);
|
|
963
|
+
StatSql.addSqlTime(ctx.service_hash, sql_step.dbc, sql_step.hash, sql_step.elapsed, false, 0);
|
|
964
|
+
|
|
965
|
+
ctx.footprint("Prisma Query Done");
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
PrismaObserver.prototype._handleError = function(ctx, sql_step, err) {
|
|
969
|
+
sql_step.elapsed = ctx.getElapsedTime() - sql_step.start_time;
|
|
970
|
+
ctx.sql_time = (ctx.sql_time || 0) + sql_step.elapsed;
|
|
971
|
+
|
|
972
|
+
TraceSQL.isSlowSQL(ctx);
|
|
973
|
+
|
|
974
|
+
MeterSql.add(dbc_hash, sql_step.elapsed, true);
|
|
975
|
+
StatSql.addSqlTime(ctx.service_hash, sql_step.dbc, sql_step.hash, sql_step.elapsed, true, 0);
|
|
976
|
+
|
|
977
|
+
try {
|
|
978
|
+
sql_step.error = StatError.addError("prisma-" + (err.code || "unknown"), err.message, ctx.service_hash, TextTypes.SQL, sql_step.hash);
|
|
979
|
+
|
|
980
|
+
if (ctx.error.isZero()) {
|
|
981
|
+
ctx.error = sql_step.error;
|
|
982
|
+
}
|
|
983
|
+
} catch (e) {
|
|
984
|
+
Logger.printError("WHATAP-310", "Error handling failed", e, false);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
ctx.footprint("Prisma Query Error");
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
var toParamBytes = function(p, crc) {
|
|
991
|
+
if (p == null || p.length === 0) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
return ParamSecurity.encrypt(Buffer.from(p, "utf8"), crc);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
var checkedSql = new IntKeyMap(2000).setMax(2000);
|
|
1002
|
+
var nonLiteSql = new IntKeyMap(5000).setMax(5000);
|
|
1003
|
+
var date = DateUtil.yyyymmdd();
|
|
1004
|
+
|
|
1005
|
+
function escapeLiteral(sql) {
|
|
1006
|
+
if (sql == null) {
|
|
1007
|
+
sql = "";
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (date !== DateUtil.yyyymmdd()) {
|
|
1011
|
+
checkedSql.clear();
|
|
1012
|
+
nonLiteSql.clear();
|
|
1013
|
+
date = DateUtil.yyyymmdd();
|
|
1014
|
+
Logger.print("WHATAP-SQL-CLEAR", "PrismaObserver CLEAR OK!!!!!!!!!!!!!!!!", false);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
var sqlHash = HashUtil.hashFromString(sql);
|
|
1018
|
+
var psql = nonLiteSql.get(sqlHash);
|
|
1019
|
+
|
|
1020
|
+
if (psql != null) {
|
|
1021
|
+
return psql;
|
|
1022
|
+
}
|
|
1023
|
+
psql = checkedSql.get(sqlHash);
|
|
1024
|
+
|
|
1025
|
+
if (psql != null) {
|
|
1026
|
+
return psql;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
var els = new EscapeLiteralSQL(sql);
|
|
1030
|
+
els.process();
|
|
1031
|
+
|
|
1032
|
+
var hash = HashUtil.hashFromString(els.getParsedSql());
|
|
1033
|
+
DataTextAgent.SQL.add(hash, els.getParsedSql());
|
|
1034
|
+
|
|
1035
|
+
if (hash === sqlHash) {
|
|
1036
|
+
psql = new ParsedSql(els.sqlType, hash, null);
|
|
1037
|
+
nonLiteSql.put(sqlHash, psql);
|
|
1038
|
+
} else {
|
|
1039
|
+
psql = new ParsedSql(els.sqlType, hash, els.getParameter());
|
|
1040
|
+
checkedSql.put(sqlHash, psql);
|
|
1041
|
+
}
|
|
1042
|
+
return psql;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
exports.PrismaObserver = PrismaObserver;
|
package/package.json
CHANGED