whatap 1.0.9 → 1.0.10

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.
Binary file
@@ -278,7 +278,9 @@ var ConfigDefault = {
278
278
  "prisma_read_func_name": str("prisma_read_func_name", "read"),
279
279
  "prisma_database_url_name": str("prisma_database_url_name", "DATABASE_URL"),
280
280
 
281
- "metering_tagcount_enabled": bool("metering_tagcount_enabled", false)
281
+ "metering_tagcount_enabled": bool("metering_tagcount_enabled", false),
282
+
283
+ "trace_cron_enabled": bool("trace_cron_enabled", true)
282
284
  };
283
285
 
284
286
  ConfigDefault._hook_method_ignore_prefix = ConfigDefault.hook_method_ignore_prefixes.split(',');
@@ -191,15 +191,15 @@ PackageCtrHelper.hookMethod = function(obj, modulePath, methodName, className) {
191
191
  return originalMethod.apply(this, arguments);
192
192
  }
193
193
 
194
- var startTime = Date.now();
194
+ var startTime = ctx.start_time = Date.now();
195
195
  var error = null;
196
196
  var result = null;
197
197
 
198
198
  // Capture original arguments once for later use
199
199
  var originalArgs = arguments;
200
200
 
201
- // Format: "ClassName.methodName" if className exists, otherwise "modulePath/methodName"
202
- var methodFullPath = className ? className + '.' + methodName : modulePath + '/' + methodName;
201
+ // Format: "ClassName.methodName" if className exists, otherwise "NULL.methodName"
202
+ var methodFullPath = className ? className + '.' + methodName : 'NULL.' + methodName;
203
203
 
204
204
  // Convert arguments to string once
205
205
  var argsString = '';
package/lib/core/agent.js CHANGED
@@ -35,7 +35,8 @@ var Interceptor = require('./interceptor').Interceptor,
35
35
  ApolloObserver = require('../observers/apollo-server-observer').ApolloServerObserver,
36
36
  PrismaObserver = require('../observers/prisma-observer').PrismaObserver,
37
37
  OracleObserver = require('../observers/oracle-observer').OracleObserver,
38
- CustomMethodObserver = require('../observers/custom-method-observer').CustomMethodObserver;
38
+ CustomMethodObserver = require('../observers/custom-method-observer').CustomMethodObserver,
39
+ CronObserver = require('../observers/cron-observer').CronObserver;
39
40
 
40
41
 
41
42
  var Configuration = require('./../conf/configure'),
