whatap 1.0.3 → 1.0.4

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/core/agent.js CHANGED
@@ -32,7 +32,8 @@ var Interceptor = require('./interceptor').Interceptor,
32
32
  // GRpcObserver = require('../observers/grpc-observer').GRpcObserver,
33
33
  ApolloObserver = require('../observers/apollo-server-observer').ApolloServerObserver,
34
34
  PrismaObserver = require('../observers/prisma-observer').PrismaObserver,
35
- OracleObserver = require('../observers/oracle-observer').OracleObserver;
35
+ OracleObserver = require('../observers/oracle-observer').OracleObserver,
36
+ CustomMethodObserver = require('../observers/custom-method-observer').CustomMethodObserver;
36
37
 
37
38
 
38
39
  var Configuration = require('./../conf/configure'),
@@ -112,13 +113,13 @@ NodeAgent.prototype.isNodejsAgentProcess = function(pid) {
112
113
  let processCommand = '';
113
114
 
114
115
  if (platform === 'linux') {
115
- // On Linux, use /proc filesystem - same approach as Python
116
+ // On Linux, use /proc filesystem
116
117
  try {
117
118
  const cmdlineFile = path.join('/proc', pid.toString(), 'cmdline');
118
119
  if (fs.existsSync(cmdlineFile)) {
119
120
  processCommand = fs.readFileSync(cmdlineFile, 'utf8');
120
121
  // Log for debugging
121
- Logger.print("WHATAP-PROCESS", `Process ${pid} command line: ${processCommand}`, false);
122
+ Logger.print("WHATAP-001", `Process ${pid} command line: ${processCommand}`, false);
122
123
  }
123
124
  } catch (e) {
124
125
  Logger.printError("WHATAP-101", "Error reading Linux process info", e, false);
@@ -128,7 +129,7 @@ NodeAgent.prototype.isNodejsAgentProcess = function(pid) {
128
129
  // On macOS, use ps command
129
130
  try {
130
131
  processCommand = child_process.execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim();
131
- Logger.print("WHATAP-PROCESS", `Process ${pid} command line: ${processCommand}`, false);
132
+ Logger.print("WHATAP-001", `Process ${pid} command line: ${processCommand}`, false);
132
133
  } catch (e) {
133
134
  Logger.printError("WHATAP-102", "Error reading MacOS process info", e, false);
134
135
  return false;
@@ -141,7 +142,7 @@ NodeAgent.prototype.isNodejsAgentProcess = function(pid) {
141
142
  const match = output.match(/"([^"]+)"/);
142
143
  if (match && match[1]) {
143
144
  processCommand = match[1];
144
- Logger.print("WHATAP-PROCESS", `Process ${pid} command line: ${processCommand}`, false);
145
+ Logger.print("WHATAP-001", `Process ${pid} command line: ${processCommand}`, false);
145
146
  }
146
147
  } catch (e) {
147
148
  Logger.printError("WHATAP-103", "Error reading Windows process info", e, false);
@@ -151,19 +152,19 @@ NodeAgent.prototype.isNodejsAgentProcess = function(pid) {
151
152
  // On other platforms, try generic ps command
152
153
  try {
153
154
  processCommand = child_process.execSync(`ps -p ${pid} -o command=`, { encoding: 'utf8' }).trim();
154
- Logger.print("WHATAP-PROCESS", `Process ${pid} command line: ${processCommand}`, false);
155
+ Logger.print("WHATAP-001", `Process ${pid} command line: ${processCommand}`, false);
155
156
  } catch (e) {
156
157
  Logger.printError("WHATAP-104", "Error reading other os process info", e, false);
157
158
  return false;
158
159
  }
159
160
  }
160
161
 
161
- // Check if the command contains the agent name (equivalent to Python's find('whatap_python') >= 0)
162
+ // Check if the command contains the agent name
162
163
  const isAgentProcess = processCommand.indexOf(AGENT_NAME) >= 0;
163
- Logger.print("WHATAP-PROCESS", `Process ${pid} is ${isAgentProcess ? '' : 'not '}a WhaTap agent`, false);
164
+ Logger.print("WHATAP-002", `Process ${pid} is ${isAgentProcess ? '' : 'not '}a WhaTap agent`, false);
164
165
  return isAgentProcess;
165
166
  } catch (e) {
166
- Logger.printError("WHATAP-AGENT", "Error in isNodejsAgentProcess", e, false);
167
+ Logger.printError("WHATAP-003", "Error in isNodejsAgentProcess", e, false);
167
168
  return false;
168
169
  }
169
170
  };
@@ -176,7 +177,7 @@ NodeAgent.prototype.readPidFile = function(home, fileName) {
176
177
  return fs.readFileSync(filePath, 'utf8').trim();
177
178
  }
178
179
  } catch (e) {
179
- Logger.printError("WHATAP-PID", "Error reading PID file", e, false);
180
+ Logger.printError("WHATAP-004", "Error reading PID file", e, false);
180
181
  }
181
182
  return '';
182
183
  };
@@ -188,8 +189,8 @@ NodeAgent.prototype.writeToFile = function(home, fileName, value) {
188
189
  fs.writeFileSync(filePath, value);
189
190
  return true;
190
191
  } catch (e) {
191
- Logger.printError("WHATAP-FILE", `Error writing to file ${fileName}`, e, false);
192
- Logger.print("WHATAP-FILE", `Try to execute command: \`sudo chmod -R 777 $WHATAP_HOME\``, false);
192
+ Logger.printError("WHATAP-005", `Error writing to file ${fileName}`, e, false);
193
+ Logger.print("WHATAP-006", `Try to execute command: \`sudo chmod -R 777 $WHATAP_HOME\``, false);
193
194
  return false;
194
195
  }
195
196
  };
