@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.
- package/CHANGELOG.md +37 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/client.ts +132 -96
- package/src/core/transport.ts +30 -9
- package/src/core/types.ts +12 -0
- package/src/instrumentation/http.ts +19 -41
- package/src/utils/traceContext.ts +44 -0
package/src/core/client.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
process.on('
|
|
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
|
|
189
|
-
let
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
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:
|
|
207
|
-
contextType: 'apm',
|
|
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,
|
|
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,
|
|
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:
|
|
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
|
|
package/src/core/transport.ts
CHANGED
|
@@ -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 = {
|
|
54
|
-
|
|
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
|
-
|
|
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}
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
+
};
|