@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
|
@@ -1,162 +1,530 @@
|
|
|
1
|
-
import http from 'http';
|
|
2
|
-
import https from 'https';
|
|
3
|
-
import { URL } from 'url';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
};
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
import type { SenzorClient } from '../core/client';
|
|
5
|
+
import { Context } from '../core/context';
|
|
6
|
+
import { getRoute, normalizePath } from '../core/normalizer';
|
|
7
|
+
import { sanitizeHeaders } from '../core/sanitizer';
|
|
8
|
+
import { SenzorOptions } from '../core/types';
|
|
9
|
+
import { getClientIp } from '../utils/getClientIp';
|
|
10
|
+
import { SENZOR_INTERNAL_HEADER } from '../utils/internal';
|
|
11
|
+
import { generateTraceparent } from '../utils/traceContext';
|
|
12
|
+
import { patchMethod } from './patch';
|
|
13
|
+
import { runWithCapturedSpan, startCapturedSpan } from './span';
|
|
14
|
+
|
|
15
|
+
const getDebug = (options?: SenzorOptions): boolean =>
|
|
16
|
+
Boolean(options?.debug);
|
|
17
|
+
|
|
18
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
19
|
+
typeof value === 'object' &&
|
|
20
|
+
value !== null &&
|
|
21
|
+
!(value instanceof URL) &&
|
|
22
|
+
!(value instanceof Function) &&
|
|
23
|
+
!Array.isArray(value);
|
|
24
|
+
|
|
25
|
+
const headerValue = (
|
|
26
|
+
headers: unknown,
|
|
27
|
+
key: string
|
|
28
|
+
): unknown => {
|
|
29
|
+
if (!headers) return undefined;
|
|
30
|
+
|
|
31
|
+
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
|
|
32
|
+
return headers.get(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(headers)) {
|
|
36
|
+
const found = headers.find(
|
|
37
|
+
([name]) => String(name).toLowerCase() === key.toLowerCase()
|
|
38
|
+
);
|
|
39
|
+
return found?.[1];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (typeof headers === 'object') {
|
|
43
|
+
const normalizedKey = key.toLowerCase();
|
|
44
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
45
|
+
if (name.toLowerCase() === normalizedKey) return value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return undefined;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const hasInternalHeader = (headers: unknown): boolean =>
|
|
53
|
+
String(headerValue(headers, SENZOR_INTERNAL_HEADER) || '').toLowerCase() ===
|
|
54
|
+
'true';
|
|
55
|
+
|
|
56
|
+
const shouldIgnoreUrl = (
|
|
57
|
+
urlString: string,
|
|
58
|
+
ingestUrl: string,
|
|
59
|
+
headers?: unknown
|
|
60
|
+
): boolean => {
|
|
61
|
+
if (hasInternalHeader(headers)) return true;
|
|
62
|
+
if (!urlString) return false;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const url = new URL(urlString);
|
|
66
|
+
const ingest = new URL(ingestUrl);
|
|
67
|
+
return (
|
|
68
|
+
url.hostname === ingest.hostname &&
|
|
69
|
+
url.pathname.startsWith('/api/ingest')
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
return ingestUrl ? urlString.includes(ingestUrl) : false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const cloneHeaders = (headers: unknown): Record<string, unknown> => {
|
|
77
|
+
if (!headers) return {};
|
|
78
|
+
|
|
79
|
+
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
|
|
80
|
+
const cloned: Record<string, unknown> = {};
|
|
81
|
+
headers.forEach((value, key) => {
|
|
82
|
+
cloned[key] = value;
|
|
83
|
+
});
|
|
84
|
+
return cloned;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(headers)) {
|
|
88
|
+
return headers.reduce<Record<string, unknown>>((acc, [key, value]) => {
|
|
89
|
+
acc[key] = value;
|
|
90
|
+
return acc;
|
|
91
|
+
}, {});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof headers === 'object') {
|
|
95
|
+
return { ...(headers as Record<string, unknown>) };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const setHeader = (
|
|
102
|
+
headers: Record<string, unknown>,
|
|
103
|
+
key: string,
|
|
104
|
+
value: string
|
|
105
|
+
) => {
|
|
106
|
+
const existingKey = Object.keys(headers).find(
|
|
107
|
+
(header) => header.toLowerCase() === key.toLowerCase()
|
|
108
|
+
);
|
|
109
|
+
headers[existingKey || key] = value;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
interface PreparedRequest {
|
|
113
|
+
args: any[];
|
|
114
|
+
options: Record<string, any>;
|
|
115
|
+
url: string;
|
|
116
|
+
method: string;
|
|
117
|
+
hostname: string;
|
|
118
|
+
path: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const prepareRequestArgs = (
|
|
122
|
+
args: any[],
|
|
123
|
+
defaultProtocol: 'http:' | 'https:'
|
|
124
|
+
): PreparedRequest => {
|
|
125
|
+
const nextArgs = [...args];
|
|
126
|
+
let optionsIndex = 0;
|
|
127
|
+
let options: Record<string, any> = {};
|
|
128
|
+
let urlFromArg: URL | null = null;
|
|
129
|
+
|
|
130
|
+
if (typeof nextArgs[0] === 'string' || nextArgs[0] instanceof URL) {
|
|
131
|
+
try {
|
|
132
|
+
urlFromArg = new URL(nextArgs[0].toString());
|
|
133
|
+
} catch {
|
|
134
|
+
urlFromArg = null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (isPlainObject(nextArgs[1])) {
|
|
138
|
+
optionsIndex = 1;
|
|
139
|
+
options = {
|
|
140
|
+
...nextArgs[1],
|
|
141
|
+
headers: cloneHeaders(nextArgs[1].headers)
|
|
142
|
+
};
|
|
143
|
+
nextArgs[1] = options;
|
|
144
|
+
} else {
|
|
145
|
+
optionsIndex = 1;
|
|
146
|
+
options = { headers: {} };
|
|
147
|
+
nextArgs.splice(1, 0, options);
|
|
148
|
+
}
|
|
149
|
+
} else if (isPlainObject(nextArgs[0])) {
|
|
150
|
+
optionsIndex = 0;
|
|
151
|
+
options = {
|
|
152
|
+
...nextArgs[0],
|
|
153
|
+
headers: cloneHeaders(nextArgs[0].headers)
|
|
154
|
+
};
|
|
155
|
+
nextArgs[0] = options;
|
|
156
|
+
} else {
|
|
157
|
+
optionsIndex = 0;
|
|
158
|
+
options = { headers: {} };
|
|
159
|
+
nextArgs[0] = options;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!options.headers) options.headers = {};
|
|
163
|
+
nextArgs[optionsIndex] = options;
|
|
164
|
+
|
|
165
|
+
const protocol =
|
|
166
|
+
options.protocol ||
|
|
167
|
+
urlFromArg?.protocol ||
|
|
168
|
+
(options.port === 443 ? 'https:' : defaultProtocol);
|
|
169
|
+
const hostname =
|
|
170
|
+
options.hostname ||
|
|
171
|
+
options.host ||
|
|
172
|
+
urlFromArg?.hostname ||
|
|
173
|
+
'localhost';
|
|
174
|
+
const path =
|
|
175
|
+
options.path ||
|
|
176
|
+
`${urlFromArg?.pathname || '/'}${urlFromArg?.search || ''}`;
|
|
177
|
+
const url = urlFromArg
|
|
178
|
+
? urlFromArg.toString()
|
|
179
|
+
: `${protocol}//${hostname}${path}`;
|
|
180
|
+
const method = String(options.method || 'GET').toUpperCase();
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
args: nextArgs,
|
|
184
|
+
options,
|
|
185
|
+
url,
|
|
186
|
+
method,
|
|
187
|
+
hostname: String(hostname).replace(/:\d+$/, ''),
|
|
188
|
+
path
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const resolveIncomingRoute = (
|
|
193
|
+
req: any,
|
|
194
|
+
res: any,
|
|
195
|
+
path: string
|
|
196
|
+
): string => {
|
|
197
|
+
if (res?.statusCode === 404) return 'Not Found';
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
return getRoute(req, path);
|
|
201
|
+
} catch {
|
|
202
|
+
return normalizePath(path);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const patchIncomingServer = (
|
|
207
|
+
proto: any,
|
|
208
|
+
protocol: 'http' | 'https',
|
|
209
|
+
client: SenzorClient,
|
|
210
|
+
options?: SenzorOptions
|
|
211
|
+
) => {
|
|
212
|
+
patchMethod(
|
|
213
|
+
proto,
|
|
214
|
+
'emit',
|
|
215
|
+
`senzor.${protocol}.server`,
|
|
216
|
+
(original) =>
|
|
217
|
+
function patchedEmit(this: any, event: string, ...args: any[]) {
|
|
218
|
+
if (event !== 'request') {
|
|
219
|
+
return original.call(this, event, ...args);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const req = args[0];
|
|
223
|
+
const res = args[1];
|
|
224
|
+
|
|
225
|
+
if (!req || !res || Context.current()?.contextType === 'apm') {
|
|
226
|
+
return original.call(this, event, ...args);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const rawPath = req.originalUrl || req.url || '/';
|
|
230
|
+
const path = String(rawPath).split('?')[0] || '/';
|
|
231
|
+
const headers = req.headers || {};
|
|
232
|
+
|
|
233
|
+
if (hasInternalHeader(headers)) {
|
|
234
|
+
return original.call(this, event, ...args);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return client.startTrace(
|
|
238
|
+
{
|
|
239
|
+
method: req.method || 'GET',
|
|
240
|
+
path: rawPath,
|
|
241
|
+
route: normalizePath(path),
|
|
242
|
+
ip: getClientIp(req),
|
|
243
|
+
userAgent: headers['user-agent'],
|
|
244
|
+
headers,
|
|
245
|
+
meta: {
|
|
246
|
+
protocol,
|
|
247
|
+
httpVersion: req.httpVersion,
|
|
248
|
+
headers: options?.captureHeaders
|
|
249
|
+
? sanitizeHeaders(headers, options)
|
|
250
|
+
: undefined
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
() => {
|
|
254
|
+
const trace = Context.current();
|
|
255
|
+
let finalized = false;
|
|
256
|
+
|
|
257
|
+
const finalize = (reason: 'finish' | 'close' | 'error') => {
|
|
258
|
+
if (finalized || !trace) return;
|
|
259
|
+
finalized = true;
|
|
260
|
+
|
|
261
|
+
setImmediate(() => {
|
|
262
|
+
if (trace.ended) return;
|
|
263
|
+
|
|
264
|
+
Context.run(trace, () => {
|
|
265
|
+
client.endTrace(res.statusCode || 0, {
|
|
266
|
+
route: resolveIncomingRoute(req, res, path),
|
|
267
|
+
statusMessage: res.statusMessage,
|
|
268
|
+
meta: {
|
|
269
|
+
...trace.data.meta,
|
|
270
|
+
endReason: reason
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
res.once('finish', () => finalize('finish'));
|
|
278
|
+
res.once('close', () => finalize('close'));
|
|
279
|
+
res.once('error', (error: Error) => {
|
|
280
|
+
client.captureError(error, {
|
|
281
|
+
instrumentation: `${protocol}.server`
|
|
282
|
+
});
|
|
283
|
+
finalize('error');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
return original.call(this, event, ...args);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
client.captureError(error, {
|
|
290
|
+
instrumentation: `${protocol}.server`
|
|
291
|
+
});
|
|
292
|
+
finalize('error');
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const patchOutgoing = (
|
|
302
|
+
moduleRef: typeof http | typeof https,
|
|
303
|
+
protocol: 'http:' | 'https:',
|
|
304
|
+
ingestUrl: string,
|
|
305
|
+
options?: SenzorOptions
|
|
306
|
+
) => {
|
|
307
|
+
const patchKeyPrefix = protocol === 'https:' ? 'senzor.https' : 'senzor.http';
|
|
308
|
+
|
|
309
|
+
const requestWrapper = (original: Function) =>
|
|
310
|
+
function patchedRequest(this: any, ...args: any[]) {
|
|
311
|
+
const prepared = prepareRequestArgs(args, protocol);
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
shouldIgnoreUrl(
|
|
315
|
+
prepared.url,
|
|
316
|
+
ingestUrl,
|
|
317
|
+
prepared.options.headers
|
|
318
|
+
)
|
|
319
|
+
) {
|
|
320
|
+
return original.apply(this, args);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const trace = Context.current();
|
|
324
|
+
if (!trace) return original.apply(this, args);
|
|
325
|
+
|
|
326
|
+
const span = startCapturedSpan(
|
|
327
|
+
`${prepared.method} ${prepared.hostname}`,
|
|
328
|
+
'http',
|
|
329
|
+
{
|
|
330
|
+
url: prepared.url,
|
|
331
|
+
method: prepared.method,
|
|
332
|
+
library: protocol === 'https:' ? 'https' : 'http',
|
|
333
|
+
'http.request.method': prepared.method,
|
|
334
|
+
'url.full': prepared.url,
|
|
335
|
+
'url.path': prepared.path,
|
|
336
|
+
'server.address': prepared.hostname
|
|
337
|
+
},
|
|
338
|
+
options
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (span) {
|
|
342
|
+
setHeader(
|
|
343
|
+
prepared.options.headers,
|
|
344
|
+
'traceparent',
|
|
345
|
+
generateTraceparent(trace.id, span.spanId)
|
|
346
|
+
);
|
|
347
|
+
setHeader(prepared.options.headers, 'x-senzor-trace-id', trace.id);
|
|
348
|
+
setHeader(
|
|
349
|
+
prepared.options.headers,
|
|
350
|
+
'x-senzor-parent-span-id',
|
|
351
|
+
span.spanId
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const invoke = () => {
|
|
356
|
+
const req = original.apply(this, prepared.args);
|
|
357
|
+
if (!span || !req || typeof req.once !== 'function') {
|
|
358
|
+
return req;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
let completed = false;
|
|
362
|
+
const endSpan = (
|
|
363
|
+
status: number,
|
|
364
|
+
extraMeta: Record<string, unknown> = {}
|
|
365
|
+
) => {
|
|
366
|
+
if (completed) return;
|
|
367
|
+
completed = true;
|
|
368
|
+
span.end(status, extraMeta);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
req.once('response', (res: any) => {
|
|
372
|
+
const statusCode = res?.statusCode || 0;
|
|
373
|
+
const finish = () =>
|
|
374
|
+
endSpan(statusCode, {
|
|
375
|
+
'http.response.status_code': statusCode
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
res.once('end', finish);
|
|
379
|
+
res.once('close', finish);
|
|
380
|
+
res.once('error', (error: Error) =>
|
|
381
|
+
endSpan(500, {
|
|
382
|
+
error: error.message,
|
|
383
|
+
'error.type': error.name
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
req.once('timeout', () =>
|
|
389
|
+
endSpan(504, {
|
|
390
|
+
error: 'Request timed out',
|
|
391
|
+
'error.type': 'TimeoutError'
|
|
392
|
+
})
|
|
393
|
+
);
|
|
394
|
+
req.once('error', (error: Error) =>
|
|
395
|
+
endSpan(500, {
|
|
396
|
+
error: error.message,
|
|
397
|
+
'error.type': error.name
|
|
398
|
+
})
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return req;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
if (getDebug(options)) {
|
|
405
|
+
console.log(`[Senzor] Injecting trace headers to ${prepared.url}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return runWithCapturedSpan(span, invoke);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
patchMethod(
|
|
412
|
+
moduleRef,
|
|
413
|
+
'request',
|
|
414
|
+
`${patchKeyPrefix}.request`,
|
|
415
|
+
requestWrapper
|
|
416
|
+
);
|
|
417
|
+
patchMethod(
|
|
418
|
+
moduleRef,
|
|
419
|
+
'get',
|
|
420
|
+
`${patchKeyPrefix}.get`,
|
|
421
|
+
requestWrapper
|
|
422
|
+
);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
export const instrumentFetch = (
|
|
426
|
+
ingestUrl: string,
|
|
427
|
+
options?: SenzorOptions
|
|
428
|
+
) => {
|
|
429
|
+
if (!globalThis.fetch) return;
|
|
430
|
+
|
|
431
|
+
patchMethod(
|
|
432
|
+
globalThis,
|
|
433
|
+
'fetch',
|
|
434
|
+
'senzor.fetch',
|
|
435
|
+
(original) =>
|
|
436
|
+
async function patchedFetch(
|
|
437
|
+
this: any,
|
|
438
|
+
input: any,
|
|
439
|
+
init?: any
|
|
440
|
+
): Promise<Response> {
|
|
441
|
+
const urlString =
|
|
442
|
+
typeof input === 'string'
|
|
443
|
+
? input
|
|
444
|
+
: input instanceof URL
|
|
445
|
+
? input.toString()
|
|
446
|
+
: input?.url || '';
|
|
447
|
+
|
|
448
|
+
const originalHeaders = init?.headers || input?.headers;
|
|
449
|
+
if (shouldIgnoreUrl(urlString, ingestUrl, originalHeaders)) {
|
|
450
|
+
return original.call(this, input, init);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const trace = Context.current();
|
|
454
|
+
if (!trace) return original.call(this, input, init);
|
|
455
|
+
|
|
456
|
+
let hostname = 'unknown';
|
|
457
|
+
let path = '/';
|
|
458
|
+
try {
|
|
459
|
+
const url = new URL(urlString);
|
|
460
|
+
hostname = url.hostname;
|
|
461
|
+
path = `${url.pathname}${url.search}`;
|
|
462
|
+
} catch { }
|
|
463
|
+
|
|
464
|
+
const method = String(
|
|
465
|
+
init?.method || input?.method || 'GET'
|
|
466
|
+
).toUpperCase();
|
|
467
|
+
const span = startCapturedSpan(
|
|
468
|
+
`${method} ${hostname}`,
|
|
469
|
+
'http',
|
|
470
|
+
{
|
|
471
|
+
url: urlString,
|
|
472
|
+
method,
|
|
473
|
+
library: 'fetch',
|
|
474
|
+
'http.request.method': method,
|
|
475
|
+
'url.full': urlString,
|
|
476
|
+
'url.path': path,
|
|
477
|
+
'server.address': hostname
|
|
478
|
+
},
|
|
479
|
+
options
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
if (!span) return original.call(this, input, init);
|
|
483
|
+
|
|
484
|
+
const nextInit = { ...(init || {}) };
|
|
485
|
+
const headers =
|
|
486
|
+
typeof Headers !== 'undefined'
|
|
487
|
+
? new Headers(originalHeaders || undefined)
|
|
488
|
+
: cloneHeaders(originalHeaders);
|
|
489
|
+
|
|
490
|
+
if (typeof Headers !== 'undefined' && headers instanceof Headers) {
|
|
491
|
+
headers.set('traceparent', generateTraceparent(trace.id, span.spanId));
|
|
492
|
+
headers.set('x-senzor-trace-id', trace.id);
|
|
493
|
+
headers.set('x-senzor-parent-span-id', span.spanId);
|
|
494
|
+
} else {
|
|
495
|
+
setHeader(headers as Record<string, unknown>, 'traceparent', generateTraceparent(trace.id, span.spanId));
|
|
496
|
+
setHeader(headers as Record<string, unknown>, 'x-senzor-trace-id', trace.id);
|
|
497
|
+
setHeader(headers as Record<string, unknown>, 'x-senzor-parent-span-id', span.spanId);
|
|
498
|
+
}
|
|
499
|
+
nextInit.headers = headers;
|
|
500
|
+
|
|
501
|
+
return runWithCapturedSpan(span, async () => {
|
|
502
|
+
try {
|
|
503
|
+
const response = await original.call(this, input, nextInit);
|
|
504
|
+
span.end(response.status, {
|
|
505
|
+
'http.response.status_code': response.status
|
|
506
|
+
});
|
|
507
|
+
return response;
|
|
508
|
+
} catch (error: any) {
|
|
509
|
+
span.end(500, {
|
|
510
|
+
error: error?.message,
|
|
511
|
+
'error.type': error?.name || 'Error'
|
|
512
|
+
});
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
);
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
export const instrumentHttp = (
|
|
521
|
+
client: SenzorClient,
|
|
522
|
+
ingestUrl: string,
|
|
523
|
+
options?: SenzorOptions
|
|
524
|
+
) => {
|
|
525
|
+
patchIncomingServer(http.Server?.prototype, 'http', client, options);
|
|
526
|
+
patchIncomingServer(https.Server?.prototype, 'https', client, options);
|
|
527
|
+
|
|
528
|
+
patchOutgoing(http, 'http:', ingestUrl, options);
|
|
529
|
+
patchOutgoing(https, 'https:', ingestUrl, options);
|
|
530
|
+
};
|