@senzops/apm-node 1.2.3 → 1.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senzops/apm-node",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Universal APM SDK for Senzor",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -2,23 +2,23 @@ import { Transport } from './transport';
2
2
  import { Context } from './context';
3
3
  import { SenzorOptions, ActiveTrace, TaskRun, SenzorLog } from './types';
4
4
  import { randomUUID } from 'crypto';
5
- import { instrumentHttp, instrumentFetch } from '../instrumentation/http';
6
- import { instrumentExpress } from '../instrumentation/express';
7
- import { instrumentFastify } from '../instrumentation/fastify';
8
- import { instrumentKoa } from '../instrumentation/koa';
9
- import { instrumentMongo } from '../instrumentation/mongo';
10
- import { instrumentPg } from '../instrumentation/pg';
11
- import { instrumentUndici } from '../instrumentation/undici';
12
- import { instrumentRedis } from '../instrumentation/redis';
13
- import { instrumentMysql } from '../instrumentation/mysql';
14
- import { instrumentMongoose } from '../instrumentation/mongoose';
15
- import { instrumentBullMQ } from '../instrumentation/bullmq';
16
- import { instrumentNodeCron } from '../instrumentation/cron';
17
- import { SDK_META } from '../utils/sdkMeta';
18
- import { parseTraceparent } from '../utils/traceContext';
19
- import { generateSpanId, generateTraceId } from '../utils/ids';
20
- import { sanitizeAttributes } from './sanitizer';
21
- import { startCapturedSpan } from '../instrumentation/span';
5
+ import { instrumentHttp, instrumentFetch } from '../instrumentation/http';
6
+ import { instrumentExpress } from '../instrumentation/express';
7
+ import { instrumentFastify } from '../instrumentation/fastify';
8
+ import { instrumentKoa } from '../instrumentation/koa';
9
+ import { instrumentMongo } from '../instrumentation/mongo';
10
+ import { instrumentPg } from '../instrumentation/pg';
11
+ import { instrumentUndici } from '../instrumentation/undici';
12
+ import { instrumentRedis } from '../instrumentation/redis';
13
+ import { instrumentMysql } from '../instrumentation/mysql';
14
+ import { instrumentMongoose } from '../instrumentation/mongoose';
15
+ import { instrumentBullMQ } from '../instrumentation/bullmq';
16
+ import { instrumentNodeCron } from '../instrumentation/cron';
17
+ import { SDK_META } from '../utils/sdkMeta';
18
+ import { parseTraceparent } from '../utils/traceContext';
19
+ import { generateSpanId, generateTraceId } from '../utils/ids';
20
+ import { sanitizeAttributes } from './sanitizer';
21
+ import { startCapturedSpan } from '../instrumentation/span';
22
22
 
23
23
  // Memory-safe JSON stringifier to handle cyclical objects
24
24
  // (like Express 'req' objects) passed into console.log
@@ -33,69 +33,69 @@ const safeStringify = (obj: any): string => {
33
33
  });
34
34
  };
35
35
 
