@senzops/apm-node 1.1.15 → 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,6 +8,22 @@ 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';
12
+
13
+ const generateW3CTraceId = () => randomUUID().replace(/-/g, '');
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
+ };
11
27
 
12
28
  export class SenzorClient {
13
29
  private transport: Transport | null = null;
@@ -27,21 +43,101 @@ export class SenzorClient {
27
43
 
28
44
  if (!this.isInstrumented) {
29
45
  this.setupGlobalErrorHandlers();
46
+ this.setupLogInterception(); // Fire up Auto Log Instrumentation
30
47
 
31
48
  try { instrumentHttp(endpoint, debug); } catch (e) { }
32
49
  try { instrumentFetch(endpoint, debug); } catch (e) { }
33
50
  try { instrumentMongo(debug); } catch (e) { }
34
51
  try { instrumentPg(); } catch (e) { }
35
52
 
36
- // Task Integrations (NEW)
53
+ // Task Integrations
37
54
  try { instrumentBullMQ(this, debug); } catch (e) { }
38
55
  try { instrumentNodeCron(this, debug); } catch (e) { }
39
56
 
40
57
  this.isInstrumented = true;
41
- if (debug) console.log('[Senzor] Auto-instrumentation & Error Tracking enabled');
58
+ if (debug) console.log('[Senzor] Auto-instrumentation enabled');
42
59
  }
43
60
  }
44
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
+
45
141
  private setupGlobalErrorHandlers() {
46
142
  if ((process as any).__senzorGlobalHandlersInstalled) {
47
143
  return;
@@ -94,21 +190,14 @@ export class SenzorClient {
94
190
  }
95
191
  const enrichedMeta = {
96
192
  ...meta,
97
- runtime: {
98
- name: 'node',
99
- version: process.version
100
- },
193
+ runtime: { name: 'node', version: process.version },
101
194
  process: getProcessContext(),
102
195
  memory: getMemoryContext(),
103
- sdk: {
104
- name: SDK_META.name,
105
- version: SDK_META.version
106
- }
196
+ sdk: { name: SDK_META.name, version: SDK_META.version }
107
197
  };
108
198
 
109
199
  this.captureError(parsedError, enrichedMeta);
110
200
  } catch (internalFailure) {
111
- // NEVER allow SDK to crash host app
112
201
  try {
113
202
  if (this.options?.debug) {
114
203
  console.error('[Senzor] Error handler failure:', internalFailure);
@@ -117,76 +206,21 @@ export class SenzorClient {
117
206
  }
118
207
  };
119
208
 
120
- process.on('uncaughtExceptionMonitor', (error) => {
121
- safeCapture(error, {
122
- type: 'uncaughtExceptionMonitor',
123
- severity: 'fatal'
124
- });
125
- });
126
-
127
- process.on('uncaughtException', (error) => {
128
- safeCapture(error, {
129
- type: 'uncaughtException',
130
- severity: 'fatal'
131
- });
132
- });
133
-
134
- process.on('unhandledRejection', (reason) => {
135
- safeCapture(reason, {
136
- type: 'unhandledRejection',
137
- severity: 'error'
138
- });
139
- });
140
-
141
- process.on('warning', (warning) => {
142
- safeCapture(warning, {
143
- type: 'processWarning',
144
- severity: 'warning'
145
- });
146
- });
147
-
148
- process.on('multipleResolves', (type, promise, reason) => {
149
- safeCapture(reason || new Error('Multiple promise resolves'), {
150
- type: 'multipleResolves',
151
- resolveType: type,
152
- severity: 'warning'
153
- });
154
- });
155
-
156
- process.on('rejectionHandled', (promise) => {
157
- if (this.options?.debug) {
158
- try {
159
- console.warn('[Senzor] rejectionHandled event detected');
160
- } catch { }
161
- }
162
- });
163
-
164
- process.on('SIGTERM', () => {
165
- safeCapture(
166
- new Error('Process received SIGTERM'),
167
- {
168
- type: 'processSignal',
169
- signal: 'SIGTERM'
170
- }
171
- );
172
- });
173
-
174
- process.on('SIGINT', () => {
175
- safeCapture(
176
- new Error('Process received SIGINT'),
177
- {
178
- type: 'processSignal',
179
- signal: 'SIGINT'
180
- }
181
- );
182
- });
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' }));
183
217
  }
184
218
 
185
219
  public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
186
220
  if (!this.transport) return next();
187
221
 
188
- let parentTraceId = undefined;
189
- let parentSpanId = undefined;
222
+ let inheritedTraceId: string | undefined = undefined;
223
+ let inheritedParentSpanId: string | undefined = undefined;
190
224
 
191
225
  if (data.headers) {
192
226
  const getHeader = (key: string) => {
@@ -195,22 +229,27 @@ export class SenzorClient {
195
229
  return undefined;
196
230
  };
197
231
 
198
- parentTraceId = getHeader('x-senzor-trace-id');
199
- parentSpanId = getHeader('x-senzor-parent-span-id');
200
-
201
- if (Array.isArray(parentTraceId)) parentTraceId = parentTraceId[0];
202
- if (Array.isArray(parentSpanId)) parentSpanId = parentSpanId[0];
232
+ const traceparent = getHeader('traceparent');
233
+ const parsedContext = parseTraceparent(traceparent);
234
+
235
+ if (parsedContext) {
236
+ inheritedTraceId = parsedContext.traceId;
237
+ inheritedParentSpanId = parsedContext.parentSpanId;
238
+ } else {
239
+ const rawTrace = getHeader('x-senzor-trace-id');
240
+ const rawSpan = getHeader('x-senzor-parent-span-id');
241
+ inheritedTraceId = Array.isArray(rawTrace) ? rawTrace[0] : rawTrace;
242
+ inheritedParentSpanId = Array.isArray(rawSpan) ? rawSpan[0] : rawSpan;
243
+ }
203
244
  }
204
245
 
246
+ const activeTraceId = inheritedTraceId || generateW3CTraceId();
247
+
205
248
  const trace: ActiveTrace = {
206
- id: randomUUID(),
207
- contextType: 'apm', // Ensure we distinguish APM traces from Background Tasks
249
+ id: activeTraceId,
250
+ contextType: 'apm',
208
251
  startTime: performance.now(),
209
- data: {
210
- ...data,
211
- parentTraceId,
212
- parentSpanId
213
- },
252
+ data: { ...data, parentTraceId: inheritedTraceId, parentSpanId: inheritedParentSpanId },
214
253
  spans: []
215
254
  };
216
255
 
@@ -240,7 +279,6 @@ export class SenzorClient {
240
279
  const currentContext = Context.current();
241
280
  const triggerTraceId = currentContext?.contextType === 'apm' ? currentContext.id : undefined;
242
281
 
243
- // Snapshot system resources before execution
244
282
  const startMemory = process.memoryUsage ? process.memoryUsage().heapUsed : 0;
245
283
  const startCpu = process.cpuUsage ? process.cpuUsage() : undefined;
246
284
 
@@ -260,14 +298,13 @@ export class SenzorClient {
260
298
  const task = Context.current();
261
299
  if (!task || task.contextType !== 'task' || !this.transport) return;
262
300
 
263
- // Calculate resource deltas
264
301
  let resourceMetrics;
265
302
  if (process.memoryUsage && task.startMemory !== undefined && process.cpuUsage && task.startCpu) {
266
303
  const endMemory = process.memoryUsage().heapUsed;
267
304
  const cpuDelta = process.cpuUsage(task.startCpu);
268
305
 
269
306
  resourceMetrics = {
270
- memoryDeltaBytes: endMemory - task.startMemory, // Can be negative if GC ran!
307
+ memoryDeltaBytes: endMemory - task.startMemory,
271
308
  cpuUserUs: cpuDelta.user,
272
309
  cpuSystemUs: cpuDelta.system
273
310
  };
@@ -280,7 +317,7 @@ export class SenzorClient {
280
317
  triggerTraceId: task.data.triggerTraceId,
281
318
  queueDelay: task.data.queueDelay,
282
319
  attempts: task.data.attempts,
283
- isDeadLetter: task.data.isDeadLetter, // Extracted from options/metadata if provided
320
+ isDeadLetter: task.data.isDeadLetter,
284
321
  metadata: { ...task.data.metadata, ...extraMetadata },
285
322
  resourceMetrics,
286
323
  status,
@@ -308,7 +345,6 @@ export class SenzorClient {
308
345
  }) as unknown as T;
309
346
  }
310
347
 
311
- // --- MODIFIED: Context-Aware Error Capture ---
312
348
  public captureError(error: unknown, context: any = {}) {
313
349
  if (!this.transport) return;
314
350
 
@@ -337,7 +373,7 @@ export class SenzorClient {
337
373
  }
338
374
 
339
375
  public track(data: any) {
340
- this.transport?.addTrace({ traceId: randomUUID(), ...data, spans: [], timestamp: new Date().toISOString() });
376
+ this.transport?.addTrace({ traceId: generateW3CTraceId(), ...data, spans: [], timestamp: new Date().toISOString() });
341
377
  }
342
378
 
343
379
  public startSpan(name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') {
@@ -345,7 +381,7 @@ export class SenzorClient {
345
381
  if (!trace) return { end: () => { } };
346
382
  const startTime = performance.now() - trace.startTime;
347
383
  const spanStartAbs = performance.now();
348
- const spanId = randomUUID();
384
+ const spanId = randomUUID().replace(/-/g, '').slice(0, 16);
349
385
  return { end: (meta?: any, status?: number) => { Context.addSpan({ spanId, name, type, startTime, duration: performance.now() - spanStartAbs, status, meta }); } };
350
386
  }
351
387
 
@@ -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;
@@ -3,6 +3,7 @@ import https from 'https';
3
3
  import { URL } from 'url';
4
4
  import { Context } from '../core/context';
5
5
  import { randomUUID } from 'crypto';
6
+ import { generateTraceparent } from '../utils/traceContext';
6
7
 
7
8
  const shimmer = (module: any, methodName: string, wrapper: (original: Function) => Function) => {
8
9
  if (!module[methodName]) return;
@@ -10,6 +11,9 @@ const shimmer = (module: any, methodName: string, wrapper: (original: Function)
10
11
  module[methodName] = wrapper(original);
11
12
  };
12
13
 
14
+ // 16-char hex for W3C standard spans
15
+ const generateSpanId = () => randomUUID().replace(/-/g, '').slice(0, 16);
16
+
13
17
  // --- FETCH INSTRUMENTATION ---
14
18
  export const instrumentFetch = (ingestUrl: string, debug = false) => {
15
19
  if (!globalThis.fetch) return;
@@ -38,16 +42,14 @@ export const instrumentFetch = (ingestUrl: string, debug = false) => {
38
42
  const method = (init?.method || 'GET').toUpperCase();
39
43
  const startTime = performance.now() - trace.startTime;
40
44
  const spanStartAbs = performance.now();
41
- const spanId = randomUUID();
45
+ const spanId = generateSpanId();
42
46
 
43
47
  let hostname = 'unknown';
44
48
  try { hostname = new URL(urlStr).hostname; } catch (e) { }
45
49
 
46
- // Inject Headers
47
50
  const newInit = { ...init } as RequestInit;
48
51
  if (!newInit.headers) newInit.headers = {};
49
52
 
50
- // Helper to set header on various types
51
53
  const setHeader = (key: string, value: string) => {
52
54
  if (newInit.headers instanceof Headers) {
53
55
  newInit.headers.set(key, value);
@@ -58,35 +60,21 @@ export const instrumentFetch = (ingestUrl: string, debug = false) => {
58
60
  }
59
61
  };
60
62
 
63
+ // W3C Trace Context Injection
64
+ setHeader('traceparent', generateTraceparent(trace.id, spanId));
65
+
66
+ // Legacy fallback for older Senzor services
61
67
  setHeader('x-senzor-trace-id', trace.id);
62
68
  setHeader('x-senzor-parent-span-id', spanId);
63
69
 
64
70
  try {
65
71
  const response = await originalFetch(input, newInit);
66
-
67
72
  const duration = performance.now() - spanStartAbs;
68
- Context.addSpan({
69
- spanId,
70
- name: `${method} ${hostname}`,
71
- type: 'http',
72
- startTime,
73
- duration,
74
- status: response.status,
75
- meta: { url: urlStr, method, library: 'fetch' }
76
- });
77
-
73
+ Context.addSpan({ spanId, name: `${method} ${hostname}`, type: 'http', startTime, duration, status: response.status, meta: { url: urlStr, method, library: 'fetch' } });
78
74
  return response;
79
75
  } catch (err: any) {
80
76
  const duration = performance.now() - spanStartAbs;
81
- Context.addSpan({
82
- spanId,
83
- name: `${method} ${hostname}`,
84
- type: 'http',
85
- startTime,
86
- duration,
87
- status: 500,
88
- meta: { error: err.message, url: urlStr, library: 'fetch' }
89
- });
77
+ Context.addSpan({ spanId, name: `${method} ${hostname}`, type: 'http', startTime, duration, status: 500, meta: { error: err.message, url: urlStr, library: 'fetch' } });
90
78
  throw err;
91
79
  }
92
80
  };
@@ -103,7 +91,6 @@ export const instrumentHttp = (ingestUrl: string, debug = false) => {
103
91
  let urlStr = '';
104
92
  let optionsIndex = 0;
105
93
 
106
- // Parsing Logic: http.request(url, options, cb) OR http.request(options, cb)
107
94
  if (typeof args[0] === 'string' || args[0] instanceof URL) {
108
95
  urlStr = args[0].toString();
109
96
  optionsIndex = 1;
@@ -111,13 +98,11 @@ export const instrumentHttp = (ingestUrl: string, debug = false) => {
111
98
  optionsIndex = 0;
112
99
  }
113
100
 
114
- // Ensure options object exists at correct index
115
101
  if (!args[optionsIndex] || typeof args[optionsIndex] !== 'object') {
116
102
  args[optionsIndex] = {};
117
103
  }
118
104
  options = args[optionsIndex];
119
105
 
120
- // Construct URL if missing
121
106
  if (!urlStr) {
122
107
  const protocol = options.protocol || (options.port === 443 ? 'https:' : 'http:');
123
108
  const host = options.hostname || options.host || 'localhost';
@@ -125,7 +110,6 @@ export const instrumentHttp = (ingestUrl: string, debug = false) => {
125
110
  urlStr = `${protocol}//${host}${path}`;
126
111
  }
127
112
 
128
- // Guard
129
113
  if (ingestHost && (urlStr.includes(ingestHost) || (options.hostname && options.hostname.includes(ingestHost)))) {
130
114
  return original.apply(this, args);
131
115
  }
@@ -136,33 +120,27 @@ export const instrumentHttp = (ingestUrl: string, debug = false) => {
136
120
  const method = (options.method || 'GET').toUpperCase();
137
121
  const startTime = performance.now() - trace.startTime;
138
122
  const spanStartAbs = performance.now();
139
- const spanId = randomUUID();
123
+ const spanId = generateSpanId();
140
124
 
141
125
  let hostname = 'unknown';
142
126
  try { hostname = new URL(urlStr).hostname; } catch (e) { hostname = options.hostname || 'unknown'; }
143
127
 
144
- // Inject Headers (Mutate the options object reference directly)
145
128
  if (!options.headers) options.headers = {};
129
+
130
+ // W3C Trace Context Injection
131
+ options.headers['traceparent'] = generateTraceparent(trace.id, spanId);
132
+
133
+ // Legacy fallback
146
134
  options.headers['x-senzor-trace-id'] = trace.id;
147
135
  options.headers['x-senzor-parent-span-id'] = spanId;
148
136
 
149
- // Debug
150
- if (debug) console.log(`[Senzor] Injecting headers to ${urlStr}`);
137
+ if (debug) console.log(`[Senzor] Injecting W3C traceparent headers to ${urlStr}`);
151
138
 
152
- // Call Original
153
139
  const req = original.apply(this, args);
154
140
 
155
141
  const captureSpan = (res: any, error?: Error) => {
156
142
  const duration = performance.now() - spanStartAbs;
157
- Context.addSpan({
158
- spanId,
159
- name: `${method} ${hostname}`,
160
- type: 'http',
161
- startTime,
162
- duration,
163
- status: error ? 500 : res?.statusCode || 0,
164
- meta: { url: urlStr, method, library: 'http' }
165
- });
143
+ Context.addSpan({ spanId, name: `${method} ${hostname}`, type: 'http', startTime, duration, status: error ? 500 : res?.statusCode || 0, meta: { url: urlStr, method, library: 'http' } });
166
144
  };
167
145
 
168
146
  req.on('response', (res: any) => {
@@ -0,0 +1,44 @@
1
+ /**
2
+ * W3C Trace Context Implementation
3
+ * Standard: https://www.w3.org/TR/trace-context/
4
+ * Format: 00-{traceId}-{spanId}-{traceFlags}
5
+ */
6
+
7
+ export interface TraceContext {
8
+ traceId: string;
9
+ parentSpanId: string;
10
+ sampled: boolean;
11
+ }
12
+
13
+ const TRACEPARENT_REGEX = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
14
+
15
+ export const parseTraceparent = (header?: string | string[]): TraceContext | null => {
16
+ if (!header) return null;
17
+
18
+ const traceparent = Array.isArray(header) ? header[0] : header;
19
+ if (typeof traceparent !== 'string') return null;
20
+
21
+ const match = traceparent.trim().toLowerCase().match(TRACEPARENT_REGEX);
22
+ if (!match) return null;
23
+
24
+ const traceId = match[1];
25
+ const parentSpanId = match[2];
26
+ const flags = match[3];
27
+
28
+ // Invalid IDs according to W3C specification
29
+ if (traceId === '00000000000000000000000000000000') return null;
30
+ if (parentSpanId === '0000000000000000') return null;
31
+
32
+ // The least significant bit of flags indicates if the trace is sampled
33
+ const sampled = (parseInt(flags, 16) & 0x01) === 0x01;
34
+
35
+ return { traceId, parentSpanId, sampled };
36
+ };
37
+
38
+ /**
39
+ * Generates a valid W3C traceparent header string for OUTGOING requests.
40
+ */
41
+ export const generateTraceparent = (traceId: string, spanId: string, sampled: boolean = true): string => {
42
+ const flags = sampled ? '01' : '00';
43
+ return `00-${traceId}-${spanId}-${flags}`;
44
+ };