@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +398 -48
  3. package/dist/index.d.mts +14 -0
  4. package/dist/index.d.ts +14 -0
  5. package/dist/index.global.js +1 -1
  6. package/dist/index.global.js.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/index.mjs +1 -1
  10. package/dist/index.mjs.map +1 -1
  11. package/dist/register.d.mts +2 -0
  12. package/dist/register.d.ts +2 -0
  13. package/dist/register.js +2 -0
  14. package/dist/register.js.map +1 -0
  15. package/dist/register.mjs +2 -0
  16. package/dist/register.mjs.map +1 -0
  17. package/package.json +15 -4
  18. package/src/core/client.ts +167 -107
  19. package/src/core/context.ts +48 -21
  20. package/src/core/sanitizer.ts +203 -0
  21. package/src/core/transport.ts +273 -104
  22. package/src/core/types.ts +43 -24
  23. package/src/index.ts +5 -4
  24. package/src/instrumentation/express.ts +338 -0
  25. package/src/instrumentation/fastify.ts +296 -0
  26. package/src/instrumentation/framework.ts +301 -0
  27. package/src/instrumentation/hook.ts +49 -31
  28. package/src/instrumentation/http.ts +530 -162
  29. package/src/instrumentation/koa.ts +173 -0
  30. package/src/instrumentation/mongo.ts +202 -105
  31. package/src/instrumentation/mongoose.ts +156 -0
  32. package/src/instrumentation/mysql.ts +169 -0
  33. package/src/instrumentation/patch.ts +56 -0
  34. package/src/instrumentation/pg.ts +131 -41
  35. package/src/instrumentation/redis.ts +109 -0
  36. package/src/instrumentation/span.ts +73 -0
  37. package/src/instrumentation/undici.ts +189 -0
  38. package/src/register.ts +58 -0
  39. package/src/utils/ids.ts +7 -0
  40. package/src/utils/internal.ts +1 -0
  41. package/src/wrappers/fastify.ts +10 -7
  42. package/src/wrappers/h3.ts +40 -16
  43. package/src/wrappers/next.ts +68 -21
  44. package/tsup.config.ts +21 -11
  45. package/wiki.md +852 -120