36
- export class SenzorClient {
37
- private transport: Transport | null = null;
38
- private options: SenzorOptions | null = null;
39
- private isInstrumented = false;
40
-
41
- public preload(options: Partial<SenzorOptions> = {}) {
42
- const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
43
- const debug = options.debug || false;
44
-
45
- this.options = {
46
- apiKey: '',
47
- ...this.options,
48
- ...options
49
- };
50
-
51
- this.installNativeInstrumentations(endpoint, debug);
52
- }
53
-
54
- public init(options: SenzorOptions) {
55
- if (!options.apiKey) {
56
- console.warn('[Senzor] API Key missing. SDK disabled.');
57
- return;
58
- }
36
+ export class SenzorClient {
37
+ private transport: Transport | null = null;
38
+ private options: SenzorOptions | null = null;
39
+ private isInstrumented = false;
40
+
41
+ public preload(options: Partial<SenzorOptions> = {}) {
42
+ const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
43
+ const debug = options.debug || false;
44
+
45
+ this.options = {
46
+ apiKey: '',
47
+ ...this.options,
48
+ ...options
49
+ };
50
+
51
+ this.installNativeInstrumentations(endpoint, debug);
52
+ }
53
+
54
+ public init(options: SenzorOptions) {
55
+ if (!options.apiKey) {
56
+ console.warn('[Senzor] API Key missing. SDK disabled.');
57
+ return;
58
+ }
59
59
  this.options = options;
60
60
  const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
61
61
  const debug = options.debug || false;
62
-
63
- this.transport = new Transport({ ...options, endpoint });
64
- this.installNativeInstrumentations(endpoint, debug);
65
- }
66
-
67
- private isInstrumentationEnabled(name: string): boolean {
68
- const setting = this.options?.instrumentations;
69
- if (setting === false) return false;
70
- if (Array.isArray(setting)) return setting.includes(name);
71
- return true;
72
- }
73
-
74
- private installNativeInstrumentations(endpoint: string, debug: boolean) {
75
- if (!this.isInstrumented) {
76
- this.setupGlobalErrorHandlers();
77
- this.setupLogInterception(); // Fire up Auto Log Instrumentation
78
-
79
- try { if (this.isInstrumentationEnabled('http')) instrumentHttp(this, endpoint, this.options || undefined); } catch (e) { }
80
- try { if (this.isInstrumentationEnabled('express')) instrumentExpress(this.options || undefined); } catch (e) { }
81
- try { if (this.isInstrumentationEnabled('fastify')) instrumentFastify(this.options || undefined); } catch (e) { }
82
- try { if (this.isInstrumentationEnabled('koa')) instrumentKoa(this.options || undefined); } catch (e) { }
83
- try { if (this.isInstrumentationEnabled('fetch')) instrumentFetch(endpoint, this.options || undefined); } catch (e) { }
84
- try { if (this.isInstrumentationEnabled('undici')) instrumentUndici(this.options || undefined); } catch (e) { }
85
- try { if (this.isInstrumentationEnabled('mongo')) instrumentMongo(this.options || undefined); } catch (e) { }
86
- try { if (this.isInstrumentationEnabled('mongoose')) instrumentMongoose(this.options || undefined); } catch (e) { }
87
- try { if (this.isInstrumentationEnabled('pg')) instrumentPg(this.options || undefined); } catch (e) { }
88
- try { if (this.isInstrumentationEnabled('mysql')) instrumentMysql(this.options || undefined); } catch (e) { }
89
- try { if (this.isInstrumentationEnabled('redis')) instrumentRedis(this.options || undefined); } catch (e) { }
90
-
91
- // Task Integrations
92
- try { if (this.isInstrumentationEnabled('bullmq')) instrumentBullMQ(this, debug); } catch (e) { }
93
- try { if (this.isInstrumentationEnabled('cron')) instrumentNodeCron(this, debug); } catch (e) { }
94
-
95
- this.isInstrumented = true;
96
- if (debug) console.log('[Senzor] Auto-instrumentation enabled');
97
- }
98
- }
62
+
63
+ this.transport = new Transport({ ...options, endpoint });
64
+ this.installNativeInstrumentations(endpoint, debug);
65
+ }
66
+
67
+ private isInstrumentationEnabled(name: string): boolean {
68
+ const setting = this.options?.instrumentations;
69
+ if (setting === false) return false;
70
+ if (Array.isArray(setting)) return setting.includes(name);
71
+ return true;
72
+ }
73
+
74
+ private installNativeInstrumentations(endpoint: string, debug: boolean) {
75
+ if (!this.isInstrumented) {
76
+ this.setupGlobalErrorHandlers();
77
+ this.setupLogInterception(); // Fire up Auto Log Instrumentation
78
+
79
+ try { if (this.isInstrumentationEnabled('http')) instrumentHttp(this, endpoint, this.options || undefined); } catch (e) { }
80
+ try { if (this.isInstrumentationEnabled('express')) instrumentExpress(this.options || undefined); } catch (e) { }
81
+ try { if (this.isInstrumentationEnabled('fastify')) instrumentFastify(this.options || undefined); } catch (e) { }
82
+ try { if (this.isInstrumentationEnabled('koa')) instrumentKoa(this.options || undefined); } catch (e) { }
83
+ try { if (this.isInstrumentationEnabled('fetch')) instrumentFetch(endpoint, this.options || undefined); } catch (e) { }
84
+ try { if (this.isInstrumentationEnabled('undici')) instrumentUndici(this.options || undefined); } catch (e) { }
85
+ try { if (this.isInstrumentationEnabled('mongo')) instrumentMongo(this.options || undefined); } catch (e) { }
86
+ try { if (this.isInstrumentationEnabled('mongoose')) instrumentMongoose(this.options || undefined); } catch (e) { }
87
+ try { if (this.isInstrumentationEnabled('pg')) instrumentPg(this.options || undefined); } catch (e) { }
88
+ try { if (this.isInstrumentationEnabled('mysql')) instrumentMysql(this.options || undefined); } catch (e) { }
89
+ try { if (this.isInstrumentationEnabled('redis')) instrumentRedis(this.options || undefined); } catch (e) { }
90
+
91
+ // Task Integrations
92
+ try { if (this.isInstrumentationEnabled('bullmq')) instrumentBullMQ(this, debug); } catch (e) { }
93
+ try { if (this.isInstrumentationEnabled('cron')) instrumentNodeCron(this, debug); } catch (e) { }
94
+
95
+ this.isInstrumented = true;
96
+ if (debug) console.log('[Senzor] Auto-instrumentation enabled');
97
+ }
98
+ }
99
99
 
100
100
  // --- Enterprise Auto-Log Interception ---
101
101
  private setupLogInterception() {
@@ -132,13 +132,13 @@ export class SenzorClient {
132
132
  attributes.errorStack = arg.stack;
133
133
  attributes.errorName = arg.name;
134
134
  } else if (typeof arg === 'object' && arg !== null) {
135
- try {
136
- // New Relic Style Destructuring: Merge all object keys into `attributes`
137
- const parsed = JSON.parse(safeStringify(arg));
138
- attributes = { ...attributes, ...sanitizeAttributes(parsed, this.options || undefined) };
139
- } catch (e) {
140
- attributes.unparseableObject = true;
141
- }
135
+ try {
136
+ // New Relic Style Destructuring: Merge all object keys into `attributes`
137
+ const parsed = JSON.parse(safeStringify(arg));
138
+ attributes = { ...attributes, ...sanitizeAttributes(parsed, this.options || undefined) };
139
+ } catch (e) {
140
+ attributes.unparseableObject = true;
141
+ }
142
142
  } else {
143
143
  message += (message ? ' ' : '') + String(arg);
144
144
  }
@@ -254,20 +254,17 @@ export class SenzorClient {
254
254
  process.on('SIGINT', () => safeCapture(new Error('Process received SIGINT'), { type: 'processSignal', signal: 'SIGINT' }));
255
255
  }
256
256
 
257
- public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
258
- if (!this.transport) return next();
259
-
260
- const existingTrace = Context.current();
261
- if (existingTrace?.contextType === 'apm') {
262
- existingTrace.data = {
263
- ...existingTrace.data,
264
- ...data
265
- };
266
- return next();
267
- }
268
-
269
- let inheritedTraceId: string | undefined = undefined;
270
- let inheritedParentSpanId: string | undefined = undefined;
257
+ public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
258
+ if (!this.transport) return next();
259
+
260
+ const existingTrace = Context.current();
261
+ if (existingTrace?.contextType === 'apm') {
262
+ Object.assign(existingTrace.data, data);
263
+ return next();
264
+ }
265
+
266
+ let inheritedTraceId: string | undefined = undefined;
267
+ let inheritedParentSpanId: string | undefined = undefined;
271
268
 
272
269
  if (data.headers) {
273
270
  const getHeader = (key: string) => {
@@ -290,44 +287,47 @@ export class SenzorClient {
290
287
  }
291
288
  }
292
289
 
293
- const activeTraceId = inheritedTraceId || generateTraceId();
294
- const rootSpanId = generateSpanId();
295
-
296
- const trace: ActiveTrace = {
297
- id: activeTraceId,
298
- contextType: 'apm',
299
- startTime: performance.now(),
300
- rootSpanId,
301
- activeSpanId: rootSpanId,
302
- data: { ...data, parentTraceId: inheritedTraceId, parentSpanId: inheritedParentSpanId, rootSpanId },
303
- spans: [],
304
- maxSpans: this.options?.maxSpansPerTrace ?? 500,
305
- droppedSpans: 0
306
- };
290
+ const activeTraceId = inheritedTraceId || generateTraceId();
291
+ const rootSpanId = generateSpanId();
292
+
293
+ const trace: ActiveTrace = {
294
+ id: activeTraceId,
295
+ contextType: 'apm',
296
+ startTime: performance.now(),
297
+ rootSpanId,
298
+ activeSpanId: rootSpanId,
299
+ data: { ...data, parentTraceId: inheritedTraceId, parentSpanId: inheritedParentSpanId, rootSpanId },
300
+ spans: [],
301
+ maxSpans: this.options?.maxSpansPerTrace ?? 500,
302
+ state: {
303
+ ended: false,
304
+ droppedSpans: 0
305
+ }
306
+ };
307
307
 
308
308
  return Context.run(trace, next);
309
309
  }
310
310
 
311
- public endTrace(status: number, extraData: any = {}) {
312
- const trace = Context.current();
313
- if (!trace || trace.contextType !== 'apm' || !this.transport) return;
314
- if (trace.ended) return;
315
- trace.ended = true;
316
- const duration = performance.now() - trace.startTime;
317
-
318
- const payload = {
319
- traceId: trace.id,
320
- parentTraceId: trace.data.parentTraceId,
321
- parentSpanId: trace.data.parentSpanId,
322
- rootSpanId: trace.rootSpanId,
323
- ...trace.data,
324
- ...extraData,
325
- status,
326
- duration,
327
- spans: trace.spans,
328
- droppedSpans: trace.droppedSpans,
329
- timestamp: new Date().toISOString()
330
- };
311
+ public endTrace(status: number, extraData: any = {}) {
312
+ const trace = Context.current();
313
+ if (!trace || trace.contextType !== 'apm' || !this.transport) return;
314
+ if (trace.state.ended) return;
315
+ trace.state.ended = true;
316
+ const duration = performance.now() - trace.startTime;
317
+
318
+ const payload = {
319
+ traceId: trace.id,
320
+ parentTraceId: trace.data.parentTraceId,
321
+ parentSpanId: trace.data.parentSpanId,
322
+ rootSpanId: trace.rootSpanId,
323
+ ...trace.data,
324
+ ...extraData,
325
+ status,
326
+ duration,
327
+ spans: trace.spans,
328
+ droppedSpans: trace.state.droppedSpans,
329
+ timestamp: new Date().toISOString()
330
+ };
331
331
  this.transport.addTrace(payload);
332
332
  }
333
333
 
@@ -341,25 +341,30 @@ export class SenzorClient {
341
341
  const startMemory = process.memoryUsage ? process.memoryUsage().heapUsed : 0;
342
342
  const startCpu = process.cpuUsage ? process.cpuUsage() : undefined;
343
343
 
344
- const task: ActiveTrace = {
345
- id: randomUUID(),
346
- contextType: 'task',
347
- startTime: performance.now(),
348
- rootSpanId: generateSpanId(),
349
- startMemory,
350
- startCpu,
351
- data: { taskName: name, taskType: type, triggerTraceId, ...options },
352
- spans: [],
353
- maxSpans: this.options?.maxSpansPerTrace ?? 500,
354
- droppedSpans: 0
355
- };
356
- task.activeSpanId = task.rootSpanId;
357
- return Context.run(task, next);
358
- }
344
+ const task: ActiveTrace = {
345
+ id: randomUUID(),
346
+ contextType: 'task',
347
+ startTime: performance.now(),
348
+ rootSpanId: generateSpanId(),
349
+ startMemory,
350
+ startCpu,
351
+ data: { taskName: name, taskType: type, triggerTraceId, ...options },
352
+ spans: [],
353
+ maxSpans: this.options?.maxSpansPerTrace ?? 500,
354
+ state: {
355
+ ended: false,
356
+ droppedSpans: 0
357
+ }
358
+ };
359
+ task.activeSpanId = task.rootSpanId;
360
+ return Context.run(task, next);
361
+ }
359
362
 
360
363
  public endTask(status: 'success' | 'failed', extraMetadata: any = {}) {
361
364
  const task = Context.current();
362
365
  if (!task || task.contextType !== 'task' || !this.transport) return;
366
+ if (task.state.ended) return;
367
+ task.state.ended = true;
363
368
 
364
369
  let resourceMetrics;
365
370
  if (process.memoryUsage && task.startMemory !== undefined && process.cpuUsage && task.startCpu) {
@@ -381,7 +386,7 @@ export class SenzorClient {
381
386
  queueDelay: task.data.queueDelay,
382
387
  attempts: task.data.attempts,
383
388
  isDeadLetter: task.data.isDeadLetter,
384
- metadata: { ...task.data.metadata, ...extraMetadata, droppedSpans: task.droppedSpans },
389
+ metadata: { ...task.data.metadata, ...extraMetadata, droppedSpans: task.state.droppedSpans },
385
390
  resourceMetrics,
386
391
  status,
387
392
  duration: performance.now() - task.startTime,
@@ -420,32 +425,32 @@ export class SenzorClient {
420
425
 
421
426
  const currentTrace = Context.current();
422
427
 
423
- const errPayload = {
424
- errorClass: parsedError.name || 'Error',
425
- message: parsedError.message,
426
- stackTrace: parsedError.stack,
427
- context: sanitizeAttributes(context, this.options || undefined),
428
- timestamp: new Date().toISOString()
429
- };
428
+ const errPayload = {
429
+ errorClass: parsedError.name || 'Error',
430
+ message: parsedError.message,
431
+ stackTrace: parsedError.stack,
432
+ context: sanitizeAttributes(context, this.options || undefined),
433
+ timestamp: new Date().toISOString()
434
+ };
430
435
 
431
436
  if (currentTrace?.contextType === 'task') {
432
437
  this.transport.addError({ ...errPayload, runId: currentTrace.id }, 'task');
433
438
  } else {
434
439
  this.transport.addError({ ...errPayload, traceId: currentTrace?.id }, 'apm');
435
440
  }
436
- }
437
-
438
- public track(data: any) {
439
- this.transport?.addTrace({ traceId: generateTraceId(), ...data, spans: [], timestamp: new Date().toISOString() });
440
- }
441
-
442
- public startSpan(name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') {
443
- const span = startCapturedSpan(name, type, {}, this.options || undefined);
444
- if (!span) return { end: () => { } };
445
- return { end: (meta?: any, status?: number) => span.end(status, meta) };
446
- }
441
+ }
442
+
443
+ public track(data: any) {
444
+ this.transport?.addTrace({ traceId: generateTraceId(), ...data, spans: [], timestamp: new Date().toISOString() });
445
+ }
446
+
447
+ public startSpan(name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') {
448
+ const span = startCapturedSpan(name, type, {}, this.options || undefined);
449
+ if (!span) return { end: () => { } };
450
+ return { end: (meta?: any, status?: number) => span.end(status, meta) };
451
+ }
447
452
 
448
453
  public async flush() { if (this.transport) await this.transport.flush(); }
449
454
  }
450
455
 
451
- export const client = new SenzorClient();
456
+ export const client = new SenzorClient();
@@ -35,11 +35,11 @@ export const Context = {
35
35
  },
36
36
 
37
37
  addSpanToTrace: (trace: ActiveTrace, span: Span) => {
38
- if (trace.ended) return;
38
+ if (trace.state.ended) return;
39
39
 
40
40
  const maxSpans = trace.maxSpans ?? 500;
41
41
  if (trace.spans.length >= maxSpans) {
42
- trace.droppedSpans = (trace.droppedSpans ?? 0) + 1;
42
+ trace.state.droppedSpans = (trace.state.droppedSpans ?? 0) + 1;
43
43
  return;
44
44
  }
45
45
 
package/src/core/types.ts CHANGED
@@ -1,32 +1,32 @@
1
- export interface SenzorOptions {
2
- apiKey: string;
3
- endpoint?: string;
4
- batchSize?: number;
5
- flushInterval?: number;
6
- flushTimeoutMs?: number;
7
- maxQueueSize?: number;
8
- maxSpansPerTrace?: number;
9
- maxAttributeLength?: number;
10
- maxAttributes?: number;
11
- captureHeaders?: boolean;
12
- captureDbStatement?: boolean;
13
- instrumentations?: boolean | string[];
14
- frameworkSpans?: boolean;
15
- captureMiddlewareSpans?: boolean;
16
- captureRouterSpans?: boolean;
17
- captureLifecycleHookSpans?: boolean;
18
- ignoreFrameworkSpanTypes?: string[];
19
- debug?: boolean;
20
- autoLogs?: boolean;
21
- }
22
-
23
- export interface Span {
24
- spanId: string;
25
- parentSpanId?: string;
26
- name: string;
27
- type: 'db' | 'http' | 'function' | 'custom';
28
- startTime: number;
29
- duration: number;
1
+ export interface SenzorOptions {
2
+ apiKey: string;
3
+ endpoint?: string;
4
+ batchSize?: number;
5
+ flushInterval?: number;
6
+ flushTimeoutMs?: number;
7
+ maxQueueSize?: number;
8
+ maxSpansPerTrace?: number;
9
+ maxAttributeLength?: number;
10
+ maxAttributes?: number;
11
+ captureHeaders?: boolean;
12
+ captureDbStatement?: boolean;
13
+ instrumentations?: boolean | string[];
14
+ frameworkSpans?: boolean;
15
+ captureMiddlewareSpans?: boolean;
16
+ captureRouterSpans?: boolean;
17
+ captureLifecycleHookSpans?: boolean;
18
+ ignoreFrameworkSpanTypes?: string[];
19
+ debug?: boolean;
20
+ autoLogs?: boolean;
21
+ }
22
+
23
+ export interface Span {
24
+ spanId: string;
25
+ parentSpanId?: string;
26
+ name: string;
27
+ type: 'db' | 'http' | 'function' | 'custom';
28
+ startTime: number;
29
+ duration: number;
30
30
  status?: number;
31
31
  meta?: Record<string, any>;
32
32
  }
@@ -89,18 +89,23 @@ export interface TaskRun {
89
89
  timestamp: string;
90
90
  }
91
91
 
92
+ // NEW: Shared mutable state for a trace/task to prevent duplication during context shallow copying
93
+ export interface ActiveTraceState {
94
+ ended: boolean;
95
+ droppedSpans: number;
96
+ }
97
+
92
98
  // Unified Context Payload for async_hooks
93
- export interface ActiveTrace {
94
- id: string; // The APM traceId OR the Task runId
95
- contextType: 'apm' | 'task';
96
- startTime: number;
97
- rootSpanId?: string;
98
- activeSpanId?: string;
99
- startMemory?: number; // Baseline heap
100
- startCpu?: NodeJS.CpuUsage; // Baseline CPU tick
101
- data: any; // Holds Partial<Trace> or Partial<TaskRun>
102
- spans: Span[];
103
- maxSpans?: number;
104
- droppedSpans?: number;
105
- ended?: boolean;
106
- }
99
+ export interface ActiveTrace {
100
+ id: string; // The APM traceId OR the Task runId
101
+ contextType: 'apm' | 'task';
102
+ startTime: number;
103
+ rootSpanId?: string;
104
+ activeSpanId?: string;
105
+ startMemory?: number; // Baseline heap
106
+ startCpu?: NodeJS.CpuUsage; // Baseline CPU tick
107
+ data: any; // Holds Partial<Trace> or Partial<TaskRun>
108
+ spans: Span[];
109
+ maxSpans?: number;
110
+ state: ActiveTraceState;
111
+ }