@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,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,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
+ }
@@ -0,0 +1,7 @@
1
+ import { randomUUID } from 'crypto';
2
+
3
+ export const generateTraceId = (): string =>
4
+ randomUUID().replace(/-/g, '');
5
+
6
+ export const generateSpanId = (): string =>
7
+ randomUUID().replace(/-/g, '').slice(0, 16);
@@ -0,0 +1 @@
1
+ export const SENZOR_INTERNAL_HEADER = 'x-senzor-sdk-internal';
@@ -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
+ };
@@ -1,6 +1,7 @@
1
- import { client } from '../core/client';
2
- import { getRoute } from '../core/normalizer';
3
- import { getClientIp } from '../utils/getClientIp';
1
+ import { client } from '../core/client';
2
+ import { getRoute } from '../core/normalizer';
3
+ import { invokeWithFrameworkSpan } from '../instrumentation/framework';
4
+ import { getClientIp } from '../utils/getClientIp';
4
5
 
5
6
  type EventHandler = (event: any) => any;
6
7
 
@@ -15,21 +16,44 @@ export const wrapH3 = (handler: EventHandler) => {
15
16
  ip: getClientIp(req),
16
17
  userAgent: req.headers['user-agent'],
17
18
  headers: req.headers // Pass headers
18
- }, async () => {
19
- try {
20
- const response = await handler(event);
21
- let status = 200;
22
- if (event.node.res.statusCode) status = event.node.res.statusCode;
23
- if (response && response.statusCode) status = response.statusCode;
24
-
25
- client.endTrace(status, { route: getRoute(event, path) });
26
- return response;
27
- } catch (err: any) {
28
- client.captureError(err);
29
- const status = err.statusCode || err.status || 500;
19
+ }, async () => {
20
+ try {
21
+ const route = getRoute(event, path);
22
+ const response = await invokeWithFrameworkSpan(
23
+ handler,
24
+ undefined,
25
+ [event],
26
+ {
27
+ framework: 'h3',
28
+ type: 'event_handler',
29
+ name: `h3.event_handler ${req.method || 'GET'} ${route}`,
30
+ route,
31
+ method: req.method || 'GET',
32
+ request: req,
33
+ response: event.node.res,
34
+ attributes: {
35
+ 'h3.type': 'event_handler',
36
+ 'http.route': route
37
+ }
38
+ },
39
+ undefined,
40
+ {
41
+ callbackCompletesSpan: false,
42
+ responseEndsSpan: false
43
+ }
44
+ );
45
+ let status = 200;
46
+ if (event.node.res.statusCode) status = event.node.res.statusCode;
47
+ if (response && response.statusCode) status = response.statusCode;
48
+
49
+ client.endTrace(status, { route });
50
+ return response;
51
+ } catch (err: any) {
52
+ client.captureError(err);
53
+ const status = err.statusCode || err.status || 500;
30
54
  client.endTrace(status, { route: getRoute(event, path) });
31
55
  throw err;
32
56
  }
33
57
  });
34
58
  };
35
- };
59
+ };
@@ -1,6 +1,7 @@
1
- import { client } from '../core/client';
2
- import { normalizePath } from '../core/normalizer';
3
- import { getClientIp } from '../utils/getClientIp';
1
+ import { client } from '../core/client';
2
+ import { normalizePath } from '../core/normalizer';
3
+ import { invokeWithFrameworkSpan } from '../instrumentation/framework';
4
+ import { getClientIp } from '../utils/getClientIp';
4
5
 
5
6
  // --- App Router Wrapper ---
6
7
  export const wrapNextRoute = (handler: Function) => {
@@ -37,16 +38,39 @@ export const wrapNextRoute = (handler: Function) => {
37
38
  userAgent: ua,
38
39
  ip: ip || getClientIp(req),
39
40
  headers: headers // Pass extracted headers
40
- }, async () => {
41
- try {
42
- const response = await handler(req, context);
43
- const status = response?.status || 200;
44
-
45
- client.endTrace(status, { route: normalizePath(url.pathname) });
46
- return response;
47
- } catch (err: any) {
48
- client.captureError(err);
49
- client.endTrace(500, { route: normalizePath(url.pathname) });
41
+ }, async () => {
42
+ try {
43
+ const route = normalizePath(url.pathname);
44
+ const response = await invokeWithFrameworkSpan(
45
+ handler,
46
+ undefined,
47
+ [req, context],
48
+ {
49
+ framework: 'next',
50
+ type: 'route_handler',
51
+ name: `next.app_route_handler ${method} ${route}`,
52
+ route,
53
+ method,
54
+ request: req,
55
+ attributes: {
56
+ 'next.router': 'app',
57
+ 'http.route': route,
58
+ 'url.path': url.pathname
59
+ }
60
+ },
61
+ undefined,
62
+ {
63
+ callbackCompletesSpan: false,
64
+ responseEndsSpan: false
65
+ }
66
+ );
67
+ const status = response?.status || 200;
68
+
69
+ client.endTrace(status, { route });
70
+ return response;
71
+ } catch (err: any) {
72
+ client.captureError(err);
73
+ client.endTrace(500, { route: normalizePath(url.pathname) });
50
74
  throw err;
51
75
  }
52
76
  });
@@ -71,14 +95,37 @@ export const wrapNextPages = (handler: Function) => {
71
95
  };
72
96
 
73
97
  res.once('finish', done);
74
- res.once('close', done);
75
-
76
- try {
77
- return await handler(req, res);
78
- } catch (e: any) {
79
- client.captureError(e);
80
- throw e;
98
+ res.once('close', done);
99
+
100
+ try {
101
+ const route = normalizePath(path);
102
+ return await invokeWithFrameworkSpan(
103
+ handler,
104
+ undefined,
105
+ [req, res],
106
+ {
107
+ framework: 'next',
108
+ type: 'route_handler',
109
+ name: `next.pages_api_handler ${req.method || 'GET'} ${route}`,
110
+ route,
111
+ method: req.method || 'GET',
112
+ request: req,
113
+ response: res,
114
+ attributes: {
115
+ 'next.router': 'pages',
116
+ 'http.route': route
117
+ }
118
+ },
119
+ undefined,
120
+ {
121
+ callbackCompletesSpan: false,
122
+ responseEndsSpan: true
123
+ }
124
+ );
125
+ } catch (e: any) {
126
+ client.captureError(e);
127
+ throw e;
81
128
  }
82
129
  });
83
130
  };
84
- };
131
+ };
package/tsup.config.ts CHANGED
@@ -1,11 +1,21 @@
1
- import { defineConfig } from 'tsup';
2
-
3
- export default defineConfig({
4
- entry: ['src/index.ts'],
5
- format: ['cjs', 'esm'],
6
- dts: true,
7
- clean: true,
8
- minify: true,
9
- sourcemap: true,
10
- splitting: false,
11
- });
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig([
4
+ {
5
+ entry: ['src/index.ts', 'src/register.ts'],
6
+ format: ['cjs', 'esm'],
7
+ dts: true,
8
+ clean: true,
9
+ minify: true,
10
+ sourcemap: true,
11
+ splitting: false,
12
+ },
13
+ {
14
+ entry: ['src/index.ts'],
15
+ format: ['iife'],
16
+ clean: false,
17
+ minify: true,
18
+ sourcemap: true,
19
+ splitting: false,
20
+ }
21
+ ]);