@senzops/apm-node 1.1.18 → 1.2.1
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 +8 -0
- package/README.md +398 -48
- package/dist/index.d.mts +14 -0
- package/dist/index.d.ts +14 -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 +167 -107
- 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 +43 -24
- package/src/index.ts +5 -4
- package/src/instrumentation/express.ts +338 -0
- package/src/instrumentation/fastify.ts +296 -0
- package/src/instrumentation/framework.ts +301 -0
- package/src/instrumentation/hook.ts +49 -31
- package/src/instrumentation/http.ts +530 -162
- package/src/instrumentation/koa.ts +173 -0
- 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 +58 -0
- package/src/utils/ids.ts +7 -0
- package/src/utils/internal.ts +1 -0
- package/src/wrappers/fastify.ts +10 -7
- package/src/wrappers/h3.ts +40 -16
- package/src/wrappers/next.ts +68 -21
- package/tsup.config.ts +21 -11
- package/wiki.md +852 -120
package/src/core/client.ts
CHANGED
|
@@ -2,15 +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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
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';
|
|
14
22
|
|
|
15
23
|
// Memory-safe JSON stringifier to handle cyclical objects
|
|
16
24
|
// (like Express 'req' objects) passed into console.log
|
|
@@ -25,39 +33,69 @@ const safeStringify = (obj: any): string => {
|
|
|
25
33
|
});
|
|
26
34
|
};
|
|
27
35
|
|
|
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
|
-
|
|
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
|
+
}
|
|
38
59
|
this.options = options;
|
|
39
60
|
const endpoint = options.endpoint || 'https://api.senzor.dev/api/ingest/apm';
|
|
40
61
|
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
|
-
|
|
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
|
+
}
|
|
61
99
|
|
|
62
100
|
// --- Enterprise Auto-Log Interception ---
|
|
63
101
|
private setupLogInterception() {
|
|
@@ -94,13 +132,13 @@ export class SenzorClient {
|
|
|
94
132
|
attributes.errorStack = arg.stack;
|
|
95
133
|
attributes.errorName = arg.name;
|
|
96
134
|
} 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
|
-
}
|
|
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
|
+
}
|
|
104
142
|
} else {
|
|
105
143
|
message += (message ? ' ' : '') + String(arg);
|
|
106
144
|
}
|
|
@@ -216,11 +254,20 @@ export class SenzorClient {
|
|
|
216
254
|
process.on('SIGINT', () => safeCapture(new Error('Process received SIGINT'), { type: 'processSignal', signal: 'SIGINT' }));
|
|
217
255
|
}
|
|
218
256
|
|
|
219
|
-
public startTrace<T>(data: Partial<ActiveTrace['data']> & { headers?: any }, next: () => T): T {
|
|
220
|
-
if (!this.transport) return next();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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;
|
|
224
271
|
|
|
225
272
|
if (data.headers) {
|
|
226
273
|
const getHeader = (key: string) => {
|
|
@@ -243,32 +290,44 @@ export class SenzorClient {
|
|
|
243
290
|
}
|
|
244
291
|
}
|
|
245
292
|
|
|
246
|
-
const activeTraceId = inheritedTraceId ||
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
+
};
|
|
255
307
|
|
|
256
308
|
return Context.run(trace, next);
|
|
257
309
|
}
|
|
258
310
|
|
|
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
|
-
|
|
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
|
+
};
|
|
272
331
|
this.transport.addTrace(payload);
|
|
273
332
|
}
|
|
274
333
|
|
|
@@ -282,17 +341,21 @@ export class SenzorClient {
|
|
|
282
341
|
const startMemory = process.memoryUsage ? process.memoryUsage().heapUsed : 0;
|
|
283
342
|
const startCpu = process.cpuUsage ? process.cpuUsage() : undefined;
|
|
284
343
|
|
|
285
|
-
const task: ActiveTrace = {
|
|
286
|
-
id: randomUUID(),
|
|
287
|
-
contextType: 'task',
|
|
288
|
-
startTime: performance.now(),
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
359
|
|
|
297
360
|
public endTask(status: 'success' | 'failed', extraMetadata: any = {}) {
|
|
298
361
|
const task = Context.current();
|
|
@@ -318,7 +381,7 @@ export class SenzorClient {
|
|
|
318
381
|
queueDelay: task.data.queueDelay,
|
|
319
382
|
attempts: task.data.attempts,
|
|
320
383
|
isDeadLetter: task.data.isDeadLetter,
|
|
321
|
-
metadata: { ...task.data.metadata, ...extraMetadata },
|
|
384
|
+
metadata: { ...task.data.metadata, ...extraMetadata, droppedSpans: task.droppedSpans },
|
|
322
385
|
resourceMetrics,
|
|
323
386
|
status,
|
|
324
387
|
duration: performance.now() - task.startTime,
|
|
@@ -357,35 +420,32 @@ export class SenzorClient {
|
|
|
357
420
|
|
|
358
421
|
const currentTrace = Context.current();
|
|
359
422
|
|
|
360
|
-
const errPayload = {
|
|
361
|
-
errorClass: parsedError.name || 'Error',
|
|
362
|
-
message: parsedError.message,
|
|
363
|
-
stackTrace: parsedError.stack,
|
|
364
|
-
context,
|
|
365
|
-
timestamp: new Date().toISOString()
|
|
366
|
-
};
|
|
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
|
+
};
|
|
367
430
|
|
|
368
431
|
if (currentTrace?.contextType === 'task') {
|
|
369
432
|
this.transport.addError({ ...errPayload, runId: currentTrace.id }, 'task');
|
|
370
433
|
} else {
|
|
371
434
|
this.transport.addError({ ...errPayload, traceId: currentTrace?.id }, 'apm');
|
|
372
435
|
}
|
|
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
|
-
}
|
|
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
|
+
}
|
|
387
447
|
|
|
388
448
|
public async flush() { if (this.transport) await this.transport.flush(); }
|
|
389
449
|
}
|
|
390
450
|
|
|
391
|
-
export const client = new SenzorClient();
|
|
451
|
+
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
|
+
};
|