@@ -965,12 +966,19 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
965
966
  if (pid) {
966
967
  try {
967
968
  if (this.isGoAgentRunning(pid)) {
968
- Logger.print("WHATAP-106", `Found existing agent with PID ${pid}, terminating...`, false);
969
- try {
970
- process.kill(parseInt(pid), 'SIGKILL');
971
- Logger.print("WHATAP-107", `Successfully terminated existing agent with PID ${pid}`, false);
972
- } catch (killError) {
973
- Logger.printError("WHATAP-108", `Error terminating process with PID ${pid}`, killError, false);
969
+ // Windows: 기존 프로세스가 실행 중이면 스킵 (Python agent와 동일한 동작)
970
+ // Unix/Linux: 기존 프로세스 종료 후 새로 시작
971
+ if (process.platform === 'win32') {
972
+ Logger.print("WHATAP-106", `Agent already running (PID: ${pid}). Skipping duplicate execution.`, false);
973
+ return;
974
+ } else {
975
+ Logger.print("WHATAP-106", `Found existing agent with PID ${pid}, terminating...`, false);
976
+ try {
977
+ process.kill(parseInt(pid), 'SIGKILL');
978
+ Logger.print("WHATAP-107", `Successfully terminated existing agent with PID ${pid}`, false);
979
+ } catch (killError) {
980
+ Logger.printError("WHATAP-108", `Error terminating process with PID ${pid}`, killError, false);
981
+ }
974
982
  }
975
983
  }
976
984
  } catch (e) {
@@ -983,10 +991,16 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
983
991
 
984
992
  try {
985
993
  // 바이너리 경로 설정
986
- const agentPath = path.join(whatapHome, AGENT_NAME);
987
994
  const platform = process.platform;
988
995
  const architecture = ARCH[process.arch] || process.arch;
989
- const sourcePath = path.join(__dirname, '..', '..', 'agent', platform, architecture, AGENT_NAME);
996
+
997
+ const binaryName = platform === 'win32' ? AGENT_NAME + '.exe' : AGENT_NAME;
998
+ const agentPath = path.join(whatapHome, binaryName);
999
+
1000
+ // Windows는 architecture 하위 디렉토리 없이 단일 바이너리 사용
1001
+ const sourcePath = platform === 'win32'
1002
+ ? path.join(__dirname, '..', '..', 'agent', 'windows', binaryName)
1003
+ : path.join(__dirname, '..', '..', 'agent', platform, architecture, AGENT_NAME);
990
1004
 
991
1005
  // sourcePath 존재 여부 확인
992
1006
  if (!fs.existsSync(sourcePath)) {
@@ -1004,8 +1018,10 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
1004
1018
  Logger.print("WHATAP-037", `Symlink failed (${e.code}), copying binary instead`, false);
1005
1019
  try {
1006
1020
  fs.copyFileSync(sourcePath, agentPath);
1007
- // 실행 권한 부여
1008
- fs.chmodSync(agentPath, 0o755);
1021
+ // 실행 권한 부여 (Windows는 불필요)
1022
+ if (platform !== 'win32') {
1023
+ fs.chmodSync(agentPath, 0o755);
1024
+ }
1009
1025
  Logger.print("WHATAP-221", `Binary copied and made executable at ${agentPath}`, false);
1010
1026
  } catch (copyErr) {
1011
1027
  throw new Error(`Failed to copy agent binary: ${copyErr.message}`);
@@ -1014,12 +1030,14 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
1014
1030
  }
1015
1031
  }
1016
1032
 
1017
- // agentPath 실행 권한 확인
1018
- try {
1019
- fs.accessSync(agentPath, fs.constants.X_OK);
1020
- } catch (e) {
1021
- Logger.print("WHATAP-222", `Agent binary not executable, adding execute permission`, false);
1022
- fs.chmodSync(agentPath, 0o755);
1033
+ // agentPath 실행 권한 확인 (Windows는 스킵)
1034
+ if (platform !== 'win32') {
1035
+ try {
1036
+ fs.accessSync(agentPath, fs.constants.X_OK);
1037
+ } catch (e) {
1038
+ Logger.print("WHATAP-222", `Agent binary not executable, adding execute permission`, false);
1039
+ fs.chmodSync(agentPath, 0o755);
1040
+ }
1023
1041
  }
1024
1042
 
1025
1043
  // run 디렉토리 생성
@@ -1068,16 +1086,27 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
1068
1086
  Object.assign(newEnv, opts);
1069
1087
 
1070
1088
  // 에이전트 프로세스 시작
1071
- const agentProcess = child_process.spawn(
1072
- agentPath,
1073
- ['-t', '2', '-d', '1'],
1074
- {
1075
- cwd: whatapHome,
1076
- env: newEnv,
1077
- stdio: ['pipe', 'pipe', 'pipe'],
1078
- detached: false
1079
- }
1080
- );
1089
+ // Windows: -t 2 foreground (detached 모드)
1090
+ // Unix/Linux: -t 2 -d 1 (daemon 모드)
1091
+ const spawnArgs = platform === 'win32'
1092
+ ? ['-t', '2', 'foreground']
1093
+ : ['-t', '2', '-d', '1'];
1094
+
1095
+ const spawnOptions = {
1096
+ cwd: whatapHome,
1097
+ env: newEnv,
1098
+ stdio: ['pipe', 'pipe', 'pipe'],
1099
+ detached: platform === 'win32' // Windows에서는 detached 모드
1100
+ };
1101
+
1102
+ // Windows에서는 DETACHED_PROCESS 플래그 추가
1103
+ if (platform === 'win32') {
1104
+ spawnOptions.detached = true;
1105
+ // Windows 전용: 새로운 콘솔 윈도우 생성 방지
1106
+ spawnOptions.windowsHide = true;
1107
+ }
1108
+
1109
+ const agentProcess = child_process.spawn(agentPath, spawnArgs, spawnOptions);
1081
1110
 
1082
1111
  const MAX_BUFFER_SIZE = 50000; // 50KB - prevent memory leak
1083
1112
  let stdout = '';
@@ -1322,6 +1351,7 @@ NodeAgent.prototype.loadObserves = function() {
1322
1351
  observes.push(ApolloObserver);
1323
1352
  observes.push(PrismaObserver);
1324
1353
  observes.push(OracleObserver);
1354
+ observes.push(CronObserver);
1325
1355
 
1326
1356
  var packageToObserve = {};
1327
1357
  observes.forEach(function(observeObj) {
@@ -0,0 +1,279 @@
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
+ AsyncSender = require('../udp/async_sender'),
11
+ PacketTypeEnum = require('../udp/packet_type_enum');
12
+ const shimmer = require('../core/shimmer');
13
+
14
+ var CronObserver = function (agent) {
15
+ this.agent = agent;
16
+ this.packages = ['node-cron', 'cron', 'node-schedule'];
17
+ };
18
+
19
+ CronObserver.prototype.__createCronJobObserver = function (callback, cronExpression, jobName) {
20
+ var self = this;
21
+
22
+ // callback이 async 함수인지 확인
23
+ const isAsync = callback.constructor.name === 'AsyncFunction';
24
+
25
+ return function wrappedCronCallback() {
26
+ return TraceContextManager._asyncLocalStorage.run(self.__initCtx(cronExpression, jobName), () => {
27
+ var ctx = TraceContextManager._asyncLocalStorage.getStore();
28
+
29
+ if (!ctx) {
30
+ return callback.apply(this, arguments);
31
+ }
32
+
33
+ try {
34
+ ctx.service_name = jobName || '/';
35
+ let hostname = '0.0.0.0';
36
+
37
+ let startDatas = [
38
+ hostname,
39
+ ctx.service_name,
40
+ '0.0.0.0',
41
+ '',
42
+ '',
43
+ String(0),
44
+ String(false),
45
+ ''
46
+ ];
47
+
48
+ AsyncSender.send_packet(PacketTypeEnum.TX_START, ctx, startDatas);
49
+
50
+ // 원본 callback 실행
51
+ const result = callback.apply(this, arguments);
52
+
53
+ // async 함수인 경우 Promise 처리
54
+ if (isAsync || (result && typeof result.then === 'function')) {
55
+ return Promise.resolve(result)
56
+ .then((res) => {
57
+ self.__endTransaction(null, ctx);
58
+ return res;
59
+ })
60
+ .catch((error) => {
61
+ self.__handleError(error, ctx);
62
+ self.__endTransaction(null, ctx);
63
+ throw error;
64
+ });
65
+ } else {
66
+ // 동기 함수인 경우 바로 종료
67
+ self.__endTransaction(null, ctx);
68
+ return result;
69
+ }
70
+ } catch (error) {
71
+ // 동기 에러 처리
72
+ self.__handleError(error, ctx);
73
+ self.__endTransaction(null, ctx);
74
+ throw error;
75
+ }
76
+ });
77
+ };
78
+ };
79
+
80
+ CronObserver.prototype.__handleError = function (error, ctx) {
81
+ if (!ctx) return;
82
+
83
+ try {
84
+ ctx.error = 1;
85
+ ctx.status = 500;
86
+
87
+ var errorClass = error.code || error.name || error.constructor?.name || 'CronJobError';
88
+ var errorMessage = error.message || 'Cron job execution failed';
89
+ var errorStack = '';
90
+
91
+ if (conf.trace_sql_error_stack && conf.trace_sql_error_depth && error.stack) {
92
+ var traceDepth = conf.trace_sql_error_depth;
93
+ var stackLines = error.stack.split("\n");
94
+ if (stackLines.length > traceDepth) {
95
+ stackLines = stackLines.slice(0, traceDepth + 1);
96
+ }
97
+ errorStack = stackLines.join("\n");
98
+ }
99
+
100
+ var errors = [errorClass, errorMessage];
101
+ if (errorStack || error.stack) {
102
+ errors.push(errorStack || error.stack);
103
+ }
104
+
105
+ AsyncSender.send_packet(PacketTypeEnum.TX_ERROR, ctx, errors);
106
+ } catch (e) {
107
+ Logger.printError('WHATAP-301', 'Error handling cron job error', e, false);
108
+ }
109
+ };
110
+
111
+ CronObserver.prototype.__endTransaction = function (error, ctx) {
112
+ try {
113
+ if (error) {
114
+ TraceContextManager.end(ctx != null ? ctx.id : null);
115
+ ctx = null;
116
+ return;
117
+ }
118
+
119
+ if (ctx == null || TraceContextManager.isExist(ctx.id) === false) {
120
+ return;
121
+ }
122
+
123
+ let endDatas = [
124
+ '0.0.0.0',
125
+ ctx.service_name,
126
+ 0, // mtid
127
+ 0, // mdepth
128
+ 0, // mcaller_txid
129
+ 0, // mcaller_pcode
130
+ '', // mcaller_spec
131
+ String(0), // mcaller_url_hash
132
+ ctx.status || 200
133
+ ];
134
+
135
+ ctx.start_time = Date.now();
136
+ // ctx.elapsed = Date.now() - ctx.start_time;
137
+ AsyncSender.send_packet(PacketTypeEnum.TX_END, ctx, endDatas);
138
+
139
+ TraceContextManager.end(ctx.id);
140
+ ctx = null;
141
+ } catch (e) {
142
+ Logger.printError('WHATAP-302', 'End cron transaction error', e, false);
143
+ TraceContextManager.end(ctx != null ? ctx.id : null);
144
+ ctx = null;
145
+ }
146
+ };
147
+
148
+ CronObserver.prototype.__initCtx = function (cronExpression, jobName) {
149
+ if (conf.getProperty('profile_enabled', true) === false) {
150
+ return null;
151
+ }
152
+
153
+ // 크론잡 모니터링 비활성화 옵션 체크
154
+ if (conf.getProperty('trace_cron_enabled', true) === false) {
155
+ return null;
156
+ }
157
+
158
+ var ctx = TraceContextManager.start();
159
+ if (ctx == null) {
160
+ return null;
161
+ }
162
+
163
+ ctx.service_name = jobName || '/';
164
+ ctx.remoteIp = 0;
165
+ ctx.userid = 0;
166
+ ctx.userAgentString = '';
167
+ ctx.referer = '';
168
+ ctx.status = 200;
169
+ ctx.error = 0;
170
+
171
+ return ctx;
172
+ };
173
+
174
+ /**
175
+ * node-cron 라이브러리 후킹
176
+ */
177
+ CronObserver.prototype.inject = function (mod, moduleName) {
178
+ var self = this;
179
+
180
+ if (mod.__whatap_observe__) {
181
+ return;
182
+ }
183
+ mod.__whatap_observe__ = true;
184
+ Logger.initPrint("CronObserver");
185
+
186
+ if (conf.getProperty('profile_enabled', true) === false) {
187
+ return;
188
+ }
189
+
190
+ // node-cron의 schedule 메서드 후킹
191
+ if (typeof mod.schedule === 'function') {
192
+ shimmer.wrap(mod, 'schedule', function (original) {
193
+ return function wrappedSchedule(cronExpression, callback, options) {
194
+ // 크론잡 이름 추출 (옵션에서 가져오거나 자동 생성)
195
+ var jobName = '/';
196
+
197
+ if (options && options.name) {
198
+ jobName = `/${options.name}`;
199
+ } else if (typeof cronExpression === 'string') {
200
+ jobName = `/${cronExpression.replace(/\s+/g, '-')}`;
201
+ }
202
+
203
+ // callback을 래핑
204
+ var wrappedCallback = self.__createCronJobObserver(callback, cronExpression, jobName);
205
+
206
+ // 원본 schedule 호출 (래핑된 callback으로)
207
+ return original.call(this, cronExpression, wrappedCallback, options);
208
+ };
209
+ });
210
+
211
+ }
212
+
213
+ // node-schedule 라이브러리 지원
214
+ if (mod.scheduleJob && typeof mod.scheduleJob === 'function') {
215
+ shimmer.wrap(mod, 'scheduleJob', function (original) {
216
+ return function wrappedScheduleJob(nameOrSpec, specOrCallback, callback) {
217
+ var jobName = '/';
218
+ var spec, actualCallback;
219
+
220
+ // 오버로딩 처리
221
+ if (typeof nameOrSpec === 'string') {
222
+ jobName = `/${nameOrSpec}`;
223
+ spec = specOrCallback;
224
+ actualCallback = callback;
225
+ } else {
226
+ spec = nameOrSpec;
227
+ actualCallback = specOrCallback;
228
+ }
229
+
230
+ // callback 래핑
231
+ var wrappedCallback = self.__createCronJobObserver(
232
+ actualCallback,
233
+ JSON.stringify(spec),
234
+ jobName
235
+ );
236
+
237
+ // 원본 scheduleJob 호출
238
+ if (typeof nameOrSpec === 'string') {
239
+ return original.call(this, nameOrSpec, spec, wrappedCallback);
240
+ } else {
241
+ return original.call(this, spec, wrappedCallback);
242
+ }
243
+ };
244
+ });
245
+ }
246
+
247
+ // cron 라이브러리 지원 (CronJob 클래스)
248
+ if (mod.CronJob && typeof mod.CronJob === 'function') {
249
+ // mod 객체의 'CronJob' 프로퍼티를 래핑
250
+ shimmer.wrap(mod, 'CronJob', function(OriginalCronJob) {
251
+ return function WrappedCronJob(cronTime, onTick, onComplete, start, timezone, context, runOnInit, utcOffset, unrefTimeout) {
252
+ // onTick 콜백 래핑
253
+ var wrappedOnTick = null;
254
+ if (typeof onTick === 'function') {
255
+ wrappedOnTick = self.__createCronJobObserver(
256
+ onTick,
257
+ typeof cronTime === 'string' ? cronTime : 'custom',
258
+ '/'
259
+ );
260
+ }
261
+
262
+ // 원본 생성자 호출
263
+ return new OriginalCronJob(
264
+ cronTime,
265
+ wrappedOnTick || onTick,
266
+ onComplete,
267
+ start,
268
+ timezone,
269
+ context,
270
+ runOnInit,
271
+ utcOffset,
272
+ unrefTimeout
273
+ );
274
+ };
275
+ });
276
+ }
277
+ };
278
+
279
+ exports.CronObserver = CronObserver;
@@ -128,6 +128,16 @@ CustomMethodObserver.prototype.inject = function(mod, moduleName) {
128
128
  Logger.print('WHATAP-224',
129
129
  'Hooked class method: ' + className + '.' + methodName,
130
130
  false);
131
+ } else if (typeof moduleExports === 'function' && moduleExports.name === className) {
132
+ // CommonJS default export: module.exports = ExternalService
133
+ if (moduleExports.prototype && typeof moduleExports.prototype[methodName] === 'function') {
134
+ PackageCtrHelper.hookMethod(moduleExports.prototype, tryPath, methodName, className);
135
+ } else {
136
+ PackageCtrHelper.hookMethod(moduleExports, tryPath, methodName, className);
137
+ }
138
+ Logger.print('WHATAP-224-CJS',
139
+ 'Hooked CommonJS default class method: ' + className + '.' + methodName,
140
+ false);
131
141
  } else if (moduleExports.default) {
132
142
  // ESM default export
133
143
  if (typeof moduleExports.default === 'function' && moduleExports.default.name === className) {
@@ -128,6 +128,11 @@ HttpObserver.prototype.__createTransactionObserver = function (callback, isHttps
128
128
  }
129
129
 
130
130
  ctx.service_name = req.url ? req.url : "";
131
+ ctx.isStaticContents = isStatic(ctx.service_name);
132
+ if(ctx.isStaticContents){
133
+ return callback(req, res);
134
+ }
135
+
131
136
  let hostname = ctx.host && ctx.host.includes(':') ? ctx.host.split(':')[0] : '';
132
137
  let datas = [
133
138
  hostname,
@@ -247,17 +252,6 @@ function initCtx(req, res) {
247
252
  // Host
248
253
  ctx.host = req.headers.host;
249
254
 
250
- /************************************/
251
- /* Header / param Trace */
252
- /************************************/
253
- var header_enabled = false;
254
- if(conf.profile_http_header_enabled === true && req.headers) {
255
- header_enabled = true;
256
- var prefix = conf.profile_header_url_prefix;
257
- if(prefix && ctx.service_name.indexOf(prefix) < 0) {
258
- header_enabled = false;
259
- }
260
- }
261
255
  /************************************/
262
256
  /* Header / param Trace */
263
257
  /************************************/
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "whatap",
3
3
  "homepage": "http://www.whatap.io",
4
- "version": "1.0.9",
5
- "releaseDate": "20251111",
4
+ "version": "1.0.10",
5
+ "releaseDate": "20251212",
6
6
  "description": "Monitoring and Profiling Service",
7
7
  "main": "index.js",
8
8
  "scripts": {},