@senzops/apm-node 1.2.1 → 1.2.2
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/.claude/worktrees/infallible-chatelet-f3fb36/.claude/settings.local.json +9 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/CHANGELOG.md +49 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/README.md +398 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/package-lock.json +1494 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/package.json +42 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/client.ts +451 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/context.ts +48 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/normalizer.ts +44 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/sanitizer.ts +203 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/transport.ts +273 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/core/types.ts +106 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/index.ts +36 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/bullmq.ts +195 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/cron.ts +204 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/express.ts +338 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/fastify.ts +296 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/framework.ts +301 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/hook.ts +134 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/http.ts +530 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/koa.ts +173 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mongo.ts +202 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mongoose.ts +156 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/mysql.ts +169 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/patch.ts +56 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/pg.ts +131 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/redis.ts +109 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/span.ts +73 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/instrumentation/undici.ts +189 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/middleware/express.ts +48 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/register.ts +58 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/getClientIp.ts +175 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/ids.ts +7 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/internal.ts +1 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/sdkMeta.ts +6 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/utils/traceContext.ts +44 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/fastify.ts +35 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/h3.ts +59 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/src/wrappers/next.ts +131 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/tsconfig.json +15 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/tsup.config.ts +21 -0
- package/.claude/worktrees/infallible-chatelet-f3fb36/wiki.md +852 -0
- package/CHANGELOG.md +4 -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.js +1 -1
- package/dist/register.js.map +1 -1
- package/dist/register.mjs +1 -1
- package/dist/register.mjs.map +1 -1
- package/package.json +1 -1
- package/src/instrumentation/hook.ts +85 -216
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const PATCHES = Symbol.for('senzor.patch.keys');
|
|
2
|
+
const ORIGINAL = Symbol.for('senzor.patch.original');
|
|
3
|
+
|
|
4
|
+
type WrappedFunction = Function & {
|
|
5
|
+
[PATCHES]?: Set<string>;
|
|
6
|
+
[ORIGINAL]?: Function;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const patchMethod = (
|
|
10
|
+
target: any,
|
|
11
|
+
methodName: string,
|
|
12
|
+
patchKey: string,
|
|
13
|
+
wrapper: (original: Function) => Function
|
|
14
|
+
): boolean => {
|
|
15
|
+
if (!target) return false;
|
|
16
|
+
|
|
17
|
+
const current = target[methodName] as WrappedFunction | undefined;
|
|
18
|
+
if (typeof current !== 'function') return false;
|
|
19
|
+
|
|
20
|
+
const existingPatches = current[PATCHES];
|
|
21
|
+
if (existingPatches?.has(patchKey)) return false;
|
|
22
|
+
|
|
23
|
+
const original = current[ORIGINAL] || current;
|
|
24
|
+
const wrapped = wrapper(current) as WrappedFunction;
|
|
25
|
+
const patches = new Set(existingPatches || []);
|
|
26
|
+
patches.add(patchKey);
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
Object.defineProperty(wrapped, PATCHES, {
|
|
30
|
+
value: patches,
|
|
31
|
+
enumerable: false
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(wrapped, ORIGINAL, {
|
|
34
|
+
value: original,
|
|
35
|
+
enumerable: false
|
|
36
|
+
});
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
target[methodName] = wrapped;
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const isPatched = (
|
|
50
|
+
target: any,
|
|
51
|
+
methodName: string,
|
|
52
|
+
patchKey: string
|
|
53
|
+
): boolean => {
|
|
54
|
+
const current = target?.[methodName] as WrappedFunction | undefined;
|
|
55
|
+
return Boolean(current?.[PATCHES]?.has(patchKey));
|
|
56
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { getSqlOperation, normalizeSql } from '../core/sanitizer';
|
|
2
|
+
import { SenzorOptions } from '../core/types';
|
|
3
|
+
import { hookRequire } from './hook';
|
|
4
|
+
import { patchMethod } from './patch';
|
|
5
|
+
import { runWithCapturedSpan, startCapturedSpan } from './span';
|
|
6
|
+
|
|
7
|
+
const extractSql = (args: any[]): string | undefined => {
|
|
8
|
+
const first = args[0];
|
|
9
|
+
if (typeof first === 'string') return first;
|
|
10
|
+
if (first && typeof first.text === 'string') return first.text;
|
|
11
|
+
return undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const wrapQueryMethod = (
|
|
15
|
+
proto: any,
|
|
16
|
+
label: string,
|
|
17
|
+
options?: SenzorOptions
|
|
18
|
+
) => {
|
|
19
|
+
patchMethod(
|
|
20
|
+
proto,
|
|
21
|
+
'query',
|
|
22
|
+
`senzor.pg.${label}.query`,
|
|
23
|
+
(original) =>
|
|
24
|
+
function patchedPgQuery(this: any, ...args: any[]) {
|
|
25
|
+
const sql = extractSql(args);
|
|
26
|
+
const operation = getSqlOperation(sql) || 'QUERY';
|
|
27
|
+
const span = startCapturedSpan(
|
|
28
|
+
`Postgres ${operation}`,
|
|
29
|
+
'db',
|
|
30
|
+
{
|
|
31
|
+
query: normalizeSql(sql, options),
|
|
32
|
+
operation,
|
|
33
|
+
'db.system.name': 'postgresql',
|
|
34
|
+
'db.operation.name': operation,
|
|
35
|
+
'db.query.text': normalizeSql(sql, options),
|
|
36
|
+
library: 'pg'
|
|
37
|
+
},
|
|
38
|
+
options
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!span) return original.apply(this, args);
|
|
42
|
+
|
|
43
|
+
const callbackIndex = args.findIndex(
|
|
44
|
+
(arg) => typeof arg === 'function'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (callbackIndex >= 0) {
|
|
48
|
+
const originalCallback = args[callbackIndex];
|
|
49
|
+
args[callbackIndex] = function wrappedPgCallback(
|
|
50
|
+
this: unknown,
|
|
51
|
+
err: Error | null,
|
|
52
|
+
result: any
|
|
53
|
+
) {
|
|
54
|
+
span.end(err ? 500 : 0, {
|
|
55
|
+
error: err?.message,
|
|
56
|
+
'error.type': err?.name,
|
|
57
|
+
rowCount: result?.rowCount,
|
|
58
|
+
'db.response.row_count': result?.rowCount
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return originalCallback.apply(this, arguments as any);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return runWithCapturedSpan(span, () => {
|
|
66
|
+
try {
|
|
67
|
+
const result = original.apply(this, args);
|
|
68
|
+
|
|
69
|
+
if (result && typeof result.then === 'function') {
|
|
70
|
+
return result.then(
|
|
71
|
+
(value: any) => {
|
|
72
|
+
span.end(0, {
|
|
73
|
+
rowCount: value?.rowCount,
|
|
74
|
+
'db.response.row_count': value?.rowCount
|
|
75
|
+
});
|
|
76
|
+
return value;
|
|
77
|
+
},
|
|
78
|
+
(error: any) => {
|
|
79
|
+
span.end(500, {
|
|
80
|
+
error: error?.message,
|
|
81
|
+
'error.type': error?.name || 'Error'
|
|
82
|
+
});
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (
|
|
89
|
+
callbackIndex < 0 &&
|
|
90
|
+
result &&
|
|
91
|
+
typeof result.once === 'function'
|
|
92
|
+
) {
|
|
93
|
+
result.once('end', () => span.end(0));
|
|
94
|
+
result.once('error', (error: Error) =>
|
|
95
|
+
span.end(500, {
|
|
96
|
+
error: error.message,
|
|
97
|
+
'error.type': error.name
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
} else if (callbackIndex < 0) {
|
|
101
|
+
span.end(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
} catch (error: any) {
|
|
106
|
+
span.end(500, {
|
|
107
|
+
error: error?.message,
|
|
108
|
+
'error.type': error?.name || 'Error'
|
|
109
|
+
});
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const patchPg = (pg: any, options?: SenzorOptions) => {
|
|
118
|
+
if (!pg) return;
|
|
119
|
+
|
|
120
|
+
wrapQueryMethod(pg.Client?.prototype, 'client', options);
|
|
121
|
+
wrapQueryMethod(pg.Pool?.prototype, 'pool', options);
|
|
122
|
+
|
|
123
|
+
if (pg.default) {
|
|
124
|
+
wrapQueryMethod(pg.default.Client?.prototype, 'default.client', options);
|
|
125
|
+
wrapQueryMethod(pg.default.Pool?.prototype, 'default.pool', options);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const instrumentPg = (options?: SenzorOptions) => {
|
|
130
|
+
hookRequire('pg', (exports: any) => patchPg(exports, options));
|
|
131
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { SenzorOptions } from '../core/types';
|
|
2
|
+
import { hookRequire } from './hook';
|
|
3
|
+
import { patchMethod } from './patch';
|
|
4
|
+
import { runWithCapturedSpan, startCapturedSpan } from './span';
|
|
5
|
+
|
|
6
|
+
const getCommandName = (command: any): string => {
|
|
7
|
+
if (typeof command === 'string') return command.toUpperCase();
|
|
8
|
+
if (Array.isArray(command)) return String(command[0] || 'COMMAND').toUpperCase();
|
|
9
|
+
if (command?.name) return String(command.name).toUpperCase();
|
|
10
|
+
if (Array.isArray(command?.args)) return String(command.args[0] || 'COMMAND').toUpperCase();
|
|
11
|
+
return 'COMMAND';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const patchSendCommand = (
|
|
15
|
+
target: any,
|
|
16
|
+
label: string,
|
|
17
|
+
options?: SenzorOptions
|
|
18
|
+
) => {
|
|
19
|
+
patchMethod(
|
|
20
|
+
target,
|
|
21
|
+
'sendCommand',
|
|
22
|
+
`senzor.redis.${label}.sendCommand`,
|
|
23
|
+
(original) =>
|
|
24
|
+
function patchedRedisSendCommand(this: any, command: any, ...args: any[]) {
|
|
25
|
+
const commandName = getCommandName(command);
|
|
26
|
+
const span = startCapturedSpan(
|
|
27
|
+
`Redis ${commandName}`,
|
|
28
|
+
'db',
|
|
29
|
+
{
|
|
30
|
+
command: commandName,
|
|
31
|
+
operation: commandName,
|
|
32
|
+
'db.system.name': label === 'ioredis' ? 'redis' : 'redis',
|
|
33
|
+
'db.operation.name': commandName,
|
|
34
|
+
library: label
|
|
35
|
+
},
|
|
36
|
+
options
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (!span) return original.apply(this, arguments as any);
|
|
40
|
+
|
|
41
|
+
return runWithCapturedSpan(span, () => {
|
|
42
|
+
try {
|
|
43
|
+
const result = original.call(this, command, ...args);
|
|
44
|
+
if (result && typeof result.then === 'function') {
|
|
45
|
+
return result.then(
|
|
46
|
+
(value: any) => {
|
|
47
|
+
span.end(0);
|
|
48
|
+
return value;
|
|
49
|
+
},
|
|
50
|
+
(error: any) => {
|
|
51
|
+
span.end(500, {
|
|
52
|
+
error: error?.message,
|
|
53
|
+
'error.type': error?.name || 'Error'
|
|
54
|
+
});
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
span.end(0);
|
|
61
|
+
return result;
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
span.end(500, {
|
|
64
|
+
error: error?.message,
|
|
65
|
+
'error.type': error?.name || 'Error'
|
|
66
|
+
});
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const patchCreatedClient = (
|
|
75
|
+
client: any,
|
|
76
|
+
label: string,
|
|
77
|
+
options?: SenzorOptions
|
|
78
|
+
) => {
|
|
79
|
+
patchSendCommand(client, label, options);
|
|
80
|
+
patchSendCommand(Object.getPrototypeOf(client), label, options);
|
|
81
|
+
return client;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const patchRedisPackage = (redis: any, options?: SenzorOptions) => {
|
|
85
|
+
['createClient', 'createCluster'].forEach((factory) => {
|
|
86
|
+
patchMethod(
|
|
87
|
+
redis,
|
|
88
|
+
factory,
|
|
89
|
+
`senzor.redis.${factory}`,
|
|
90
|
+
(original) =>
|
|
91
|
+
function patchedRedisFactory(this: any, ...args: any[]) {
|
|
92
|
+
const client = original.apply(this, args);
|
|
93
|
+
return patchCreatedClient(client, 'redis', options);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const patchIORedisPackage = (ioredis: any, options?: SenzorOptions) => {
|
|
100
|
+
patchSendCommand(ioredis?.prototype, 'ioredis', options);
|
|
101
|
+
patchSendCommand(ioredis?.Redis?.prototype, 'ioredis', options);
|
|
102
|
+
patchSendCommand(ioredis?.Cluster?.prototype, 'ioredis-cluster', options);
|
|
103
|
+
patchSendCommand(ioredis?.default?.prototype, 'ioredis', options);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const instrumentRedis = (options?: SenzorOptions) => {
|
|
107
|
+
hookRequire('redis', (exports: any) => patchRedisPackage(exports, options));
|
|
108
|
+
hookRequire('ioredis', (exports: any) => patchIORedisPackage(exports, options));
|
|
109
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Context } from '../core/context';
|
|
2
|
+
import { sanitizeAttributes } from '../core/sanitizer';
|
|
3
|
+
import { ActiveTrace, SenzorOptions, Span } from '../core/types';
|
|
4
|
+
import { generateSpanId } from '../utils/ids';
|
|
5
|
+
|
|
6
|
+
type SpanType = Span['type'];
|
|
7
|
+
|
|
8
|
+
export interface CapturedSpan {
|
|
9
|
+
spanId: string;
|
|
10
|
+
parentSpanId?: string;
|
|
11
|
+
trace?: ActiveTrace;
|
|
12
|
+
end: (
|
|
13
|
+
status?: number,
|
|
14
|
+
meta?: Record<string, unknown>
|
|
15
|
+
) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const startCapturedSpan = (
|
|
19
|
+
name: string,
|
|
20
|
+
type: SpanType,
|
|
21
|
+
meta: Record<string, unknown> = {},
|
|
22
|
+
options?: SenzorOptions
|
|
23
|
+
): CapturedSpan | null => {
|
|
24
|
+
const trace = Context.current();
|
|
25
|
+
if (!trace) return null;
|
|
26
|
+
|
|
27
|
+
const spanId = generateSpanId();
|
|
28
|
+
const parentSpanId = trace.activeSpanId;
|
|
29
|
+
const startTime = performance.now() - trace.startTime;
|
|
30
|
+
const startedAt = performance.now();
|
|
31
|
+
let ended = false;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
spanId,
|
|
35
|
+
parentSpanId,
|
|
36
|
+
trace,
|
|
37
|
+
end: (
|
|
38
|
+
status?: number,
|
|
39
|
+
extraMeta: Record<string, unknown> = {}
|
|
40
|
+
) => {
|
|
41
|
+
if (ended) return;
|
|
42
|
+
ended = true;
|
|
43
|
+
|
|
44
|
+
const mergedMeta = sanitizeAttributes(
|
|
45
|
+
{
|
|
46
|
+
...meta,
|
|
47
|
+
...extraMeta,
|
|
48
|
+
parentSpanId
|
|
49
|
+
},
|
|
50
|
+
options
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
Context.addSpanToTrace(trace, {
|
|
54
|
+
spanId,
|
|
55
|
+
parentSpanId,
|
|
56
|
+
name,
|
|
57
|
+
type,
|
|
58
|
+
startTime,
|
|
59
|
+
duration: performance.now() - startedAt,
|
|
60
|
+
status,
|
|
61
|
+
meta: mergedMeta
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const runWithCapturedSpan = <T>(
|
|
68
|
+
span: CapturedSpan | null,
|
|
69
|
+
fn: () => T
|
|
70
|
+
): T => {
|
|
71
|
+
if (!span) return fn();
|
|
72
|
+
return Context.withActiveSpan(span.spanId, fn);
|
|
73
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { normalizePath } from '../core/normalizer';
|
|
2
|
+
import { SenzorOptions } from '../core/types';
|
|
3
|
+
import { SENZOR_INTERNAL_HEADER } from '../utils/internal';
|
|
4
|
+
import { generateTraceparent } from '../utils/traceContext';
|
|
5
|
+
import { hookRequire } from './hook';
|
|
6
|
+
import { patchMethod } from './patch';
|
|
7
|
+
import { runWithCapturedSpan, startCapturedSpan } from './span';
|
|
8
|
+
|
|
9
|
+
const hasInternalHeader = (headers: any): boolean => {
|
|
10
|
+
if (!headers) return false;
|
|
11
|
+
if (Array.isArray(headers)) {
|
|
12
|
+
return headers.some(
|
|
13
|
+
([key, value]) =>
|
|
14
|
+
String(key).toLowerCase() === SENZOR_INTERNAL_HEADER &&
|
|
15
|
+
String(value).toLowerCase() === 'true'
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Object.entries(headers).some(
|
|
20
|
+
([key, value]) =>
|
|
21
|
+
key.toLowerCase() === SENZOR_INTERNAL_HEADER &&
|
|
22
|
+
String(value).toLowerCase() === 'true'
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const setHeader = (headers: any, key: string, value: string) => {
|
|
27
|
+
if (Array.isArray(headers)) {
|
|
28
|
+
headers.push([key, value]);
|
|
29
|
+
return headers;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const nextHeaders = { ...(headers || {}) };
|
|
33
|
+
const existingKey = Object.keys(nextHeaders).find(
|
|
34
|
+
(header) => header.toLowerCase() === key.toLowerCase()
|
|
35
|
+
);
|
|
36
|
+
nextHeaders[existingKey || key] = value;
|
|
37
|
+
return nextHeaders;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const getUrlDetails = (input: any) => {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL(String(input));
|
|
43
|
+
return {
|
|
44
|
+
url: url.toString(),
|
|
45
|
+
hostname: url.hostname,
|
|
46
|
+
path: `${url.pathname}${url.search}`
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
return {
|
|
50
|
+
url: String(input || ''),
|
|
51
|
+
hostname: 'unknown',
|
|
52
|
+
path: '/'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const patchRequestLike = (
|
|
58
|
+
target: any,
|
|
59
|
+
methodName: string,
|
|
60
|
+
patchKey: string,
|
|
61
|
+
options?: SenzorOptions
|
|
62
|
+
) => {
|
|
63
|
+
patchMethod(
|
|
64
|
+
target,
|
|
65
|
+
methodName,
|
|
66
|
+
patchKey,
|
|
67
|
+
(original) =>
|
|
68
|
+
function patchedUndiciRequest(this: any, input: any, opts?: any, cb?: any) {
|
|
69
|
+
if (hasInternalHeader(opts?.headers)) {
|
|
70
|
+
return original.apply(this, arguments as any);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const details = getUrlDetails(input?.origin ? input.origin : input);
|
|
74
|
+
const method = String(opts?.method || 'GET').toUpperCase();
|
|
75
|
+
const span = startCapturedSpan(
|
|
76
|
+
`${method} ${details.hostname}`,
|
|
77
|
+
'http',
|
|
78
|
+
{
|
|
79
|
+
url: details.url,
|
|
80
|
+
method,
|
|
81
|
+
route: normalizePath(details.path),
|
|
82
|
+
library: 'undici',
|
|
83
|
+
'http.request.method': method,
|
|
84
|
+
'url.full': details.url,
|
|
85
|
+
'url.path': details.path,
|
|
86
|
+
'server.address': details.hostname
|
|
87
|
+
},
|
|
88
|
+
options
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!span) return original.apply(this, arguments as any);
|
|
92
|
+
|
|
93
|
+
const nextOptions = { ...(opts || {}) };
|
|
94
|
+
nextOptions.headers = setHeader(
|
|
95
|
+
nextOptions.headers,
|
|
96
|
+
'traceparent',
|
|
97
|
+
generateTraceparent(span.trace!.id, span.spanId)
|
|
98
|
+
);
|
|
99
|
+
nextOptions.headers = setHeader(
|
|
100
|
+
nextOptions.headers,
|
|
101
|
+
'x-senzor-trace-id',
|
|
102
|
+
span.trace!.id
|
|
103
|
+
);
|
|
104
|
+
nextOptions.headers = setHeader(
|
|
105
|
+
nextOptions.headers,
|
|
106
|
+
'x-senzor-parent-span-id',
|
|
107
|
+
span.spanId
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const wrappedCallback =
|
|
111
|
+
typeof cb === 'function'
|
|
112
|
+
? function wrappedUndiciCallback(this: unknown, err: any, data: any) {
|
|
113
|
+
span.end(err ? 500 : data?.statusCode || 0, {
|
|
114
|
+
error: err?.message,
|
|
115
|
+
'error.type': err?.name,
|
|
116
|
+
'http.response.status_code': data?.statusCode
|
|
117
|
+
});
|
|
118
|
+
return cb.apply(this, arguments as any);
|
|
119
|
+
}
|
|
120
|
+
: cb;
|
|
121
|
+
|
|
122
|
+
return runWithCapturedSpan(span, () => {
|
|
123
|
+
try {
|
|
124
|
+
const result = original.call(
|
|
125
|
+
this,
|
|
126
|
+
input,
|
|
127
|
+
nextOptions,
|
|
128
|
+
wrappedCallback
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
if (result && typeof result.then === 'function') {
|
|
132
|
+
return result.then(
|
|
133
|
+
(value: any) => {
|
|
134
|
+
span.end(value?.statusCode || value?.status || 0, {
|
|
135
|
+
'http.response.status_code':
|
|
136
|
+
value?.statusCode || value?.status
|
|
137
|
+
});
|
|
138
|
+
return value;
|
|
139
|
+
},
|
|
140
|
+
(error: any) => {
|
|
141
|
+
span.end(500, {
|
|
142
|
+
error: error?.message,
|
|
143
|
+
'error.type': error?.name || 'Error'
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof wrappedCallback !== 'function') {
|
|
151
|
+
span.end(0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
} catch (error: any) {
|
|
156
|
+
span.end(500, {
|
|
157
|
+
error: error?.message,
|
|
158
|
+
'error.type': error?.name || 'Error'
|
|
159
|
+
});
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const patchUndici = (undici: any, options?: SenzorOptions) => {
|
|
168
|
+
patchRequestLike(undici, 'request', 'senzor.undici.request', options);
|
|
169
|
+
patchRequestLike(undici, 'stream', 'senzor.undici.stream', options);
|
|
170
|
+
patchRequestLike(undici, 'pipeline', 'senzor.undici.pipeline', options);
|
|
171
|
+
|
|
172
|
+
[
|
|
173
|
+
undici?.Client?.prototype,
|
|
174
|
+
undici?.Pool?.prototype,
|
|
175
|
+
undici?.Agent?.prototype,
|
|
176
|
+
undici?.ProxyAgent?.prototype
|
|
177
|
+
].forEach((proto, index) => {
|
|
178
|
+
patchRequestLike(
|
|
179
|
+
proto,
|
|
180
|
+
'request',
|
|
181
|
+
`senzor.undici.dispatcher.${index}.request`,
|
|
182
|
+
options
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const instrumentUndici = (options?: SenzorOptions) => {
|
|
188
|
+
hookRequire('undici', (exports: any) => patchUndici(exports, options));
|
|
189
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { client } from '../core/client';
|
|
2
|
+
import { getClientIp } from '../utils/getClientIp';
|
|
3
|
+
|
|
4
|
+
// 1. Request Handler (Place before routes)
|
|
5
|
+
export const expressMiddleware = () => {
|
|
6
|
+
return (req: any, res: any, next: () => void) => {
|
|
7
|
+
client.startTrace({
|
|
8
|
+
method: req.method,
|
|
9
|
+
path: req.originalUrl || req.url,
|
|
10
|
+
ip: getClientIp(req),
|
|
11
|
+
userAgent: req.headers['user-agent'],
|
|
12
|
+
headers: req.headers
|
|
13
|
+
}, () => {
|
|
14
|
+
|
|
15
|
+
// Auto-detect status code on finish
|
|
16
|
+
res.once('finish', () => {
|
|
17
|
+
try {
|
|
18
|
+
let route = 'UNKNOWN';
|
|
19
|
+
// Express populates req.route only if a route matched
|
|
20
|
+
if (req.route && req.route.path) {
|
|
21
|
+
route = (req.baseUrl || '') + req.route.path;
|
|
22
|
+
} else if (res.statusCode === 404) {
|
|
23
|
+
route = 'Not Found';
|
|
24
|
+
} else {
|
|
25
|
+
route = req.path || 'Wildcard';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
client.endTrace(res.statusCode, { route });
|
|
29
|
+
} catch (e) { /* Fail open */ }
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
next();
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 2. Error Handler (Place after routes)
|
|
38
|
+
// This is required in Express to capture the actual Error Object (Stack Trace)
|
|
39
|
+
export const expressErrorHandler = () => {
|
|
40
|
+
return (err: any, req: any, res: any, next: (err?: any) => void) => {
|
|
41
|
+
|
|
42
|
+
// 1. Capture the exception context
|
|
43
|
+
client.captureError(err);
|
|
44
|
+
|
|
45
|
+
// 2. Pass it to the next error handler (don't swallow it)
|
|
46
|
+
next(err);
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { client } from './core/client';
|
|
2
|
+
|
|
3
|
+
const truthy = (value: string | undefined): boolean =>
|
|
4
|
+
value === '1' || value === 'true' || value === 'yes';
|
|
5
|
+
|
|
6
|
+
const numberFromEnv = (value: string | undefined): number | undefined => {
|
|
7
|
+
if (!value) return undefined;
|
|
8
|
+
const parsed = Number(value);
|
|
9
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const apiKey =
|
|
13
|
+
process.env.SENZOR_API_KEY ||
|
|
14
|
+
process.env.SENZOR_APM_API_KEY ||
|
|
15
|
+
process.env.SENZOR_SERVICE_API_KEY;
|
|
16
|
+
|
|
17
|
+
const endpoint =
|
|
18
|
+
process.env.SENZOR_ENDPOINT ||
|
|
19
|
+
process.env.SENZOR_APM_ENDPOINT;
|
|
20
|
+
|
|
21
|
+
const options = {
|
|
22
|
+
apiKey: apiKey || '',
|
|
23
|
+
endpoint,
|
|
24
|
+
debug: truthy(process.env.SENZOR_DEBUG),
|
|
25
|
+
autoLogs: process.env.SENZOR_AUTO_LOGS === 'false' ? false : undefined,
|
|
26
|
+
batchSize: numberFromEnv(process.env.SENZOR_BATCH_SIZE),
|
|
27
|
+
flushInterval: numberFromEnv(process.env.SENZOR_FLUSH_INTERVAL),
|
|
28
|
+
flushTimeoutMs: numberFromEnv(process.env.SENZOR_FLUSH_TIMEOUT_MS),
|
|
29
|
+
maxQueueSize: numberFromEnv(process.env.SENZOR_MAX_QUEUE_SIZE),
|
|
30
|
+
maxSpansPerTrace: numberFromEnv(process.env.SENZOR_MAX_SPANS_PER_TRACE),
|
|
31
|
+
captureHeaders: truthy(process.env.SENZOR_CAPTURE_HEADERS),
|
|
32
|
+
captureDbStatement:
|
|
33
|
+
process.env.SENZOR_CAPTURE_DB_STATEMENT === 'false'
|
|
34
|
+
? false
|
|
35
|
+
: undefined,
|
|
36
|
+
frameworkSpans:
|
|
37
|
+
process.env.SENZOR_FRAMEWORK_SPANS === 'false'
|
|
38
|
+
? false
|
|
39
|
+
: undefined,
|
|
40
|
+
captureMiddlewareSpans:
|
|
41
|
+
process.env.SENZOR_CAPTURE_MIDDLEWARE_SPANS === 'false'
|
|
42
|
+
? false
|
|
43
|
+
: undefined,
|
|
44
|
+
captureRouterSpans:
|
|
45
|
+
process.env.SENZOR_CAPTURE_ROUTER_SPANS === 'false'
|
|
46
|
+
? false
|
|
47
|
+
: undefined,
|
|
48
|
+
captureLifecycleHookSpans:
|
|
49
|
+
process.env.SENZOR_CAPTURE_LIFECYCLE_HOOK_SPANS === 'false'
|
|
50
|
+
? false
|
|
51
|
+
: undefined
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (apiKey) {
|
|
55
|
+
client.init(options);
|
|
56
|
+
} else {
|
|
57
|
+
client.preload(options);
|
|
58
|
+
}
|