@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,169 @@
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.sql === 'string') return first.sql;
11
+ return undefined;
12
+ };
13
+
14
+ const patchSqlMethod = (
15
+ proto: any,
16
+ method: 'query' | 'execute',
17
+ library: string,
18
+ options?: SenzorOptions
19
+ ) => {
20
+ patchMethod(
21
+ proto,
22
+ method,
23
+ `senzor.${library}.${method}`,
24
+ (original) =>
25
+ function patchedMysqlMethod(this: any, ...args: any[]) {
26
+ const sql = extractSql(args);
27
+ const operation = getSqlOperation(sql) || method.toUpperCase();
28
+ const span = startCapturedSpan(
29
+ `MySQL ${operation}`,
30
+ 'db',
31
+ {
32
+ query: normalizeSql(sql, options),
33
+ operation,
34
+ 'db.system.name': 'mysql',
35
+ 'db.operation.name': operation,
36
+ 'db.query.text': normalizeSql(sql, options),
37
+ library
38
+ },
39
+ options
40
+ );
41
+
42
+ if (!span) return original.apply(this, args);
43
+
44
+ const callbackIndex = args.findIndex(
45
+ (arg) => typeof arg === 'function'
46
+ );
47
+ if (callbackIndex >= 0) {
48
+ const originalCallback = args[callbackIndex];
49
+ args[callbackIndex] = function wrappedMysqlCallback(
50
+ this: unknown,
51
+ err: any,
52
+ rows: any
53
+ ) {
54
+ span.end(err ? 500 : 0, {
55
+ error: err?.message,
56
+ 'error.type': err?.name,
57
+ rowCount: Array.isArray(rows) ? rows.length : undefined
58
+ });
59
+ return originalCallback.apply(this, arguments as any);
60
+ };
61
+ }
62
+
63
+ return runWithCapturedSpan(span, () => {
64
+ try {
65
+ const result = original.apply(this, args);
66
+
67
+ if (result && typeof result.then === 'function') {
68
+ return result.then(
69
+ (value: any) => {
70
+ const rows = Array.isArray(value) ? value[0] : value;
71
+ span.end(0, {
72
+ rowCount: Array.isArray(rows) ? rows.length : undefined
73
+ });
74
+ return value;
75
+ },
76
+ (error: any) => {
77
+ span.end(500, {
78
+ error: error?.message,
79
+ 'error.type': error?.name || 'Error'
80
+ });
81
+ throw error;
82
+ }
83
+ );
84
+ }
85
+
86
+ if (
87
+ callbackIndex < 0 &&
88
+ result &&
89
+ typeof result.once === 'function'
90
+ ) {
91
+ result.once('end', () => span.end(0));
92
+ result.once('error', (error: Error) =>
93
+ span.end(500, {
94
+ error: error.message,
95
+ 'error.type': error.name
96
+ })
97
+ );
98
+ } else if (callbackIndex < 0) {
99
+ span.end(0);
100
+ }
101
+
102
+ return result;
103
+ } catch (error: any) {
104
+ span.end(500, {
105
+ error: error?.message,
106
+ 'error.type': error?.name || 'Error'
107
+ });
108
+ throw error;
109
+ }
110
+ });
111
+ }
112
+ );
113
+ };
114
+
115
+ const patchKnownPrototypes = (
116
+ mysql: any,
117
+ library: string,
118
+ options?: SenzorOptions
119
+ ) => {
120
+ [
121
+ mysql?.Connection?.prototype,
122
+ mysql?.Pool?.prototype,
123
+ mysql?.PoolConnection?.prototype,
124
+ mysql?.PromiseConnection?.prototype,
125
+ mysql?.PromisePool?.prototype,
126
+ mysql?.default?.Connection?.prototype,
127
+ mysql?.default?.Pool?.prototype
128
+ ].forEach((proto) => {
129
+ patchSqlMethod(proto, 'query', library, options);
130
+ patchSqlMethod(proto, 'execute', library, options);
131
+ });
132
+ };
133
+
134
+ const patchFactories = (
135
+ mysql: any,
136
+ library: string,
137
+ options?: SenzorOptions
138
+ ) => {
139
+ ['createConnection', 'createPool'].forEach((factory) => {
140
+ patchMethod(
141
+ mysql,
142
+ factory,
143
+ `senzor.${library}.${factory}`,
144
+ (original) =>
145
+ function patchedMysqlFactory(this: any, ...args: any[]) {
146
+ const client = original.apply(this, args);
147
+ patchSqlMethod(client, 'query', library, options);
148
+ patchSqlMethod(client, 'execute', library, options);
149
+ patchSqlMethod(Object.getPrototypeOf(client), 'query', library, options);
150
+ patchSqlMethod(Object.getPrototypeOf(client), 'execute', library, options);
151
+ return client;
152
+ }
153
+ );
154
+ });
155
+ };
156
+
157
+ const patchMysql = (
158
+ mysql: any,
159
+ library: string,
160
+ options?: SenzorOptions
161
+ ) => {
162
+ patchKnownPrototypes(mysql, library, options);
163
+ patchFactories(mysql, library, options);
164
+ };
165
+
166
+ export const instrumentMysql = (options?: SenzorOptions) => {
167
+ hookRequire('mysql', (exports: any) => patchMysql(exports, 'mysql', options));
168
+ hookRequire('mysql2', (exports: any) => patchMysql(exports, 'mysql2', options));
169
+ };
@@ -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
+ };
@@ -1,41 +1,131 @@
1
- import { Context } from '../core/context';
2
-
3
- // Simple shim for 'pg' library
4
- export const instrumentPg = () => {
5
- try {
6
- // Try to require pg (it might not be installed by user)
7
- const pg = require('pg');
8
- const originalQuery = pg.Client.prototype.query;
9
-
10
- pg.Client.prototype.query = function (...args: any[]) {
11
- const trace = Context.current();
12
- if (!trace) return originalQuery.apply(this, args);
13
-
14
- const startTime = performance.now() - trace.startTime;
15
- const spanStartAbs = performance.now();
16
-
17
- // Extract SQL (first arg usually string or config object)
18
- const sql = typeof args[0] === 'string' ? args[0] : args[0].text;
19
-
20
- // Wrap callback if present, or handle Promise
21
- const result = originalQuery.apply(this, args);
22
-
23
- if (result && typeof result.then === 'function') {
24
- return result.then((res: any) => {
25
- const duration = performance.now() - spanStartAbs;
26
- Context.addSpan({
27
- name: 'Postgres Query',
28
- type: 'db',
29
- startTime,
30
- duration,
31
- meta: { query: sql }
32
- });
33
- return res;
34
- });
35
- }
36
- return result;
37
- };
38
- } catch (e) {
39
- // User doesn't use pg, ignore
40
- }
41
- };
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
+ };