@@ -0,0 +1,301 @@
1
+ import { Context } from '../core/context';
2
+ import { SenzorOptions } from '../core/types';
3
+ import { CapturedSpan, runWithCapturedSpan, startCapturedSpan } from './span';
4
+
5
+ export type FrameworkSpanType =
6
+ | 'middleware'
7
+ | 'router'
8
+ | 'request_handler'
9
+ | 'route_handler'
10
+ | 'controller_handler'
11
+ | 'lifecycle_hook'
12
+ | 'error_handler'
13
+ | 'event_handler';
14
+
15
+ export interface FrameworkSpanInfo {
16
+ framework: string;
17
+ type: FrameworkSpanType;
18
+ name: string;
19
+ route?: string;
20
+ method?: string;
21
+ layerPath?: string;
22
+ handlerName?: string;
23
+ request?: any;
24
+ response?: any;
25
+ attributes?: Record<string, unknown>;
26
+ }
27
+
28
+ interface InvokeOptions {
29
+ callbackIndex?: number;
30
+ callbackCompletesSpan?: boolean;
31
+ responseEndsSpan?: boolean;
32
+ }
33
+
34
+ const ignoredNextValues = new Set([undefined, null, 'route', 'router']);
35
+
36
+ export const shouldCaptureFrameworkSpan = (
37
+ type: FrameworkSpanType,
38
+ options?: SenzorOptions
39
+ ): boolean => {
40
+ if (options?.frameworkSpans === false) return false;
41
+ if (type === 'middleware' && options?.captureMiddlewareSpans === false) return false;
42
+ if (type === 'router' && options?.captureRouterSpans === false) return false;
43
+ if (type === 'lifecycle_hook' && options?.captureLifecycleHookSpans === false) return false;
44
+ if (options?.ignoreFrameworkSpanTypes?.includes(type)) return false;
45
+ return true;
46
+ };
47
+
48
+ const statusFrom = (
49
+ info: FrameworkSpanInfo,
50
+ fallback = 0
51
+ ): number => {
52
+ const res = info.response;
53
+ return (
54
+ res?.statusCode ||
55
+ res?.status ||
56
+ res?.raw?.statusCode ||
57
+ res?.status_code ||
58
+ fallback
59
+ );
60
+ };
61
+
62
+ const isPromiseLike = (value: unknown): value is Promise<unknown> =>
63
+ Boolean(value && typeof (value as any).then === 'function');
64
+
65
+ const runWithParentOf = <T>(
66
+ span: CapturedSpan,
67
+ fn: () => T
68
+ ): T => {
69
+ if (!span.parentSpanId) return fn();
70
+ return Context.withActiveSpan(span.parentSpanId, fn);
71
+ };
72
+
73
+ const copyFunctionProperties = (
74
+ source: Function,
75
+ target: Function
76
+ ) => {
77
+ for (const key in source as any) {
78
+ try {
79
+ Object.defineProperty(target, key, {
80
+ configurable: true,
81
+ enumerable: true,
82
+ get() {
83
+ return (source as any)[key];
84
+ },
85
+ set(value) {
86
+ (source as any)[key] = value;
87
+ }
88
+ });
89
+ } catch { }
90
+ }
91
+ };
92
+
93
+ export const invokeWithFrameworkSpan = (
94
+ handler: Function,
95
+ thisArg: unknown,
96
+ args: any[],
97
+ info: FrameworkSpanInfo,
98
+ options?: SenzorOptions,
99
+ invokeOptions: InvokeOptions = {}
100
+ ) => {
101
+ if (!shouldCaptureFrameworkSpan(info.type, options) || !Context.current()) {
102
+ return handler.apply(thisArg, args);
103
+ }
104
+
105
+ const span = startCapturedSpan(
106
+ info.name,
107
+ 'function',
108
+ {
109
+ framework: info.framework,
110
+ 'senzor.framework': info.framework,
111
+ 'senzor.framework.type': info.type,
112
+ 'http.route': info.route,
113
+ route: info.route,
114
+ method: info.method,
115
+ layerPath: info.layerPath,
116
+ handlerName: info.handlerName,
117
+ ...info.attributes
118
+ },
119
+ options
120
+ );
121
+
122
+ if (!span) return handler.apply(thisArg, args);
123
+
124
+ let ended = false;
125
+ const cleanup: Array<() => void> = [];
126
+
127
+ const endSpan = (
128
+ status = statusFrom(info),
129
+ meta: Record<string, unknown> = {}
130
+ ) => {
131
+ if (ended) return;
132
+ ended = true;
133
+
134
+ for (const clean of cleanup) {
135
+ try { clean(); } catch { }
136
+ }
137
+
138
+ span.end(status, meta);
139
+ };
140
+
141
+ const res = info.response;
142
+ if (invokeOptions.responseEndsSpan !== false && res?.once) {
143
+ const onFinish = () =>
144
+ endSpan(statusFrom(info), { completion: 'response.finish' });
145
+ const onClose = () =>
146
+ endSpan(statusFrom(info), { completion: 'response.close' });
147
+
148
+ res.once('finish', onFinish);
149
+ res.once('close', onClose);
150
+
151
+ cleanup.push(() => {
152
+ try { res.removeListener?.('finish', onFinish); } catch { }
153
+ try { res.removeListener?.('close', onClose); } catch { }
154
+ });
155
+ }
156
+
157
+ const callbackIndex =
158
+ invokeOptions.callbackIndex ??
159
+ args.findIndex((arg) => typeof arg === 'function');
160
+
161
+ if (
162
+ invokeOptions.callbackCompletesSpan !== false &&
163
+ callbackIndex >= 0 &&
164
+ typeof args[callbackIndex] === 'function'
165
+ ) {
166
+ const originalCallback = args[callbackIndex];
167
+ args[callbackIndex] = function wrappedFrameworkCallback(
168
+ this: unknown,
169
+ ...callbackArgs: any[]
170
+ ) {
171
+ const maybeError = callbackArgs[0];
172
+ const hasError = !ignoredNextValues.has(maybeError);
173
+
174
+ endSpan(hasError ? 500 : statusFrom(info), {
175
+ completion: 'callback',
176
+ error: hasError ? String(maybeError?.message || maybeError) : undefined,
177
+ 'error.type': hasError
178
+ ? maybeError?.name || typeof maybeError
179
+ : undefined
180
+ });
181
+
182
+ return runWithParentOf(span, () =>
183
+ originalCallback.apply(this, callbackArgs)
184
+ );
185
+ };
186
+ }
187
+
188
+ return runWithCapturedSpan(span, () => {
189
+ try {
190
+ const result = handler.apply(thisArg, args);
191
+
192
+ if (isPromiseLike(result)) {
193
+ return result.then(
194
+ (value) => {
195
+ endSpan(statusFrom(info), { completion: 'promise.resolve' });
196
+ return value;
197
+ },
198
+ (error) => {
199
+ endSpan(500, {
200
+ completion: 'promise.reject',
201
+ error: error?.message,
202
+ 'error.type': error?.name || 'Error'
203
+ });
204
+ throw error;
205
+ }
206
+ );
207
+ }
208
+
209
+ if (callbackIndex < 0 && invokeOptions.responseEndsSpan === false) {
210
+ endSpan(statusFrom(info), { completion: 'sync.return' });
211
+ }
212
+
213
+ return result;
214
+ } catch (error: any) {
215
+ endSpan(500, {
216
+ completion: 'throw',
217
+ error: error?.message,
218
+ 'error.type': error?.name || 'Error'
219
+ });
220
+ throw error;
221
+ }
222
+ });
223
+ };
224
+
225
+ export const wrapFrameworkHandler = <T extends Function>(
226
+ handler: T,
227
+ getInfo: (thisArg: unknown, args: any[]) => FrameworkSpanInfo,
228
+ options?: SenzorOptions,
229
+ invokeOptions: InvokeOptions = {}
230
+ ): T => {
231
+ if (typeof handler !== 'function') return handler;
232
+
233
+ const wrapped = function wrappedFrameworkHandler(
234
+ this: unknown,
235
+ ...args: any[]
236
+ ) {
237
+ return invokeWithFrameworkSpan(
238
+ handler,
239
+ this,
240
+ args,
241
+ getInfo(this, args),
242
+ options,
243
+ invokeOptions
244
+ );
245
+ };
246
+
247
+ copyFunctionProperties(handler, wrapped);
248
+ return wrapped as unknown as T;
249
+ };
250
+
251
+ export const wrapFrameworkHandlerWithArity = <T extends Function>(
252
+ handler: T,
253
+ getInfo: (thisArg: unknown, args: any[]) => FrameworkSpanInfo,
254
+ options?: SenzorOptions,
255
+ invokeOptions: InvokeOptions = {}
256
+ ): T => {
257
+ if (typeof handler !== 'function') return handler;
258
+
259
+ const invoke = (thisArg: unknown, args: any[]) =>
260
+ invokeWithFrameworkSpan(
261
+ handler,
262
+ thisArg,
263
+ args,
264
+ getInfo(thisArg, args),
265
+ options,
266
+ invokeOptions
267
+ );
268
+
269
+ let wrapped: Function;
270
+
271
+ switch (handler.length) {
272
+ case 4:
273
+ wrapped = function wrapped4(this: unknown, a: any, b: any, c: any, d: any) {
274
+ return invoke(this, [a, b, c, d]);
275
+ };
276
+ break;
277
+ case 3:
278
+ wrapped = function wrapped3(this: unknown, a: any, b: any, c: any) {
279
+ return invoke(this, [a, b, c]);
280
+ };
281
+ break;
282
+ case 2:
283
+ wrapped = function wrapped2(this: unknown, a: any, b: any) {
284
+ return invoke(this, [a, b]);
285
+ };
286
+ break;
287
+ case 1:
288
+ wrapped = function wrapped1(this: unknown, a: any) {
289
+ return invoke(this, [a]);
290
+ };
291
+ break;
292
+ default:
293
+ wrapped = function wrapped0(this: unknown) {
294
+ return invoke(this, Array.from(arguments));
295
+ };
296
+ break;
297
+ }
298
+
299
+ copyFunctionProperties(handler, wrapped);
300
+ return wrapped as unknown as T;
301
+ };
@@ -6,8 +6,8 @@ const SENZOR_PATCHED =
6
6
  const SENZOR_HOOKS =