@@ -206,7 +207,7 @@ NodeAgent.prototype.isGoAgentRunning = function(pid) {
206
207
  parseInt(pid) - 1
207
208
  ];
208
209
 
209
- Logger.print("WHATAP-AGENT", `Checking PIDs: ${pidsToCheck.join(', ')} (original: ${pid})`, false);
210
+ Logger.print("WHATAP-007", `Checking PIDs: ${pidsToCheck.join(', ')} (original: ${pid})`, false);
210
211
 
211
212
  for (const pidToCheck of pidsToCheck) {
212
213
  try {
@@ -215,7 +216,7 @@ NodeAgent.prototype.isGoAgentRunning = function(pid) {
215
216
 
216
217
  // whatap_nodejs 프로세스인지 확인
217
218
  if (this.isNodejsAgentProcess(pidToCheck)) {
218
- Logger.print("WHATAP-AGENT",
219
+ Logger.print("WHATAP-008",
219
220
  `Found agent process at PID ${pidToCheck} (original PID in file: ${pid})`,
220
221
  false);
221
222
  return true;
@@ -228,16 +229,16 @@ NodeAgent.prototype.isGoAgentRunning = function(pid) {
228
229
  }
229
230
  }
230
231
 
231
- Logger.print("WHATAP-AGENT", `No agent process found for PID ${pid} or adjacent PIDs`, false);
232
+ Logger.print("WHATAP-009", `No agent process found for PID ${pid} or adjacent PIDs`, false);
232
233
  return false;
233
234
  } catch (e) {
234
- Logger.printError("WHATAP-AGENT", "Error checking if agent is running", e, false);
235
+ Logger.printError("WHATAP-010", "Error checking if agent is running", e, false);
235
236
  return false;
236
237
  }
237
238
  };
238
239
 
239
240
  /**
240
- * Read content from a file - similar to Python's read_file
241
+ * Read content from a file
241
242
  * @param {string} home - Environment variable name for home directory
242
243
  * @param {string} fileName - Name of the file to read
243
244
  * @returns {string} - Content of the file, or empty string on error
@@ -249,13 +250,13 @@ NodeAgent.prototype.readFile = function(home, fileName) {
249
250
  return fs.readFileSync(filePath, 'utf8').toString().trim();
250
251
  }
251
252
  } catch (e) {
252
- Logger.printError("WHATAP-FILE", `Error reading file ${fileName}`, e, false);
253
+ Logger.printError("WHATAP-011", `Error reading file ${fileName}`, e, false);
253
254
  }
254
255
  return '';
255
256
  };
256
257
 
257
258
  /**
258
- * Write content to a file - similar to Python's write_file
259
+ * Write content to a file
259
260
  * @param {string} home - Environment variable name for home directory
260
261
  * @param {string} fileName - Name of the file to write
261
262
  * @param {string} value - Content to write to the file
@@ -267,14 +268,14 @@ NodeAgent.prototype.writeFile = function(home, fileName, value) {
267
268
  fs.writeFileSync(filePath, value);
268
269
  return true;
269
270
  } catch (e) {
270
- Logger.printError("WHATAP-FILE", `Error writing to file ${fileName}`, e, false);
271
- Logger.print("WHATAP-FILE", `Try to execute command: \`sudo chmod -R 777 $WHATAP_HOME\``, false);
271
+ Logger.printError("WHATAP-012", `Error writing to file ${fileName}`, e, false);
272
+ Logger.print("WHATAP-013", `Try to execute command: \`sudo chmod -R 777 $WHATAP_HOME\``, false);
272
273
  return false;
273
274
  }
274
275
  };
275
276
 
276
277
  /**
277
- * Find whatap.conf file by searching directories - similar to Python's find_whatap_conf
278
+ * Find whatap.conf file by searching directories
278
279
  * @returns {string|null} - Path to whatap.conf or null if not found
279
280
  */
280
281
  NodeAgent.prototype.findWhatapConf = function() {
@@ -303,7 +304,7 @@ NodeAgent.prototype.findWhatapConf = function() {
303
304
  };
304
305
 
305
306
  /**
306
- * Open and lock the port file - similar to Python's openPortFile
307
+ * Open and lock the port file
307
308
  * @param {string} filepath - Path to the lock file
308
309
  * @returns {object|null} - File descriptor or null if failed
309
310
  */
@@ -313,7 +314,7 @@ NodeAgent.prototype.openPortFile = function(filepath) {
313
314
  let fileHandle = null;
314
315
  let attempts = 0;
315
316
 
316
- // Try to open the file, similar to Python's while loop
317
+ // Try to open the file
317
318
  while (!fileHandle && attempts < 100) {
318
319
  try {
319
320
  // Try to open file for reading and writing
@@ -329,10 +330,10 @@ NodeAgent.prototype.openPortFile = function(filepath) {
329
330
  fileHandle = fs.openSync(filepath, 'w+');
330
331
  }
331
332
  } catch (e) {
332
- Logger.printError("WHATAP-PORT", `Error opening port file (attempt ${attempts})`, e, false);
333
+ Logger.printError("WHATAP-014", `Error opening port file (attempt ${attempts})`, e, false);
333
334
  }
334
335
 
335
- // Similar to Python, add a delay after 50 attempts
336
+ // Add a delay after 50 attempts
336
337
  if (attempts > 50) {
337
338
  setTimeout(() => {}, 100); // 0.1 second delay
338
339
  }
@@ -346,7 +347,7 @@ NodeAgent.prototype.openPortFile = function(filepath) {
346
347
  properLock.lockSync(filepath);
347
348
  return { fileHandle, filepath };
348
349
  } catch (e) {
349
- Logger.printError("WHATAP-PORT", "Failed to lock port file", e, false);
350
+ Logger.printError("WHATAP-015", "Failed to lock port file", e, false);
350
351
  try {
351
352
  fs.closeSync(fileHandle);
352
353
  } catch (closeErr) {}
@@ -357,7 +358,7 @@ NodeAgent.prototype.openPortFile = function(filepath) {
357
358
  };
358
359
 
359
360
  /**
360
- * Get available port number for the agent - similar to Python's get_port_number
361
+ * Get available port number for the agent
361
362
  * @param {number} defaultPort - Default port to start with
362
363
  * @param {string} home - WHATAP_HOME directory
363
364
  * @returns {number|null} - Allocated port number or null if failed
@@ -371,8 +372,33 @@ NodeAgent.prototype.getPortNumber = function(port, home) {
371
372
  return null;
372
373
  }
373
374
 
374
- // Open the port file
375
- const fileInfo = this.openPortFile();
375
+ // PM2 cluster 모드에서는 pm_id를 제외한 identifier 사용 (동일 포트 공유)
376
+ // 일반 모드에서는 pm_id 포함 (각자 다른 포트)
377
+ const appIdentifier = this.getApplicationIdentifier();
378
+ const portKey = `${home}:${appIdentifier}`;
379
+ const isPM2Cluster = this.isPM2ClusterMode();
380
+
381
+ const lockFilePath = process.env.WHATAP_LOCK_FILE || '/tmp/whatap-nodejs.lock';
382
+
383
+ // PM2 cluster 모드에서 whatap_nodejs가 이미 실행 중이면 lock 없이 읽기만
384
+ if (isPM2Cluster) {
385
+ const sharedPidFile = path.join(home, `agent-${appIdentifier}.pid`);
386
+ if (fs.existsSync(sharedPidFile)) {
387
+ try {
388
+ const existingPid = fs.readFileSync(sharedPidFile, 'utf8').trim();
389
+ if (existingPid && this.isGoAgentRunning(existingPid)) {
390
+ // whatap_nodejs가 실행 중이면 lock 없이 포트 파일 읽기
391
+ return this.readPortFromFile(lockFilePath, portKey, appIdentifier, isPM2Cluster);
392
+ }
393
+ } catch (e) {
394
+ Logger.printError("WHATAP-016", "Error checking agent process", e, false);
395
+ }
396
+ }
397
+ }
398
+
399
+ // 새로운 포트 할당이 필요한 경우 (첫 실행 또는 일반 모드)
400
+ // Open the port file with lock
401
+ const fileInfo = this.openPortFile(lockFilePath);
376
402
  if (!fileInfo) {
377
403
  return port; // Return default port if we couldn't open the file
378
404
  }
@@ -392,6 +418,7 @@ NodeAgent.prototype.getPortNumber = function(port, home) {
392
418
  }
393
419
 
394
420
  let lastPortFound = null;
421
+ const existingPorts = new Map();
395
422
 
396
423
  // Process each line
397
424
  if (content && content.length > 0) {
@@ -401,24 +428,30 @@ NodeAgent.prototype.getPortNumber = function(port, home) {
401
428
  if (!trimmedLine) continue;
402
429
 
403
430
  try {
404
- // Parse the line "port home"
405
- const parts = trimmedLine.split(/\s+/, 2);
431
+ // Parse the line "port portKey"
432
+ const parts = trimmedLine.split(/\t/, 2);
406
433
  if (parts.length < 2) continue;
407
434
 
408
435
  const portStr = parts[0];
409
- const portHome = parts[1];
436
+ const storedKey = parts[1];
410
437
  const currentPort = parseInt(portStr, 10);
411
438
 
412
439
  if (isNaN(currentPort)) continue;
413
440
 
414
- // If home matches, return the port
415
- if (home === portHome) {
441
+ // If portKey matches, return the existing port
442
+ if (portKey === storedKey) {
416
443
  // Clean up before returning
417
444
  properLock.unlockSync(filepath);
418
445
  fs.closeSync(fileHandle);
446
+ Logger.print("WHATAP-017",
447
+ `Reusing existing port ${currentPort} for ${appIdentifier}${isPM2Cluster ? ' (PM2 cluster - shared port)' : ''}`,
448
+ false);
419
449
  return currentPort;
420
450
  }
421
451
 
452
+ // Store existing ports
453
+ existingPorts.set(storedKey, currentPort);
454
+
422
455
  // Track the highest port number
423
456
  if (!lastPortFound || lastPortFound < currentPort) {
424
457
  lastPortFound = currentPort;
@@ -432,19 +465,27 @@ NodeAgent.prototype.getPortNumber = function(port, home) {
432
465
  // Determine the new port number
433
466
  const newPort = lastPortFound ? lastPortFound + 1 : port;
434
467
 
435
- // Clear the file completely
436
- fs.ftruncateSync(fileHandle, 0);
468
+ // Rebuild the file with existing entries + new entry
469
+ let newContent = '';
470
+ for (const [key, portNum] of existingPorts.entries()) {
471
+ newContent += `${portNum}\t${key}\n`;
472
+ }
473
+ newContent += `${newPort}\t${portKey}\n`;
437
474
 
438
- // Write the new entry directly
439
- fs.writeSync(fileHandle, `${newPort}\t${home}\n`);
475
+ // Clear and rewrite the file
476
+ fs.ftruncateSync(fileHandle, 0);
477
+ fs.writeSync(fileHandle, newContent);
440
478
 
441
479
  // Unlock and close the file
442
480
  properLock.unlockSync(filepath);
443
481
  fs.closeSync(fileHandle);
444
482
 
483
+ Logger.print("WHATAP-018",
484
+ `Allocated new port ${newPort} for ${appIdentifier}${isPM2Cluster ? ' (PM2 cluster - shared port)' : ''}`,
485
+ false);
445
486
  return newPort;
446
487
  } catch (e) {
447
- Logger.printError("WHATAP-PORT", "Error processing port file", e, false);
488
+ Logger.printError("WHATAP-019", "Error processing port file", e, false);
448
489
 
449
490
  try {
450
491
  properLock.unlockSync(filepath);
@@ -456,6 +497,56 @@ NodeAgent.prototype.getPortNumber = function(port, home) {
456
497
  }
457
498
  };
458
499
 
500
+ /**
501
+ * Read port from file without acquiring lock (for PM2 cluster mode)
502
+ * @param {string} filepath - Port file path
503
+ * @param {string} portKey - Port key to search
504
+ * @param {string} appIdentifier - Application identifier
505
+ * @param {boolean} isPM2Cluster - Whether in PM2 cluster mode
506
+ * @returns {number|null} - Port number or null if not found
507
+ */
508
+ NodeAgent.prototype.readPortFromFile = function(filepath, portKey, appIdentifier, isPM2Cluster) {
509
+ try {
510
+ if (!fs.existsSync(filepath)) {
511
+ Logger.printError("WHATAP-020", "Port file does not exist", null, false);
512
+ return null;
513
+ }
514
+
515
+ const content = fs.readFileSync(filepath, 'utf8').trim();
516
+
517
+ if (!content) {
518
+ return null;
519
+ }
520
+
521
+ const lines = content.split('\n');
522
+ for (const line of lines) {
523
+ const trimmedLine = line.trim();
524
+ if (!trimmedLine) continue;
525
+
526
+ const parts = trimmedLine.split(/\t/, 2);
527
+ if (parts.length < 2) continue;
528
+
529
+ const portStr = parts[0];
530
+ const storedKey = parts[1];
531
+ const currentPort = parseInt(portStr, 10);
532
+
533
+ if (isNaN(currentPort)) continue;
534
+
535
+ if (portKey === storedKey) {
536
+ Logger.print("WHATAP-021",
537
+ `Reusing existing port ${currentPort} for ${appIdentifier}${isPM2Cluster ? ' (PM2 cluster - shared port, lock-free read)' : ''}`,
538
+ false);
539
+ return currentPort;
540
+ }
541
+ }
542
+
543
+ return null;
544
+ } catch (e) {
545
+ Logger.printError("WHATAP-022", "Error reading port file without lock", e, false);
546
+ return null;
547
+ }
548
+ };
549
+
459
550
  /**
460
551
  * Configure the port for the agent and update the configuration
461
552
  * @returns {number|null} - The configured port or null if failed
@@ -470,7 +561,7 @@ NodeAgent.prototype.configurePort = function() {
470
561
  };
471
562
 
472
563
  /**
473
- * Update configuration file with new option value - similar to Python's update_config
564
+ * Update configuration file with new option value
474
565
  * @param {string} home - Environment variable name for WhaTap home
475
566
  * @param {string} optKey - Option key to update
476
567
  * @param {string} optValue - Option value to set
@@ -478,11 +569,13 @@ NodeAgent.prototype.configurePort = function() {
478
569
  NodeAgent.prototype.updateConfig = function(home, optKey, optValue) {
479
570
  const homePath = process.env[home];
480
571
  if (!homePath) {
481
- Logger.print("WHATAP-CONFIG", `${home} environment variable not set`, true);
572
+ Logger.print("WHATAP-023", `${home} environment variable not set`, false);
482
573
  return;
483
574
  }
484
575
 
485
- const configFile = path.join(homePath, 'whatap.conf');
576
+ // Use WHATAP_CONF environment variable or default to 'whatap.conf'
577
+ const configFileName = process.env.WHATAP_CONF || 'whatap.conf';
578
+ const configFile = path.join(homePath, configFileName);
486
579
 
487
580
  try {
488
581
  let content = '';
@@ -516,12 +609,22 @@ NodeAgent.prototype.updateConfig = function(home, optKey, optValue) {
516
609
 
517
610
  // Write updated content back to file
518
611
  fs.writeFileSync(configFile, content);
519
- Logger.print("WHATAP-CONFIG", `Updated configuration: ${optKey}=${optValue}`, false);
612
+ Logger.print("WHATAP-024", `Updated configuration: ${optKey}=${optValue}`, false);
520
613
  } catch (e) {
521
- Logger.printError("WHATAP-CONFIG", `Error updating configuration: ${optKey}=${optValue}`, e, false);
614
+ Logger.printError("WHATAP-025", `Error updating configuration: ${optKey}=${optValue}`, e, false);
522
615
  }
523
616
  };
524
617
 
618
+ /**
619
+ * Check if running in PM2 cluster mode
620
+ * @returns {boolean} - True if PM2 cluster mode
621
+ */
622
+ NodeAgent.prototype.isPM2ClusterMode = function() {
623
+ return process.env.pm_id !== undefined &&
624
+ process.env.instances !== undefined &&
625
+ parseInt(process.env.instances) > 1;
626
+ };
627
+
525
628
  NodeAgent.prototype.getApplicationIdentifier = function() {
526
629
  // 1. 명시적 그룹 지정 (환경 변수)
527
630
  if (process.env.WHATAP_APP_GROUP) {
@@ -536,24 +639,35 @@ NodeAgent.prototype.getApplicationIdentifier = function() {
536
639
  // 3. 자동 생성
537
640
  const appRoot = this._conf['app.root'] || process.cwd();
538
641
  const appName = this.getApplicationName();
642
+ const isPM2Cluster = this.isPM2ClusterMode();
643
+
644
+ // PM2 cluster 모드에서는 pm_id를 제외하여 모든 인스턴스가 동일한 identifier를 공유
645
+ // 일반 PM2 모드나 단일 인스턴스에서는 pm_id 포함
646
+ let identifierBase = `${appRoot}:${appName}`;
647
+ if (process.env.pm_id !== undefined && !isPM2Cluster) {
648
+ identifierBase += `:${process.env.pm_id}`;
649
+ }
539
650
 
540
651
  const identifier = crypto
541
652
  .createHash('md5')
542
- .update(`${appRoot}:${appName}`)
653
+ .update(identifierBase)
543
654
  .digest('hex')
544
655
  .substr(0, 8);
545
656
 
546
- Logger.print("WHATAP-AGENT",
547
- `Application identifier: ${appName} at ${appRoot} => ${identifier}`,
657
+ Logger.print("WHATAP-026",
658
+ `Application identifier: ${appName} at ${appRoot}${isPM2Cluster ? ' (PM2 cluster mode)' : (process.env.pm_id !== undefined ? ' (PM2 instance: ' + process.env.pm_id + ')' : '')} => ${identifier}`,
548
659
  false);
549
660
 
550
661
  return identifier;
551
662
  };
552
663
 
553
664
  NodeAgent.prototype.getApplicationName = function() {
554
- // 1. PM2 실행 시
555
- if (process.env.PM2_APP_NAME || process.env.name) {
556
- return process.env.PM2_APP_NAME || process.env.name;
665
+ // 1. PM2 실행 시 (PM2의 공식 환경 변수 사용)
666
+ if (process.env.pm_id !== undefined) {
667
+ // PM2 환경에서는 name 환경 변수 사용
668
+ if (process.env.name) {
669
+ return process.env.name;
670
+ }
557
671
  }
558
672
 
559
673
  // 2. 설정 파일에서
@@ -588,8 +702,9 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
588
702
  return;
589
703
  }
590
704
 
591
- // 1. 애플리케이션 식별자 생성 (포트 제외)
705
+ // 1. 애플리케이션 식별자 생성
592
706
  const appIdentifier = this.getApplicationIdentifier();
707
+ const isPM2Cluster = this.isPM2ClusterMode();
593
708
  const sharedLockFile = path.join(whatapHome, `agent-${appIdentifier}.lock`);
594
709
  const sharedPidFile = path.join(whatapHome, `agent-${appIdentifier}.pid`);
595
710
 
@@ -599,33 +714,64 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
599
714
  // PID 파일에서 기존 PID 읽기
600
715
  const existingPid = fs.readFileSync(sharedPidFile, 'utf8').trim();
601
716
  if (existingPid && this.isGoAgentRunning(existingPid)) {
602
- Logger.print("WHATAP-AGENT", `Go agent already running for application ${appIdentifier} (PID: ${existingPid})`, false);
717
+ const logMsg = isPM2Cluster
718
+ ? `Go agent already running for application ${appIdentifier} (PID: ${existingPid}) - PM2 instance ${process.env.pm_id} will connect to shared agent`
719
+ : `Go agent already running for application ${appIdentifier} (PID: ${existingPid})`;
720
+ Logger.print("WHATAP-027", logMsg, false);
603
721
  return;
604
722
  } else {
605
723
  // 프로세스가 죽었다면 파일 제거
606
- Logger.print("WHATAP-AGENT", `Previous agent process ${existingPid} not found, cleaning up`, false);
724
+ Logger.print("WHATAP-028", `Previous agent process ${existingPid} not found, cleaning up`, false);
607
725
  try { fs.unlinkSync(sharedLockFile); } catch (e) {}
608
726
  try { fs.unlinkSync(sharedPidFile); } catch (e) {}
609
727
  }
610
728
  }
611
729
  } catch (e) {
612
- Logger.printError("WHATAP-AGENT", "Error checking shared lock file", e, false);
730
+ Logger.printError("WHATAP-029", "Error checking shared lock file", e, false);
613
731
  }
614
732
 
615
- // 잠금 획득 시도
616
- let lockAcquired = false;
617
- try {
618
- if (!fs.existsSync(sharedLockFile)) {
619
- fs.writeFileSync(sharedLockFile, process.pid.toString());
620
- lockAcquired = true;
733
+ // 3. PM2 cluster 모드에서는 첫 번째 인스턴스만 에이전트 시작
734
+ if (isPM2Cluster) {
735
+ // 잠금 파일로 동시 실행 방지
736
+ let lockAcquired = false;
737
+ try {
738
+ if (!fs.existsSync(sharedLockFile)) {
739
+ fs.writeFileSync(sharedLockFile, process.pid.toString());
740
+ lockAcquired = true;
741
+ Logger.print("WHATAP-030",
742
+ `PM2 cluster mode: Instance ${process.env.pm_id} will start shared agent`,
743
+ false);
744
+ } else {
745
+ // 다른 인스턴스가 이미 에이전트를 시작하는 중
746
+ Logger.print("WHATAP-031",
747
+ `PM2 cluster mode: Instance ${process.env.pm_id} detected another instance starting the agent, will use shared agent`,
748
+ false);
749
+ return;
750
+ }
751
+ } catch (e) {
752
+ Logger.printError("WHATAP-032", "Error acquiring lock in PM2 cluster mode", e, false);
753
+ return;
621
754
  }
622
- } catch (e) {
623
- Logger.printError("WHATAP-AGENT", "Error acquiring lock", e, false);
624
- }
625
755
 
626
- if (!lockAcquired) {
627
- Logger.print("WHATAP-AGENT", "Another process is starting the agent, waiting...", false);
628
- return;
756
+ if (!lockAcquired) {
757
+ return;
758
+ }
759
+ } else {
760
+ // 일반 모드에서는 기존 로직 사용
761
+ let lockAcquired = false;
762
+ try {
763
+ if (!fs.existsSync(sharedLockFile)) {
764
+ fs.writeFileSync(sharedLockFile, process.pid.toString());
765
+ lockAcquired = true;
766
+ }
767
+ } catch (e) {
768
+ Logger.printError("WHATAP-033", "Error acquiring lock", e, false);
769
+ }
770
+
771
+ if (!lockAcquired) {
772
+ Logger.print("WHATAP-034", "Another process is starting the agent, waiting...", false);
773
+ return;
774
+ }
629
775
  }
630
776
 
631
777
  // 기존 whatap_nodejs.pid 파일 처리 (호환성 유지)
@@ -647,7 +793,7 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
647
793
  }
648
794
 
649
795
  // 포트 설정은 initUdp()에서 처리됨
650
- Logger.print("WHATAP-AGENT", "Port configuration will be handled during UDP initialization", false);
796
+ Logger.print("WHATAP-035", "Port configuration will be handled during UDP initialization", false);
651
797
 
652
798
  try {
653
799
  // 바이너리 경로 설정
@@ -660,11 +806,11 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
660
806
  if (!fs.existsSync(agentPath)) {
661
807
  try {
662
808
  fs.symlinkSync(sourcePath, agentPath);
663
- Logger.print("WHATAP-AGENT", `Created symbolic link for ${AGENT_NAME}`, false);
809
+ Logger.print("WHATAP-036", `Created symbolic link for ${AGENT_NAME}`, false);
664
810
  } catch (e) {
665
811
  if (e.code !== 'EEXIST') {
666
812
  if (platform === 'win32') {
667
- Logger.print("WHATAP-AGENT", "Symlink failed, copying binary instead", false);
813
+ Logger.print("WHATAP-037", "Symlink failed, copying binary instead", false);
668
814
  fs.copyFileSync(sourcePath, agentPath);
669
815
  } else {
670
816
  throw e;
@@ -686,20 +832,34 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
686
832
  newEnv['node.version'] = process.version;
687
833
  newEnv['node.tzname'] = new Date().toLocaleString('en', {timeZoneName: 'short'}).split(' ').pop();
688
834
  newEnv['os.release'] = os.release();
689
- newEnv['whatap.enabled'] = 'True';
835
+ newEnv['whatap.enabled'] = 'true';
690
836
  newEnv['WHATAP_PID_FILE'] = sharedPidFile; // 공유 PID 파일 사용
691
- newEnv['NODEJS_PARENT_APP_PID'] = process.pid.toString();
692
837
  newEnv['APP_IDENTIFIER'] = appIdentifier;
693
838
  newEnv['APP_NAME'] = this.getApplicationName();
694
839
 
695
840
  // PM2 정보 추가
696
- const isPM2 = typeof process.env.PM2_HOME !== 'undefined' ||
697
- typeof process.env.pm_id !== 'undefined' ||
698
- typeof process.env.PM2_JSON_PROCESSING !== 'undefined';
841
+ const isPM2 = process.env.pm_id !== undefined;
699
842
 
700
843
  if (isPM2) {
701
844
  newEnv['PM2_APP_NAME'] = process.env.name || '';
702
- newEnv['PM2_ID'] = process.env.pm_id || '';
845
+ newEnv['PM2_INSTANCES'] = process.env.instances || '1';
846
+
847
+ // PM2 cluster 모드
848
+ if (isPM2Cluster) {
849
+ // cluster 모드에서는 첫 번째 인스턴스의 ID만 전달
850
+ newEnv['PM2_CLUSTER_MODE'] = 'true';
851
+ newEnv['PM2_STARTER_ID'] = process.env.pm_id || '0';
852
+ Logger.print("WHATAP-038",
853
+ `PM2 cluster mode: Starting shared agent from instance ${process.env.pm_id}`,
854
+ false);
855
+ } else {
856
+ // 단일 인스턴스 모드
857
+ newEnv['PM2_ID'] = process.env.pm_id || '';
858
+ newEnv['NODEJS_PARENT_APP_PID'] = process.pid.toString();
859
+ }
860
+ } else {
861
+ // PM2가 아닌 경우
862
+ newEnv['NODEJS_PARENT_APP_PID'] = process.pid.toString();
703
863
  }
704
864
 
705
865
  Object.assign(newEnv, opts);
@@ -729,7 +889,7 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
729
889
 
730
890
  // PID 추적을 위한 추가 로직
731
891
  agentProcess.on('spawn', () => {
732
- Logger.print("WHATAP-AGENT", `Agent process spawned with PID: ${agentProcess.pid}`, false);
892
+ Logger.print("WHATAP-039", `Agent process spawned with PID: ${agentProcess.pid}`, false);
733
893
 
734
894
  // detached 프로세스에서 실제 PID가 다를 수 있으므로 잠시 대기 후 확인
735
895
  setTimeout(() => {
@@ -759,7 +919,7 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
759
919
  const pidNum = parseInt(foundPid);
760
920
  // spawn된 PID와 비슷한 범위인지 확인
761
921
  if (Math.abs(pidNum - agentProcess.pid) <= 2) {
762
- Logger.print("WHATAP-AGENT",
922
+ Logger.print("WHATAP-040",
763
923
  `Found actual agent PID: ${foundPid} (spawn returned: ${agentProcess.pid})`,
764
924
  false);
765
925
 
@@ -772,7 +932,7 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
772
932
  }
773
933
 
774
934
  // 실제 PID를 찾지 못한 경우 spawn이 반환한 PID 사용
775
- Logger.print("WHATAP-AGENT",
935
+ Logger.print("WHATAP-041",
776
936
  `Using spawn PID: ${agentProcess.pid} (actual PID not found)`,
777
937
  false);
778
938
 
@@ -780,7 +940,7 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
780
940
  this.writeToFile(home, pidFileName, agentProcess.pid.toString());
781
941
 
782
942
  } catch (e) {
783
- Logger.printError("WHATAP-AGENT", "Error finding actual PID", e, false);
943
+ Logger.printError("WHATAP-042", "Error finding actual PID", e, false);
784
944
  // 에러 발생 시 spawn이 반환한 PID 사용
785
945
  fs.writeFileSync(sharedPidFile, agentProcess.pid.toString());
786
946
  this.writeToFile(home, pidFileName, agentProcess.pid.toString());
@@ -790,28 +950,29 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
790
950
 
791
951
  agentProcess.on('close', (code) => {
792
952
  if (code !== 0) {
793
- Logger.printError("WHATAP-AGENT", `Agent process exited with code ${code}`, null, true);
794
- Logger.print("WHATAP-AGENT", `STDOUT: ${stdout}`, false);
795
- Logger.print("WHATAP-AGENT", `STDERR: ${stderr}`, false);
953
+ Logger.printError("WHATAP-043", `Agent process exited with code ${code}`, null, false);
954
+ Logger.print("WHATAP-044", `STDOUT: ${stdout}`, false);
955
+ Logger.print("WHATAP-045", `STDERR: ${stderr}`, false);
796
956
  // 에이전트가 비정상 종료되면 파일 제거
797
957
  try { fs.unlinkSync(sharedLockFile); } catch (e) {}
798
958
  try { fs.unlinkSync(sharedPidFile); } catch (e) {}
799
959
  } else {
800
- Logger.print("WHATAP-AGENT", "Agent started successfully", false);
960
+ Logger.print("WHATAP-046", "Agent started successfully", false);
801
961
  }
802
962
  });
803
963
 
804
964
  agentProcess.unref();
805
965
 
806
- Logger.print("WHATAP-AGENT",
807
- `AGENT UP! (process name: ${AGENT_NAME}, app: ${appIdentifier})`,
808
- false);
966
+ const agentMsg = isPM2Cluster
967
+ ? `AGENT UP! (process name: ${AGENT_NAME}, app: ${appIdentifier}, PM2 cluster - shared by ${process.env.instances} instances)`
968
+ : `AGENT UP! (process name: ${AGENT_NAME}, app: ${appIdentifier})`;
969
+ Logger.print("WHATAP-047", agentMsg, false);
809
970
 
810
971
  // 잠금 해제
811
972
  try { fs.unlinkSync(sharedLockFile); } catch (e) {}
812
973
 
813
974
  } catch (e) {
814
- Logger.printError("WHATAP-AGENT", "Error starting agent", e, true);
975
+ Logger.printError("WHATAP-048", "Error starting agent", e, false);
815
976
  // 에러 발생 시 파일 정리
816
977
  try { fs.unlinkSync(sharedLockFile); } catch (e) {}
817
978
  try { fs.unlinkSync(sharedPidFile); } catch (e) {}
@@ -820,11 +981,23 @@ NodeAgent.prototype.startGoAgent = function(opts = {}) {
820
981
 
821
982
  // 프로세스 종료 시 정리 작업 개선
822
983
  process.on('exit', () => {
823
- if (!process.env.WHATAP_HOME || !NodeAgent.prototype.getApplicationIdentifier) {
984
+ if (!process.env.WHATAP_HOME || !NodeAgent.prototype.getApplicationIdentifier || !NodeAgent.prototype.isPM2ClusterMode) {
824
985
  return;
825
986
  }
826
987
 
827
988
  try {
989
+ const isPM2Cluster = NodeAgent.prototype.isPM2ClusterMode.call(NodeAgent);
990
+
991
+ // PM2 cluster 모드에서는 인스턴스가 종료되어도 공유 에이전트를 종료하지 않음
992
+ // 다른 인스턴스가 계속 사용 중일 수 있음
993
+ if (isPM2Cluster) {
994
+ Logger.print("WHATAP-049",
995
+ `PM2 cluster instance ${process.env.pm_id} exiting - shared agent will remain running`,
996
+ false);
997
+ return;
998
+ }
999
+
1000
+ // 일반 모드에서는 기존 로직 사용
828
1001
  const appIdentifier = NodeAgent.prototype.getApplicationIdentifier.call(NodeAgent);
829
1002
  const sharedPidFile = path.join(process.env.WHATAP_HOME, `agent-${appIdentifier}.pid`);
830
1003
 
@@ -843,12 +1016,11 @@ process.on('exit', () => {
843
1016
  // graceful shutdown 처리 추가
844
1017
  ['SIGTERM', 'SIGINT'].forEach((signal) => {
845
1018
  process.on(signal, () => {
846
- Logger.print("WHATAP-AGENT", `Received ${signal}, cleaning up...`, false);
1019
+ Logger.print("WHATAP-050", `Received ${signal}, cleaning up...`, false);
847
1020
  process.exit(0);
848
1021
  });
849
1022
  });
850
1023
 
851
- // Updated init method to incorporate configuration setup similar to Python agent
852
1024
  NodeAgent.prototype.init = function(cb) {
853
1025
  var self = this;
854
1026
  if (self._initialized) {
@@ -869,12 +1041,12 @@ NodeAgent.prototype.init = function(cb) {
869
1041
  Logger.print('WHATAP-110', 'Start initialize WhaTap Agent... Root[' + self._conf['app.root'] + ']', true);
870
1042
 
871
1043
  if (self._conf['app.root'] == null || self._conf['app.root'].length == 0) {
872
- return Logger.print("WHATAP-111", "Can not find application root directory", true);
1044
+ return Logger.print("WHATAP-111", "Can not find application root directory", false);
873
1045
  }
874
1046
 
875
1047
  // Initialize configuration with proper home directory
876
1048
  if (!self.initConfig('WHATAP_HOME')) {
877
- Logger.printError("WHATAP-CONFIG", "Failed to initialize configuration", null, true);
1049
+ Logger.printError("WHATAP-051", "Failed to initialize configuration", null, false);
878
1050
  return self;
879
1051
  }
880
1052
 
@@ -883,12 +1055,12 @@ NodeAgent.prototype.init = function(cb) {
883
1055
  self._conf.init(this._userOpt, function(e) {
884
1056
  if (e) {
885
1057
  Logger.printError("WHATAP-112", "Configuration initialize error. " + e.message,
886
- e, true);
1058
+ e, false);
887
1059
  return;
888
1060
  }
889
1061
  Logger.print('WHATAP-113', 'Finish initialize configuration... ' + Boolean(self._conf['reqlog_enabled']), false);
890
1062
  if (Boolean(self._conf['reqlog_enabled']) == true) {
891
- Logger.print("WHATAP-REQLOG", "ReqLog Init Start !!!!", false);
1063
+ Logger.print("WHATAP-052", "ReqLog Init Start !!!!", false);
892
1064
  RequestLog.initializer.process();
893
1065
  }
894
1066
 
@@ -963,11 +1135,12 @@ NodeAgent.prototype.loadObserves = function() {
963
1135
  try {
964
1136
  require(name);
965
1137
  } catch (err) {
966
- Logger.print("WHTAP-loadObserves", 'unable to load ' + name + ' module', false);
1138
+ Logger.print("WHATAP-053", 'unable to load ' + name + ' module', false);
967
1139
  }
968
1140
  }
969
1141
  new ProcessObserver(agent).inject(process, 'process');
970
1142
  new GlobalObserver(agent).inject(global, 'global');
1143
+ new CustomMethodObserver(agent).inject(null, 'custom-method');
971
1144
  };
972
1145
 
973
1146
  NodeAgent.prototype.setServicePort = function (port) {
@@ -976,13 +1149,13 @@ NodeAgent.prototype.setServicePort = function (port) {
976
1149
 
977
1150
  NodeAgent.prototype.initUdp = function() {
978
1151
  var self = this;
979
- Logger.print('WHATAP-UDP', 'Initializing UDP connection...', false);
1152
+ Logger.print('WHATAP-054', 'Initializing UDP connection...', false);
980
1153
 
981
1154
  // Configure the port before initializing UDP
982
1155
  const configuredPort = self.configurePort();
983
1156
  if (configuredPort) {
984
1157
  self._conf['net_udp_port'] = configuredPort;
985
- Logger.print('WHATAP-UDP', `Using configured port: ${configuredPort}`, false);
1158
+ Logger.print('WHATAP-055', `Using configured port: ${configuredPort}`, false);
986
1159
  }
987
1160
 
988
1161
  // Initialize the UDP session with updated config
@@ -991,27 +1164,11 @@ NodeAgent.prototype.initUdp = function() {
991
1164
  // Initialize the async sender
992
1165
  AsyncSender.startWhatapThread();
993
1166
 
994
- Logger.print('WHATAP-UDP', 'UDP connection initialized', false);
1167
+ Logger.print('WHATAP-056', 'UDP connection initialized', false);
995
1168
  };
996
1169
 
997
1170
  /**
998
- * Check if WHATAP_HOME is set - similar to Python's check_whatap_home
999
- * @param {string} target - Environment variable name to check
1000
- * @returns {string|null} - Value of the environment variable or null
1001
- */
1002
- NodeAgent.prototype.checkWhatapHome = function(target) {
1003
- let whatapHome = process.env[target];
1004
- if (!whatapHome) {
1005
- whatapHome = this.findWhatapConf();
1006
- }
1007
- if (!whatapHome) {
1008
- Logger.print("WHATAP-HOME", `${target} is empty`, true);
1009
- }
1010
- return whatapHome;
1011
- };
1012
-
1013
- /**
1014
- * Initialize configuration with default values - similar to Python's init_config
1171
+ * Initialize configuration with default values
1015
1172
  * @param {string} home - Environment variable name for home directory
1016
1173
  * @returns {boolean} - True if successful, false otherwise
1017
1174
  */
@@ -1026,9 +1183,7 @@ NodeAgent.prototype.initConfig = function(home) {
1026
1183
  whatapHome = process.cwd();
1027
1184
  process.env[home] = whatapHome;
1028
1185
 
1029
- Logger.print("WHATAP-HOME", "WHATAP_HOME is empty", true);
1030
- Logger.print("WHATAP-HOME", "WHATAP_HOME set default CURRENT_WORKING_DIRECTORY value", true);
1031
- Logger.print("WHATAP-HOME", `CURRENT_WORKING_DIRECTORY is ${whatapHome}`, true);
1186
+ Logger.print("WHATAP-058", "WHATAP_HOME is empty", true);
1032
1187
  }
1033
1188
  }
1034
1189
 
@@ -1037,7 +1192,10 @@ NodeAgent.prototype.initConfig = function(home) {
1037
1192
  }
1038
1193
 
1039
1194
  process.env[home] = whatapHome;
1040
- const configFile = path.join(process.env[home], 'whatap.conf');
1195
+
1196
+ // Use WHATAP_CONF environment variable or default to 'whatap.conf'
1197
+ const configFileName = process.env.WHATAP_CONF || 'whatap.conf';
1198
+ const configFile = path.join(process.env[home], configFileName);
1041
1199
 
1042
1200
  if (!fs.existsSync(configFile)) {
1043
1201
  try {
@@ -1051,8 +1209,7 @@ NodeAgent.prototype.initConfig = function(home) {
1051
1209
  fs.writeFileSync(configFile, '# WhaTap Node.js Agent Configuration\n');
1052
1210
  }
1053
1211
  } catch (e) {
1054
- Logger.printError("WHATAP-CONFIG", "Permission error creating config file", e, true);
1055
- Logger.print("WHATAP-CONFIG", 'Try to execute command: `sudo chmod -R 777 $WHATAP_HOME`', true);
1212
+ Logger.printError("WHATAP-059", "Permission error creating config file. Try to execute command: `sudo chmod -R 777 $WHATAP_HOME`", e, false);
1056
1213
  return false;
1057
1214
  }
1058
1215
  }