@senzops/apm-node 1.1.18 → 1.2.0
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 +4 -0
- package/README.md +386 -48
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -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/dist/register.d.mts +2 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +2 -0
- package/dist/register.js.map +1 -0
- package/dist/register.mjs +2 -0
- package/dist/register.mjs.map +1 -0
- package/package.json +15 -4
- package/src/core/client.ts +159 -105
- package/src/core/context.ts +48 -21
- package/src/core/sanitizer.ts +203 -0
- package/src/core/transport.ts +273 -104
- package/src/core/types.ts +38 -24
- package/src/index.ts +5 -4
- package/src/instrumentation/http.ts +530 -162
- package/src/instrumentation/mongo.ts +202 -105
- package/src/instrumentation/mongoose.ts +156 -0
- package/src/instrumentation/mysql.ts +169 -0
- package/src/instrumentation/patch.ts +56 -0
- package/src/instrumentation/pg.ts +131 -41
- package/src/instrumentation/redis.ts +109 -0
- package/src/instrumentation/span.ts +73 -0
- package/src/instrumentation/undici.ts +189 -0
- package/src/register.ts +42 -0
- package/src/utils/ids.ts +7 -0
- package/src/utils/internal.ts +1 -0
- package/tsup.config.ts +21 -11
- package/wiki.md +844 -120
package/src/core/client.ts
CHANGED
|
@@ -4,13 +4,18 @@ 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';
|
|
7
|
-
import { instrumentPg } from '../instrumentation/pg';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
import { instrumentPg } from '../instrumentation/pg';
|
|
8
|
+
import { instrumentUndici } from '../instrumentation/undici';
|
|
9
|
+
import { instrumentRedis } from '../instrumentation/redis';
|
|
10
|
+
import { instrumentMysql } from '../instrumentation/mysql';
|
|
11
|
+
import { instrumentMongoose } from '../instrumentation/mongoose';
|
|
12
|
+
import { instrumentBullMQ } from '../instrumentation/bullmq';
|
|
13
|
+
import { instrumentNodeCron } from '../instrumentation/cron';
|
|
14
|
+
import { SDK_META } from '../utils/sdkMeta';
|
|
15
|
+
import { parseTraceparent } from '../utils/traceContext';
|
|
16
|
+
import { generateSpanId, generateTraceId } from '../utils/ids';
|
|
17
|
+
import { sanitizeAttributes } from './sanitizer';
|
|
18
|
+
import { startCapturedSpan } from '../instrumentation/span';
|
|
14
19
|
|
|
15
20
|
// Memory-safe JSON stringifier to handle cyclical objects
|
|
16
21
|
// (like Express 'req' objects) passed into console.log
|
|
@@ -25,39 +30,66 @@ const safeStringify = (obj: any): string => {
|
|
|
25
30
|
});
|
|
26
31
|
};
|
|
27
32
|
|
|
28
|
-
export class SenzorClient {
|
|
29
|
-
private transport: Transport | null = null;
|
|
30
|
-
private options: SenzorOptions | null = null;
|
|
31
|
-
private isInstrumented = false;
|
|
32
|
-
|
|
33
|
-
public
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
export class SenzorClient {
|
|
34
|
+
private transport: Transport | null = null;
|
|
35
|
+
private options: SenzorOptions | null = null;
|
|
36
|
+
private isInstrumented = false;
|
|
37
|
+
|
|
38
|
+
public preload(options: Partial<SenzorOptions> = {}) {
|
|
39
|
+
const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
|
|
40
|
+
const debug = options.debug || false;
|
|
41
|
+
|
|
42
|
+
this.options = {
|
|
43
|
+
apiKey: '',
|
|
44
|
+
...this.options,
|
|
45
|
+
...options
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.installNativeInstrumentations(endpoint, debug);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public init(options: SenzorOptions) {
|
|
52
|
+
if (!options.apiKey) {
|
|
53
|
+
console.warn('[Senzor] API Key missing. SDK disabled.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
38
56
|
this.options = options;
|
|
39
57
|
const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
|
|
40
58
|
const debug = options.debug || false;
|
|
41
|
-
|
|
42
|
-
this.transport = new Transport({ ...options, endpoint });
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
|
|
60
|
+
this.transport = new Transport({ ...options, endpoint });
|
|
61
|
+
this.installNativeInstrumentations(endpoint, debug);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private isInstrumentationEnabled(name: string): boolean {
|
|
65
|
+
const setting = this.options?.instrumentations;
|
|
66
|
+
if (setting === false) return false;
|
|
67
|
+
if (Array.isArray(setting)) return setting.includes(name);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private installNativeInstrumentations(endpoint: string, debug: boolean) {
|
|
72
|
+
if (!this.isInstrumented) {
|
|
73
|
+
this.setupGlobalErrorHandlers();
|
|
74
|
+
this.setupLogInterception(); // Fire up Auto Log Instrumentation
|
|
75
|
+
|
|
76
|
+
try { if (this.isInstrumentationEnabled('http')) instrumentHttp(this, endpoint, this.options || undefined); } catch (e) { }
|
|
77
|
+
try { if (this.isInstrumentationEnabled('fetch')) instrumentFetch(endpoint, this.options || undefined); } catch (e) { }
|
|
78
|
+
try { if (this.isInstrumentationEnabled('undici')) instrumentUndici(this.options || undefined); } catch (e) { }
|
|
79
|
+
try { if (this.isInstrumentationEnabled('mongo')) instrumentMongo(this.options || undefined); } catch (e) { }
|
|
80
|
+
try { if (this.isInstrumentationEnabled('mongoose')) instrumentMongoose(this.options || undefined); } catch (e) { }
|
|
81
|
+
try { if (this.isInstrumentationEnabled('pg')) instrumentPg(this.options || undefined); } catch (e) { }
|
|
82
|
+
try { if (this.isInstrumentationEnabled('mysql')) instrumentMysql(this.options || undefined); } catch (e) { }
|
|
83
|
+
try { if (this.isInstrumentationEnabled('redis')) instrumentRedis(this.options || undefined); } catch (e) { }
|
|
84
|
+
|
|
85
|
+
// Task Integrations
|
|
86
|
+
try { if (this.isInstrumentationEnabled('bullmq')) instrumentBullMQ(this, debug); } catch (e) { }
|
|
87
|
+
try { if (this.isInstrumentationEnabled('cron')) instrumentNodeCron(this, debug); } catch (e) { }
|
|
88
|
+
|
|
89
|
+
this.isInstrumented = true;
|
|
90
|
+
if (debug) console.log('[Senzor] Auto-instrumentation enabled');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
61
93
|
|
|
62
94
|
// --- Enterprise Auto-Log Interception ---
|
|
63
95
|
private setupLogInterception() {
|
|
@@ -94,13 +126,13 @@ export class SenzorClient {
|
|
|
94
126
|
attributes.errorStack = arg.stack;
|
|
95
127
|
attributes.errorName = arg.name;
|
|
96
128
|
} 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
|
-
}
|
|
129
|
+
try {
|
|
130
|
+
// New Relic Style Destructuring: Merge all object keys into `attributes`
|
|
131
|
+
const parsed = JSON.parse(safeStringify(arg));
|
|
132
|
+
attributes = { ...attributes, ...sanitizeAttributes(parsed, this.options || undefined) };
|
|
133
|
+
} catch (e) {
|
|
134
|
+
attributes.unparseableObject = true;
|
|
135
|
+
}
|
|
104
136
|
} else {
|
|
105
137
|
message += (message ? ' ' : '') + String(arg);
|
|
106
138
|
}
|
|
@@ -216,11 +248,20 @@ export class SenzorClient {
|
|
|
216
248
|
process.on('SIGINT', () => safeCapture(new Error('Process received SIGINT'), { type: 'processSignal', signal: 'SIGINT' }));
|
|
217
249
|
}
|
|
218
250
|
|
|
219
|
-
public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
|
|
220
|
-
if (!this.transport) return next();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
251
|
+
public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
|
|
252
|
+
if (!this.transport) return next();
|
|
253
|
+
|
|
254
|
+
const existingTrace = Context.current();
|
|
255
|
+
if (existingTrace?.contextType === 'apm') {
|
|
256
|
+
existingTrace.data = {
|
|
257
|
+
...existingTrace.data,
|
|
258
|
+
...data
|
|
259
|
+
};
|
|
260
|
+
return next();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let inheritedTraceId: string | undefined = undefined;
|
|
264
|
+
let inheritedParentSpanId: string | undefined = undefined;
|
|
224
265
|
|
|
225
266
|
if (data.headers) {
|
|
226
267
|
const getHeader = (key: string) => {
|
|
@@ -243,32 +284,44 @@ export class SenzorClient {
|
|
|
243
284
|
}
|
|
244
285
|
}
|
|
245
286
|
|
|
246
|
-
const activeTraceId = inheritedTraceId ||
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
287
|
+
const activeTraceId = inheritedTraceId || generateTraceId();
|
|
288
|
+
const rootSpanId = generateSpanId();
|
|
289
|
+
|
|
290
|
+
const trace: ActiveTrace = {
|
|
291
|
+
id: activeTraceId,
|
|
292
|
+
contextType: 'apm',
|
|
293
|
+
startTime: performance.now(),
|
|
294
|
+
rootSpanId,
|
|
295
|
+
activeSpanId: rootSpanId,
|
|
296
|
+
data: { ...data, parentTraceId: inheritedTraceId, parentSpanId: inheritedParentSpanId, rootSpanId },
|
|
297
|
+
spans: [],
|
|
298
|
+
maxSpans: this.options?.maxSpansPerTrace ?? 500,
|
|
299
|
+
droppedSpans: 0
|
|
300
|
+
};
|
|
255
301
|
|
|
256
302
|
return Context.run(trace, next);
|
|
257
303
|
}
|
|
258
304
|
|
|
259
|
-
public endTrace(status: number, extraData: any = {}) {
|
|
260
|
-
const trace = Context.current();
|
|
261
|
-
if (!trace || trace.contextType !== 'apm' || !this.transport) return;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
305
|
+
public endTrace(status: number, extraData: any = {}) {
|
|
306
|
+
const trace = Context.current();
|
|
307
|
+
if (!trace || trace.contextType !== 'apm' || !this.transport) return;
|
|
308
|
+
if (trace.ended) return;
|
|
309
|
+
trace.ended = true;
|
|
310
|
+
const duration = performance.now() - trace.startTime;
|
|
311
|
+
|
|
312
|
+
const payload = {
|
|
313
|
+
traceId: trace.id,
|
|
314
|
+
parentTraceId: trace.data.parentTraceId,
|
|
315
|
+
parentSpanId: trace.data.parentSpanId,
|
|
316
|
+
rootSpanId: trace.rootSpanId,
|
|
317
|
+
...trace.data,
|
|
318
|
+
...extraData,
|
|
319
|
+
status,
|
|
320
|
+
duration,
|
|
321
|
+
spans: trace.spans,
|
|
322
|
+
droppedSpans: trace.droppedSpans,
|
|
323
|
+
timestamp: new Date().toISOString()
|
|
324
|
+
};
|
|
272
325
|
this.transport.addTrace(payload);
|
|
273
326
|
}
|
|
274
327
|
|
|
@@ -282,17 +335,21 @@ export class SenzorClient {
|
|
|
282
335
|
const startMemory = process.memoryUsage ? process.memoryUsage().heapUsed : 0;
|
|
283
336
|
const startCpu = process.cpuUsage ? process.cpuUsage() : undefined;
|
|
284
337
|
|
|
285
|
-
const task: ActiveTrace = {
|
|
286
|
-
id: randomUUID(),
|
|
287
|
-
contextType: 'task',
|
|
288
|
-
startTime: performance.now(),
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
338
|
+
const task: ActiveTrace = {
|
|
339
|
+
id: randomUUID(),
|
|
340
|
+
contextType: 'task',
|
|
341
|
+
startTime: performance.now(),
|
|
342
|
+
rootSpanId: generateSpanId(),
|
|
343
|
+
startMemory,
|
|
344
|
+
startCpu,
|
|
345
|
+
data: { taskName: name, taskType: type, triggerTraceId, ...options },
|
|
346
|
+
spans: [],
|
|
347
|
+
maxSpans: this.options?.maxSpansPerTrace ?? 500,
|
|
348
|
+
droppedSpans: 0
|
|
349
|
+
};
|
|
350
|
+
task.activeSpanId = task.rootSpanId;
|
|
351
|
+
return Context.run(task, next);
|
|
352
|
+
}
|
|
296
353
|
|
|
297
354
|
public endTask(status: 'success' | 'failed', extraMetadata: any = {}) {
|
|
298
355
|
const task = Context.current();
|
|
@@ -318,7 +375,7 @@ export class SenzorClient {
|
|
|
318
375
|
queueDelay: task.data.queueDelay,
|
|
319
376
|
attempts: task.data.attempts,
|
|
320
377
|
isDeadLetter: task.data.isDeadLetter,
|
|
321
|
-
metadata: { ...task.data.metadata, ...extraMetadata },
|
|
378
|
+
metadata: { ...task.data.metadata, ...extraMetadata, droppedSpans: task.droppedSpans },
|
|
322
379
|
resourceMetrics,
|
|
323
380
|
status,
|
|
324
381
|
duration: performance.now() - task.startTime,
|
|
@@ -357,35 +414,32 @@ export class SenzorClient {
|
|
|
357
414
|
|
|
358
415
|
const currentTrace = Context.current();
|
|
359
416
|
|
|
360
|
-
const errPayload = {
|
|
361
|
-
errorClass: parsedError.name || 'Error',
|
|
362
|
-
message: parsedError.message,
|
|
363
|
-
stackTrace: parsedError.stack,
|
|
364
|
-
context,
|
|
365
|
-
timestamp: new Date().toISOString()
|
|
366
|
-
};
|
|
417
|
+
const errPayload = {
|
|
418
|
+
errorClass: parsedError.name || 'Error',
|
|
419
|
+
message: parsedError.message,
|
|
420
|
+
stackTrace: parsedError.stack,
|
|
421
|
+
context: sanitizeAttributes(context, this.options || undefined),
|
|
422
|
+
timestamp: new Date().toISOString()
|
|
423
|
+
};
|
|
367
424
|
|
|
368
425
|
if (currentTrace?.contextType === 'task') {
|
|
369
426
|
this.transport.addError({ ...errPayload, runId: currentTrace.id }, 'task');
|
|
370
427
|
} else {
|
|
371
428
|
this.transport.addError({ ...errPayload, traceId: currentTrace?.id }, 'apm');
|
|
372
429
|
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
public track(data: any) {
|
|
376
|
-
this.transport?.addTrace({ traceId:
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
public startSpan(name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') {
|
|
380
|
-
const
|
|
381
|
-
if (!
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const spanId = randomUUID().replace(/-/g, '').slice(0, 16);
|
|
385
|
-
return { end: (meta?: any, status?: number) => { Context.addSpan({ spanId, name, type, startTime, duration: performance.now() - spanStartAbs, status, meta }); } };
|
|
386
|
-
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public track(data: any) {
|
|
433
|
+
this.transport?.addTrace({ traceId: generateTraceId(), ...data, spans: [], timestamp: new Date().toISOString() });
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
public startSpan(name: string, type: 'db' | 'http' | 'function' | 'custom' = 'custom') {
|
|
437
|
+
const span = startCapturedSpan(name, type, {}, this.options || undefined);
|
|
438
|
+
if (!span) return { end: () => { } };
|
|
439
|
+
return { end: (meta?: any, status?: number) => span.end(status, meta) };
|
|
440
|
+
}
|
|
387
441
|
|
|
388
442
|
public async flush() { if (this.transport) await this.transport.flush(); }
|
|
389
443
|
}
|
|
390
444
|
|
|
391
|
-
export const client = new SenzorClient();
|
|
445
|
+
export const client = new SenzorClient();
|
package/src/core/context.ts
CHANGED
|
@@ -1,21 +1,48 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
-
import { ActiveTrace } from './types';
|
|
3
|
-
|
|
4
|
-
export const storage = new AsyncLocalStorage<ActiveTrace>();
|
|
5
|
-
|
|
6
|
-
export const Context = {
|
|
7
|
-
run: <T>(trace: ActiveTrace, fn: () => T): T => {
|
|
8
|
-
return storage.run(trace, fn);
|
|
9
|
-
},
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import { ActiveTrace, Span } from './types';
|
|
3
|
+
|
|
4
|
+
export const storage = new AsyncLocalStorage<ActiveTrace>();
|
|
5
|
+
|
|
6
|
+
export const Context = {
|
|
7
|
+
run: <T>(trace: ActiveTrace, fn: () => T): T => {
|
|
8
|
+
return storage.run(trace, fn);
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
withActiveSpan: <T>(spanId: string, fn: () => T): T => {
|
|
12
|
+
const store = storage.getStore();
|
|
13
|
+
if (!store) return fn();
|
|
14
|
+
|
|
15
|
+
return storage.run(
|
|
16
|
+
{
|
|
17
|
+
...store,
|
|
18
|
+
activeSpanId: spanId,
|
|
19
|
+
data: store.data,
|
|
20
|
+
spans: store.spans
|
|
21
|
+
},
|
|
22
|
+
fn
|
|
23
|
+
);
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
current: (): ActiveTrace | undefined => {
|
|
27
|
+
return storage.getStore();
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
addSpan: (span: Span) => {
|
|
31
|
+
const store = storage.getStore();
|
|
32
|
+
if (store) {
|
|
33
|
+
Context.addSpanToTrace(store, span);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
addSpanToTrace: (trace: ActiveTrace, span: Span) => {
|
|
38
|
+
if (trace.ended) return;
|
|
39
|
+
|
|
40
|
+
const maxSpans = trace.maxSpans ?? 500;
|
|
41
|
+
if (trace.spans.length >= maxSpans) {
|
|
42
|
+
trace.droppedSpans = (trace.droppedSpans ?? 0) + 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
trace.spans.push(span);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { SenzorOptions } from './types';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_ATTRIBUTES = 64;
|
|
4
|
+
const DEFAULT_MAX_ATTRIBUTE_LENGTH = 2048;
|
|
5
|
+
const MAX_DEPTH = 4;
|
|
6
|
+
const MAX_ARRAY_ITEMS = 20;
|
|
7
|
+
|
|
8
|
+
const SENSITIVE_KEY_PATTERN =
|
|
9
|
+
/(^|[-_.])(authorization|cookie|set-cookie|password|passwd|pwd|secret|token|api[-_.]?key|x-api-key|access[-_.]?token|refresh[-_.]?token|client[-_.]?secret|private[-_.]?key)([-_.]|$)/i;
|
|
10
|
+
|
|
11
|
+
export interface SanitizerOptions {
|
|
12
|
+
maxAttributes?: number;
|
|
13
|
+
maxAttributeLength?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const getLimits = (options?: SanitizerOptions | SenzorOptions) => ({
|
|
17
|
+
maxAttributes: options?.maxAttributes ?? DEFAULT_MAX_ATTRIBUTES,
|
|
18
|
+
maxAttributeLength:
|
|
19
|
+
options?.maxAttributeLength ?? DEFAULT_MAX_ATTRIBUTE_LENGTH
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const truncate = (
|
|
23
|
+
value: string,
|
|
24
|
+
maxLength = DEFAULT_MAX_ATTRIBUTE_LENGTH
|
|
25
|
+
): string => {
|
|
26
|
+
if (value.length <= maxLength) return value;
|
|
27
|
+
return `${value.slice(0, Math.max(0, maxLength - 15))}...[truncated]`;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const isSensitiveKey = (key: string): boolean =>
|
|
31
|
+
SENSITIVE_KEY_PATTERN.test(key);
|
|
32
|
+
|
|
33
|
+
const sanitizePrimitive = (
|
|
34
|
+
value: unknown,
|
|
35
|
+
maxLength: number
|
|
36
|
+
): string | number | boolean | null | undefined => {
|
|
37
|
+
if (value === null || value === undefined) return value;
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
typeof value === 'number' ||
|
|
41
|
+
typeof value === 'boolean'
|
|
42
|
+
) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof value === 'bigint') {
|
|
47
|
+
return value.toString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof value === 'string') {
|
|
51
|
+
return truncate(value, maxLength);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return undefined;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const sanitizeValue = (
|
|
58
|
+
key: string,
|
|
59
|
+
value: unknown,
|
|
60
|
+
options: Required<SanitizerOptions>,
|
|
61
|
+
depth: number
|
|
62
|
+
): unknown => {
|
|
63
|
+
if (isSensitiveKey(key)) return '[REDACTED]';
|
|
64
|
+
|
|
65
|
+
const primitive =
|
|
66
|
+
sanitizePrimitive(value, options.maxAttributeLength);
|
|
67
|
+
|
|
68
|
+
if (primitive !== undefined || value === undefined) {
|
|
69
|
+
return primitive;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (value instanceof Error) {
|
|
73
|
+
return {
|
|
74
|
+
name: truncate(value.name, options.maxAttributeLength),
|
|
75
|
+
message: truncate(value.message, options.maxAttributeLength),
|
|
76
|
+
stack: value.stack
|
|
77
|
+
? truncate(value.stack, options.maxAttributeLength)
|
|
78
|
+
: undefined
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (depth >= MAX_DEPTH) {
|
|
83
|
+
return '[MaxDepth]';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
return value
|
|
88
|
+
.slice(0, MAX_ARRAY_ITEMS)
|
|
89
|
+
.map((item) =>
|
|
90
|
+
sanitizeValue(key, item, options, depth + 1)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof value === 'object') {
|
|
95
|
+
const output: Record<string, unknown> = {};
|
|
96
|
+
let count = 0;
|
|
97
|
+
|
|
98
|
+
for (const [childKey, childValue] of Object.entries(
|
|
99
|
+
value as Record<string, unknown>
|
|
100
|
+
)) {
|
|
101
|
+
if (count >= options.maxAttributes) {
|
|
102
|
+
output.__truncated = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
output[childKey] = sanitizeValue(
|
|
107
|
+
childKey,
|
|
108
|
+
childValue,
|
|
109
|
+
options,
|
|
110
|
+
depth + 1
|
|
111
|
+
);
|
|
112
|
+
count++;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return truncate(String(value), options.maxAttributeLength);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const sanitizeAttributes = (
|
|
122
|
+
attributes: Record<string, unknown> = {},
|
|
123
|
+
options?: SanitizerOptions | SenzorOptions
|
|
124
|
+
): Record<string, unknown> => {
|
|
125
|
+
const limits = getLimits(options);
|
|
126
|
+
const normalizedOptions: Required<SanitizerOptions> = {
|
|
127
|
+
maxAttributes: limits.maxAttributes,
|
|
128
|
+
maxAttributeLength: limits.maxAttributeLength
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const output: Record<string, unknown> = {};
|
|
132
|
+
let count = 0;
|
|
133
|
+
|
|
134
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
135
|
+
if (count >= normalizedOptions.maxAttributes) {
|
|
136
|
+
output.__truncated = true;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
output[key] = sanitizeValue(
|
|
141
|
+
key,
|
|
142
|
+
value,
|
|
143
|
+
normalizedOptions,
|
|
144
|
+
0
|
|
145
|
+
);
|
|
146
|
+
count++;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return output;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const sanitizeHeaders = (
|
|
153
|
+
headers: unknown,
|
|
154
|
+
options?: SanitizerOptions | SenzorOptions
|
|
155
|
+
): Record<string, unknown> => {
|
|
156
|
+
if (!headers || typeof headers !== 'object') return {};
|
|
157
|
+
|
|
158
|
+
const plainHeaders: Record<string, unknown> = {};
|
|
159
|
+
|
|
160
|
+
if (typeof (headers as any).forEach === 'function') {
|
|
161
|
+
(headers as any).forEach((value: unknown, key: string) => {
|
|
162
|
+
plainHeaders[key.toLowerCase()] = value;
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
for (const [key, value] of Object.entries(
|
|
166
|
+
headers as Record<string, unknown>
|
|
167
|
+
)) {
|
|
168
|
+
plainHeaders[key.toLowerCase()] = Array.isArray(value)
|
|
169
|
+
? value.join(', ')
|
|
170
|
+
: value;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return sanitizeAttributes(plainHeaders, options);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const normalizeSql = (
|
|
178
|
+
sql: unknown,
|
|
179
|
+
options?: SenzorOptions
|
|
180
|
+
): string | undefined => {
|
|
181
|
+
if (typeof sql !== 'string') return undefined;
|
|
182
|
+
|
|
183
|
+
const collapsed = sql.replace(/\s+/g, ' ').trim();
|
|
184
|
+
if (!collapsed) return undefined;
|
|
185
|
+
|
|
186
|
+
const withoutLiterals = collapsed
|
|
187
|
+
.replace(/'(?:''|[^'])*'/g, '?')
|
|
188
|
+
.replace(/"(?:\\"|[^"])*"/g, '?')
|
|
189
|
+
.replace(/\b\d+(\.\d+)?\b/g, '?');
|
|
190
|
+
|
|
191
|
+
return truncate(
|
|
192
|
+
options?.captureDbStatement === false
|
|
193
|
+
? withoutLiterals.split(' ').slice(0, 6).join(' ')
|
|
194
|
+
: withoutLiterals,
|
|
195
|
+
options?.maxAttributeLength ?? DEFAULT_MAX_ATTRIBUTE_LENGTH
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const getSqlOperation = (sql: unknown): string | undefined => {
|
|
200
|
+
if (typeof sql !== 'string') return undefined;
|
|
201
|
+
const match = sql.trim().match(/^([a-z]+)/i);
|
|
202
|
+
return match?.[1]?.toUpperCase();
|
|
203
|
+
};
|