7
7
  Symbol.for('senzor.require.hooks');
8
8
 
9
- type HookFn =
10
- (exports: unknown) => void;
9
+ type HookFn =
10
+ (exports: unknown) => unknown | void;
11
11
 
12
12
  type HookMap =
13
13
  Map<string, HookFn[]>;
@@ -37,10 +37,10 @@ function getHookRegistry(): HookMap {
37
37
 
38
38
  }
39
39
 
40
- function runHooks(
41
- moduleName: string,
42
- exports: unknown
43
- ) {
40
+ function runHooks(
41
+ moduleName: string,
42
+ exports: unknown
43
+ ) {
44
44
 
45
45
  const registry =
46
46
  (Module as unknown as Record<
@@ -48,18 +48,27 @@ function runHooks(
48
48
  HookMap
49
49
  >)[SENZOR_HOOKS];
50
50
 
51
- if (!registry) return;
51
+ if (!registry) return exports;
52
52
 
53
53
  const hooks =
54
54
  registry.get(moduleName);
55
55
 
56
- if (!hooks?.length) return;
57
-
58
- for (const hook of hooks) {
59
-
60
- try {
61
- hook(exports);
62
- }
56
+ if (!hooks?.length) return exports;
57
+
58
+ let currentExports =
59
+ exports;
60
+
61
+ for (const hook of hooks) {
62
+
63
+ try {
64
+ const nextExports =
65
+ hook(currentExports);
66
+
67
+ if (nextExports !== undefined) {
68
+ currentExports =
69
+ nextExports;
70
+ }
71
+ }
63
72
  catch (err) {
64
73
 
65
74
  console.error(
@@ -69,9 +78,11 @@ function runHooks(
69
78
 
70
79
  }
71
80
 
72
- }
73
-
74
- }
81
+ }
82
+
83
+ return currentExports;
84
+
85
+ }
75
86
 
76
87
  function patchLoaderOnce() {
77
88
 
@@ -98,12 +109,13 @@ function patchLoaderOnce() {
98
109
  arguments
99
110
  );
100
111
 
101
- runHooks(
102
- request,
103
- exports
104
- );
105
-
106
- return exports;
112
+ const patchedExports =
113
+ runHooks(
114
+ request,
115
+ exports
116
+ );
117
+
118
+ return patchedExports;
107
119
 
108
120
  };
109
121
 
@@ -137,11 +149,17 @@ function patchCached(
137
149
 
138
150
  if (cached?.exports) {
139
151
 
140
- hook(
141
- cached.exports
142
- );
143
-
144
- }
152
+ const replacement =
153
+ hook(
154
+ cached.exports
155
+ );
156
+
157
+ if (replacement !== undefined) {
158
+ cached.exports =
159
+ replacement;
160
+ }
161
+
162
+ }
145
163
 
146
164
  }
147
165
  catch { }
@@ -159,7 +177,7 @@ function tryRequire(
159
177
  require(moduleName);
160
178
 
161
179
  if (mod) {
162
- hook(mod);
180
+ hook(mod);
163
181
  }
164
182
 
165
183
  }
@@ -188,7 +206,7 @@ function retryPatch(
188
206
 
189
207
  if (mod) {
190
208
 
191
- hook(mod);
209
+ hook(mod);
192
210
 
193
211
  clearInterval(timer);
194
212
 
@@ -244,4 +262,4 @@ export const hookRequire =
244
262
  onRequire
245
263
  );
246
264
 
247
- };
265
+ };