@senzops/apm-node 1.1.16 → 1.1.17

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.
@@ -1,6 +1,6 @@
1
1
  import { Transport } from './transport';
2
2
  import { Context } from './context';
3
- import { SenzorOptions, ActiveTrace, TaskRun } from './types';
3
+ import { SenzorOptions, ActiveTrace, TaskRun, SenzorLog } from './types';
4
4
  import { randomUUID } from 'crypto';
5
5
  import { instrumentHttp, instrumentFetch } from '../instrumentation/http';
6
6
  import { instrumentMongo } from '../instrumentation/mongo';
@@ -8,10 +8,23 @@ import { instrumentPg } from '../instrumentation/pg';
8
8
  import { instrumentBullMQ } from '../instrumentation/bullmq';
9
9
  import { instrumentNodeCron } from '../instrumentation/cron';
10
10
  import { SDK_META } from '../utils/sdkMeta';
11
- import { parseTraceparent } from '../utils/traceContext'; // NEW
11
+ import { parseTraceparent } from '../utils/traceContext';
12
12
 
13
13
  const generateW3CTraceId = () => randomUUID().replace(/-/g, '');
14
14
 
15
+ // Memory-safe JSON stringifier to handle cyclical objects
16
+ // (like Express 'req' objects) passed into console.log
17
+ const safeStringify = (obj: any): string => {
18
+ const cache = new Set();
19
+ return JSON.stringify(obj, (key, value) => {
20
+ if (typeof value === 'object' && value !== null) {
21
+ if (cache.has(value)) return '[Circular]';
22
+ cache.add(value);
23
+ }
24
+ return value;
25
+ });
26
+ };
27
+
15
28
  export class SenzorClient {
16
29
  private transport: Transport | null = null;
17
30
  private options: SenzorOptions | null = null;
@@ -30,21 +43,101 @@ export class SenzorClient {
30
43
 
31
44
  if (!this.isInstrumented) {
32
45
  this.setupGlobalErrorHandlers();
46
+ this.setupLogInterception(); // Fire up Auto Log Instrumentation
33
47
 
34
48
  try { instrumentHttp(endpoint, debug); } catch (e) { }
35
49
  try { instrumentFetch(endpoint, debug); } catch (e) { }
36
50
  try { instrumentMongo(debug); } catch (e) { }
37
51
  try { instrumentPg(); } catch (e) { }
38
52
 
39
- // Task Integrations (NEW)
53
+ // Task Integrations
40
54
  try { instrumentBullMQ(this, debug); } catch (e) { }
41
55
  try { instrumentNodeCron(this, debug); } catch (e) { }
42
56
 
43
57
  this.isInstrumented = true;
44
- if (debug) console.log('[Senzor] Auto-instrumentation & Error Tracking enabled');
58
+ if (debug) console.log('[Senzor] Auto-instrumentation enabled');
45
59
  }
46
60
  }
47
61
 
62
+ // --- Enterprise Auto-Log Interception ---
63
+ private setupLogInterception() {
64
+ if (this.options?.autoLogs === false) return; // Opt-out check
65
+
66
+ const levels = ['log', 'info', 'warn', 'error', 'debug'] as const;
67
+ const originalConsole = {
68
+ log: console.log,
69
+ info: console.info,
70
+ warn: console.warn,
71
+ error: console.error,
72
+ debug: console.debug
73
+ };
74
+
75
+ let isIntercepting = false; // Lock to prevent SDK internal logs from looping infinitely
76
+
77
+ levels.forEach(level => {
78
+ console[level] = (...args: any[]) => {
79
+ // Always execute original console so user's terminal isn't broken
80
+ originalConsole[level].apply(console, args);
81
+
82
+ if (isIntercepting || !this.transport) return;
83
+ isIntercepting = true;
84
+
85
+ try {
86
+ let message = '';
87
+ let attributes: Record<string, any> = {};
88
+
89
+ args.forEach(arg => {
90
+ if (typeof arg === 'string') {
91
+ message += (message ? ' ' : '') + arg;
92
+ } else if (arg instanceof Error) {
93
+ message += (message ? ' ' : '') + arg.message;
94
+ attributes.errorStack = arg.stack;
95
+ attributes.errorName = arg.name;
96
+ } else if (typeof arg === 'object' && arg !== null) {
97
+ try {
98
+ // New Relic Style Destructuring: Merge all object keys into `attributes`
99
+ const parsed = JSON.parse(safeStringify(arg));
100
+ attributes = { ...attributes, ...parsed };
101
+ } catch (e) {
102
+ attributes.unparseableObject = true;
103
+ }
104
+ } else {
105
+ message += (message ? ' ' : '') + String(arg);
106
+ }
107
+ });
108
+
109
+ // Fallback if the user purely logged an object without text e.g., console.log({ user: 123 })
110
+ if (!message && Object.keys(attributes).length > 0) {
111
+ message = 'Object Log';
112
+ }
113
+
114
+ // Attach to Active Context seamlessly (Works for BOTH APM and Tasks!)
115
+ const currentTrace = Context.current();
116
+ const logType = currentTrace?.contextType === 'task' ? 'task' : 'apm';
117
+
118
+ const logPayload: SenzorLog = {
119
+ message: message || 'Empty log',
120
+ level: level === 'log' ? 'info' : level, // Map generic log -> info
121
+ attributes,
122
+ timestamp: new Date().toISOString()
123
+ };
124
+
125
+ // Attach the specific contextual ID
126
+ if (currentTrace) {
127
+ if (logType === 'task') logPayload.runId = currentTrace.id;
128
+ else logPayload.traceId = currentTrace.id;
129
+ }
130
+
131
+ this.transport.addLog(logPayload, logType);
132
+ } catch (e) {
133
+ // Absolute failure isolation. Never crash host app during logging.
134
+ } finally {
135
+ isIntercepting = false; // Release lock
136
+ }
137
+ };
138
+ });
139
+ }
140
+
48
141
  private setupGlobalErrorHandlers() {
49
142
  if ((process as any).__senzorGlobalHandlersInstalled) {
50
143
  return;
@@ -97,21 +190,14 @@ export class SenzorClient {
97
190
  }
98
191
  const enrichedMeta = {
99
192
  ...meta,
100
- runtime: {
101
- name: 'node',
102
- version: process.version
103
- },
193
+ runtime: { name: 'node', version: process.version },
104
194
  process: getProcessContext(),
105
195
  memory: getMemoryContext(),
106
- sdk: {
107
- name: SDK_META.name,
108
- version: SDK_META.version
109
- }
196
+ sdk: { name: SDK_META.name, version: SDK_META.version }
110
197
  };
111
198
 
112
199
  this.captureError(parsedError, enrichedMeta);
113
200
  } catch (internalFailure) {
114
- // NEVER allow SDK to crash host app
115
201
  try {
116
202
  if (this.options?.debug) {
117
203
  console.error('[Senzor] Error handler failure:', internalFailure);
@@ -120,69 +206,14 @@ export class SenzorClient {
120
206
  }
121
207
  };
122
208
 
123
- process.on('uncaughtExceptionMonitor', (error) => {
124
- safeCapture(error, {
125
- type: 'uncaughtExceptionMonitor',
126
- severity: 'fatal'
127
- });
128
- });
129
-
130
- process.on('uncaughtException', (error) => {
131
- safeCapture(error, {
132
- type: 'uncaughtException',
133
- severity: 'fatal'
134
- });
135
- });
136
-
137
- process.on('unhandledRejection', (reason) => {
138
- safeCapture(reason, {
139
- type: 'unhandledRejection',
140
- severity: 'error'
141
- });
142
- });
143
-
144
- process.on('warning', (warning) => {
145
- safeCapture(warning, {
146
- type: 'processWarning',
147
- severity: 'warning'
148
- });
149
- });
150
-
151
- process.on('multipleResolves', (type, promise, reason) => {
152
- safeCapture(reason || new Error('Multiple promise resolves'), {
153
- type: 'multipleResolves',
154
- resolveType: type,
155
- severity: 'warning'
156
- });
157
- });
158
-
159
- process.on('rejectionHandled', (promise) => {
160
- if (this.options?.debug) {
161
- try {
162
- console.warn('[Senzor] rejectionHandled event detected');
163
- } catch { }
164
- }
165
- });
166
-
167
- process.on('SIGTERM', () => {
168
- safeCapture(
169
- new Error('Process received SIGTERM'),
170
- {
171
- type: 'processSignal',
172
- signal: 'SIGTERM'
173
- }
174
- );
175
- });
176
-
177
- process.on('SIGINT', () => {
178
- safeCapture(
179
- new Error('Process received SIGINT'),
180
- {
181
- type: 'processSignal',
182
- signal: 'SIGINT'
183
- }
184
- );
185
- });
209
+ process.on('uncaughtExceptionMonitor', (error) => safeCapture(error, { type: 'uncaughtExceptionMonitor', severity: 'fatal' }));
210
+ process.on('uncaughtException', (error) => safeCapture(error, { type: 'uncaughtException', severity: 'fatal' }));
211
+ process.on('unhandledRejection', (reason) => safeCapture(reason, { type: 'unhandledRejection', severity: 'error' }));
212
+ process.on('warning', (warning) => safeCapture(warning, { type: 'processWarning', severity: 'warning' }));
213
+ process.on('multipleResolves', (type, promise, reason) => safeCapture(reason || new Error('Multiple promise resolves'), { type: 'multipleResolves', resolveType: type, severity: 'warning' }));
214
+ process.on('rejectionHandled', (promise) => { if (this.options?.debug) { try { console.warn('[Senzor] rejectionHandled event detected'); } catch { } } });
215
+ process.on('SIGTERM', () => safeCapture(new Error('Process received SIGTERM'), { type: 'processSignal', signal: 'SIGTERM' }));
216
+ process.on('SIGINT', () => safeCapture(new Error('Process received SIGINT'), { type: 'processSignal', signal: 'SIGINT' }));
186
217
  }
187
218
 
188
219
  public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
@@ -198,7 +229,6 @@ export class SenzorClient {
198
229
  return undefined;
199
230
  };
200
231
 
201
- // 1. Prioritize standard W3C Context (e.g., from RUM Frontend)
202
232
  const traceparent = getHeader('traceparent');
203
233
  const parsedContext = parseTraceparent(traceparent);
204
234
 
@@ -206,7 +236,6 @@ export class SenzorClient {
206
236
  inheritedTraceId = parsedContext.traceId;
207
237
  inheritedParentSpanId = parsedContext.parentSpanId;
208
238
  } else {
209
- // 2. Fallback to legacy proprietary headers
210
239
  const rawTrace = getHeader('x-senzor-trace-id');
211
240
  const rawSpan = getHeader('x-senzor-parent-span-id');
212
241
  inheritedTraceId = Array.isArray(rawTrace) ? rawTrace[0] : rawTrace;
@@ -214,18 +243,13 @@ export class SenzorClient {
214
243
  }
215
244
  }
216
245
 
217
- // Crucial: ADOPT the inherited traceId to perfectly link Frontend & Backend
218
246
  const activeTraceId = inheritedTraceId || generateW3CTraceId();
219
247
 
220
248
  const trace: ActiveTrace = {
221
249
  id: activeTraceId,
222
- contextType: 'apm', // Ensure we distinguish APM traces from Background Tasks
250
+ contextType: 'apm',
223
251
  startTime: performance.now(),
224
- data: {
225
- ...data,
226
- parentTraceId: inheritedTraceId,
227
- parentSpanId: inheritedParentSpanId
228
- },
252
+ data: { ...data, parentTraceId: inheritedTraceId, parentSpanId: inheritedParentSpanId },
229
253
  spans: []
230
254
  };
231
255
 
@@ -255,7 +279,6 @@ export class SenzorClient {
255
279
  const currentContext = Context.current();
256
280
  const triggerTraceId = currentContext?.contextType === 'apm' ? currentContext.id : undefined;
257
281
 
258
- // Snapshot system resources before execution
259
282
  const startMemory = process.memoryUsage ? process.memoryUsage().heapUsed : 0;
260
283
  const startCpu = process.cpuUsage ? process.cpuUsage() : undefined;
261
284
 
@@ -275,14 +298,13 @@ export class SenzorClient {
275
298
  const task = Context.current();
276
299
  if (!task || task.contextType !== 'task' || !this.transport) return;
277
300
 
278
- // Calculate resource deltas
279
301
  let resourceMetrics;
280
302
  if (process.memoryUsage && task.startMemory !== undefined && process.cpuUsage && task.startCpu) {
281
303
  const endMemory = process.memoryUsage().heapUsed;
282
304
  const cpuDelta = process.cpuUsage(task.startCpu);
283
305
 
284
306
  resourceMetrics = {
285
- memoryDeltaBytes: endMemory - task.startMemory, // Can be negative if GC ran!
307
+ memoryDeltaBytes: endMemory - task.startMemory,
286
308
  cpuUserUs: cpuDelta.user,
287
309
  cpuSystemUs: cpuDelta.system
288
310
  };
@@ -295,7 +317,7 @@ export class SenzorClient {
295
317
  triggerTraceId: task.data.triggerTraceId,
296
318
  queueDelay: task.data.queueDelay,
297
319
  attempts: task.data.attempts,
298
- isDeadLetter: task.data.isDeadLetter, // Extracted from options/metadata if provided
320
+ isDeadLetter: task.data.isDeadLetter,
299
321
  metadata: { ...task.data.metadata, ...extraMetadata },
300
322
  resourceMetrics,
301
323
  status,
@@ -323,7 +345,6 @@ export class SenzorClient {
323
345
  }) as unknown as T;
324
346
  }
325
347
 
326
- // --- MODIFIED: Context-Aware Error Capture ---
327
348
  public captureError(error: unknown, context: any = {}) {
328
349
  if (!this.transport) return;
329
350
 
@@ -360,7 +381,6 @@ export class SenzorClient {
360
381
  if (!trace) return { end: () => { } };
361
382
  const startTime = performance.now() - trace.startTime;
362
383
  const spanStartAbs = performance.now();
363
- // Use 16 char hex for span IDs for W3C compatibility
364
384
  const spanId = randomUUID().replace(/-/g, '').slice(0, 16);
365
385
  return { end: (meta?: any, status?: number) => { Context.addSpan({ spanId, name, type, startTime, duration: performance.now() - spanStartAbs, status, meta }); } };
366
386
  }
@@ -1,11 +1,13 @@
1
- import { SenzorOptions, Trace, TaskRun, SenzorError } from './types';
1
+ import { SenzorOptions, Trace, TaskRun, SenzorError, SenzorLog } from './types';
2
2
 
3
3
  export class Transport {
4
4
  private traceQueue: Trace[] = [];
5
5
  private apmErrorQueue: SenzorError[] = [];
6
+ private apmLogQueue: SenzorLog[] = []; // APM Logs
6
7
 
7
8
  private taskQueue: TaskRun[] = [];
8
9
  private taskErrorQueue: SenzorError[] = [];
10
+ private taskLogQueue: SenzorLog[] = []; // Task Logs
9
11
 
10
12
  private timer: NodeJS.Timeout | null = null;
11
13
  private apmEndpoint: string;
@@ -41,40 +43,59 @@ export class Transport {
41
43
  this.checkFlush();
42
44
  }
43
45
 
46
+ // Add captured log to the correct batch queue
47
+ public addLog(log: SenzorLog, type: 'apm' | 'task' = 'apm') {
48
+ if (type === 'task') this.taskLogQueue.push(log);
49
+ else this.apmLogQueue.push(log);
50
+ this.checkFlush();
51
+ }
52
+
44
53
  private checkFlush() {
45
- const totalApm = this.traceQueue.length + this.apmErrorQueue.length;
46
- const totalTask = this.taskQueue.length + this.taskErrorQueue.length;
54
+ const totalApm = this.traceQueue.length + this.apmErrorQueue.length + this.apmLogQueue.length;
55
+ const totalTask = this.taskQueue.length + this.taskErrorQueue.length + this.taskLogQueue.length;
47
56
  if (totalApm >= (this.config.batchSize || 100) || totalTask >= (this.config.batchSize || 100)) {
48
57
  this.flush();
49
58
  }
50
59
  }
51
60
 
52
61
  public async flush() {
53
- const apmPayload = { traces: [...this.traceQueue], errors: [...this.apmErrorQueue] };
54
- const taskPayload = { runs: [...this.taskQueue], errors: [...this.taskErrorQueue] };
55
-
62
+ const apmPayload = {
63
+ traces: [...this.traceQueue],
64
+ errors: [...this.apmErrorQueue],
65
+ logs: [...this.apmLogQueue]
66
+ };
67
+ const taskPayload = {
68
+ runs: [...this.taskQueue],
69
+ errors: [...this.taskErrorQueue],
70
+ logs: [...this.taskLogQueue]
71
+ };
72
+
73
+ // Reset Queues instantly
56
74
  this.traceQueue = [];
57
75
  this.apmErrorQueue = [];
76
+ this.apmLogQueue = [];
58
77
  this.taskQueue = [];
59
78
  this.taskErrorQueue = [];
79
+ this.taskLogQueue = [];
60
80
 
61
81
  const headers = { 'Content-Type': 'application/json', 'x-service-api-key': this.config.apiKey };
62
82
 
63
83
  try {
64
84
  const promises = [];
65
85
 
66
- if (apmPayload.traces.length > 0 || apmPayload.errors.length > 0) {
86
+ // Piggyback logs onto APM/Task batch ingestion to bypass extra network round-trips
87
+ if (apmPayload.traces.length > 0 || apmPayload.errors.length > 0 || apmPayload.logs.length > 0) {
67
88
  promises.push(fetch(this.apmEndpoint, { method: 'POST', headers, body: JSON.stringify(apmPayload), keepalive: true }));
68
89
  }
69
90
 
70
- if (taskPayload.runs.length > 0 || taskPayload.errors.length > 0) {
91
+ if (taskPayload.runs.length > 0 || taskPayload.errors.length > 0 || taskPayload.logs.length > 0) {
71
92
  promises.push(fetch(this.taskEndpoint, { method: 'POST', headers, body: JSON.stringify(taskPayload), keepalive: true }));
72
93
  }
73
94
 
74
95
  await Promise.allSettled(promises);
75
96
 
76
97
  if (this.config.debug) {
77
- console.log(`[Senzor] Flushed: ${apmPayload.traces.length} traces, ${taskPayload.runs.length} tasks`);
98
+ console.log(`[Senzor] Flushed: APM(${apmPayload.traces.length} traces, ${apmPayload.logs.length} logs), Task(${taskPayload.runs.length} runs, ${taskPayload.logs.length} logs)`);
78
99
  }
79
100
  } catch (err) {
80
101
  if (this.config.debug) console.error('[Senzor] Transport Flush Error:', err);
package/src/core/types.ts CHANGED
@@ -4,6 +4,7 @@ export interface SenzorOptions {
4
4
  batchSize?: number;
5
5
  flushInterval?: number;
6
6
  debug?: boolean;
7
+ autoLogs?: boolean;
7
8
  }
8
9
 
9
10
  export interface Span {
@@ -26,6 +27,17 @@ export interface SenzorError {
26
27
  timestamp: string;
27
28
  }
28
29
 
30
+ // NEW: Enterprise Log Payload
31
+ export interface SenzorLog {
32
+ message: string;
33
+ level: 'info' | 'warn' | 'error' | 'debug' | 'fatal';
34
+ attributes: Record<string, any>;
35
+ traceId?: string; // Used if context is APM
36
+ runId?: string; // Used if context is Task
37
+ spanId?: string;
38
+ timestamp: string;
39
+ }
40
+
29
41
  export interface Trace {
30
42
  traceId: string;
31
43
  parentTraceId?: string;