@senzops/apm-node 1.2.0 → 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.
@@ -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
+ };
@@ -0,0 +1,173 @@
1
+ import { normalizePath } from '../core/normalizer';
2
+ import { SenzorOptions } from '../core/types';
3
+ import { hookRequire } from './hook';
4
+ import { patchMethod } from './patch';
5
+ import { wrapFrameworkHandlerWithArity } from './framework';
6
+
7
+ const routerMethods = [
8
+ 'all',
9
+ 'del',
10
+ 'delete',
11
+ 'get',
12
+ 'head',
13
+ 'options',
14
+ 'patch',
15
+ 'post',
16
+ 'put'
17
+ ];
18
+
19
+ const stringifyPath = (value: unknown): string | undefined => {
20
+ if (typeof value === 'string') return value;
21
+ if (value instanceof RegExp) return value.toString();
22
+ if (Array.isArray(value)) {
23
+ return value.map(stringifyPath).filter(Boolean).join(',');
24
+ }
25
+ return undefined;
26
+ };
27
+
28
+ const getPathFromArgs = (args: any[]): string | undefined => {
29
+ for (const arg of args) {
30
+ if (typeof arg === 'function') return undefined;
31
+ const path = stringifyPath(arg);
32
+ if (path) return path;
33
+ }
34
+
35
+ return undefined;
36
+ };
37
+
38
+ const wrapKoaMiddleware = (
39
+ middleware: any,
40
+ options?: SenzorOptions,
41
+ layerPath?: string,
42
+ layerType: 'middleware' | 'router' | 'route_handler' = 'middleware',
43
+ method?: string
44
+ ) => {
45
+ if (typeof middleware !== 'function') return middleware;
46
+
47
+ return wrapFrameworkHandlerWithArity(
48
+ middleware,
49
+ (_thisArg, args) => {
50
+ const ctx = args[0];
51
+ const route =
52
+ ctx?._matchedRoute ||
53
+ ctx?.matched?.[0]?.path ||
54
+ layerPath ||
55
+ normalizePath(ctx?.path || ctx?.request?.path || '/');
56
+ const actualMethod = method || ctx?.method || ctx?.request?.method;
57
+ const handlerName = middleware.name || layerType;
58
+
59
+ return {
60
+ framework: 'koa',
61
+ type: layerType,
62
+ name:
63
+ layerType === 'route_handler'
64
+ ? `koa.request_handler ${actualMethod || ''} ${route}`.trim()
65
+ : `koa.${layerType} ${route || handlerName}`,
66
+ route,
67
+ method: actualMethod,
68
+ layerPath,
69
+ handlerName,
70
+ request: ctx?.req || ctx?.request,
71
+ response: ctx?.res || ctx?.response,
72
+ attributes: {
73
+ 'koa.type': layerType,
74
+ 'http.route': route,
75
+ path: ctx?.path || ctx?.request?.path
76
+ }
77
+ };
78
+ },
79
+ options,
80
+ {
81
+ callbackCompletesSpan: false,
82
+ responseEndsSpan: false
83
+ }
84
+ );
85
+ };
86
+
87
+ const patchKoaApplication = (
88
+ koa: any,
89
+ options?: SenzorOptions
90
+ ) => {
91
+ const proto = koa?.prototype || koa?.default?.prototype;
92
+ if (!proto) return;
93
+
94
+ patchMethod(
95
+ proto,
96
+ 'use',
97
+ 'senzor.koa.application.use',
98
+ (original) =>
99
+ function patchedKoaUse(this: any, middleware: any) {
100
+ return original.call(
101
+ this,
102
+ wrapKoaMiddleware(middleware, options, undefined, 'middleware')
103
+ );
104
+ }
105
+ );
106
+ };
107
+
108
+ const patchKoaRouter = (
109
+ routerModule: any,
110
+ options?: SenzorOptions
111
+ ) => {
112
+ const Router =
113
+ routerModule?.Router ||
114
+ routerModule?.default ||
115
+ routerModule;
116
+ const proto = Router?.prototype;
117
+ if (!proto) return;
118
+
119
+ patchMethod(
120
+ proto,
121
+ 'use',
122
+ 'senzor.koa.router.use',
123
+ (original) =>
124
+ function patchedKoaRouterUse(this: any, ...args: any[]) {
125
+ const layerPath = getPathFromArgs(args);
126
+ const nextArgs = args.map((arg) =>
127
+ typeof arg === 'function'
128
+ ? wrapKoaMiddleware(arg, options, layerPath, 'router')
129
+ : arg
130
+ );
131
+ return original.apply(this, nextArgs);
132
+ }
133
+ );
134
+
135
+ for (const method of routerMethods) {
136
+ patchMethod(
137
+ proto,
138
+ method,
139
+ `senzor.koa.router.${method}`,
140
+ (original) =>
141
+ function patchedKoaRouterMethod(this: any, ...args: any[]) {
142
+ const layerPath = getPathFromArgs(args);
143
+ const nextArgs = args.map((arg) =>
144
+ typeof arg === 'function'
145
+ ? wrapKoaMiddleware(
146
+ arg,
147
+ options,
148
+ layerPath,
149
+ 'route_handler',
150
+ method.toUpperCase()
151
+ )
152
+ : arg
153
+ );
154
+
155
+ return original.apply(this, nextArgs);
156
+ }
157
+ );
158
+ }
159
+ };
160
+
161
+ export const instrumentKoa = (options?: SenzorOptions) => {
162
+ hookRequire('koa', (exports: any) => {
163
+ patchKoaApplication(exports, options);
164
+ });
165
+
166
+ hookRequire('@koa/router', (exports: any) => {
167
+ patchKoaRouter(exports, options);
168
+ });
169
+
170
+ hookRequire('koa-router', (exports: any) => {
171
+ patchKoaRouter(exports, options);
172
+ });
173
+ };
package/src/register.ts CHANGED
@@ -31,6 +31,22 @@ const options = {
31
31
  captureHeaders: truthy(process.env.SENZOR_CAPTURE_HEADERS),
32
32
  captureDbStatement:
33
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'
34
50
  ? false
35
51
  : undefined
36
52
  };
@@ -1,11 +1,14 @@
1
- import { client } from '../core/client';
2
- import { SenzorOptions } from '../core/types';
3
- import { getClientIp } from '../utils/getClientIp';
1
+ import { client } from '../core/client';
2
+ import { SenzorOptions } from '../core/types';
3
+ import { instrumentFastifyInstance } from '../instrumentation/fastify';
4
+ import { getClientIp } from '../utils/getClientIp';
4
5
 
5
6
  export const senzorPlugin = (fastify: any, options: SenzorOptions, done: Function) => {
6
- if (options && options.apiKey) {
7
- client.init(options);
8
- }
7
+ if (options && options.apiKey) {
8
+ client.init(options);
9
+ }
10
+
11
+ instrumentFastifyInstance(fastify, options);
9
12
 
10
13
  fastify.addHook('onRequest', (request: any, reply: any, next: Function) => {
11
14
  client.startTrace({
@@ -29,4 +32,4 @@ export const senzorPlugin = (fastify: any, options: SenzorOptions, done: Functio
29
32
  });
30
33
 
31
34
  done();
32
- };
35
+ };