@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,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
+ };
@@ -1,105 +1,202 @@
1
- import { Context } from '../core/context';
2
-
3
- export const instrumentMongo = (debug = false) => {
4
- try {
5
- const mongodb = require('mongodb');
6
- const Collection = mongodb.Collection;
7
-
8
- // Attempt to get Cursor classes
9
- // Note: The location of these classes varies by driver version,
10
- // checking common locations
11
- const FindCursor = mongodb.FindCursor || require('mongodb/lib/cursor/find_cursor').FindCursor;
12
- const AggregationCursor = mongodb.AggregationCursor || require('mongodb/lib/cursor/aggregation_cursor').AggregationCursor;
13
-
14
- if (debug) console.log('[Senzor] Instrumenting MongoDB (Collection + Cursors)...');
15
-
16
- // --- Helper to Record Span ---
17
- const recordSpan = (name: string, operation: string, collection: string, startAbs: number, traceStart: number, err?: Error) => {
18
- const duration = performance.now() - startAbs;
19
- Context.addSpan({
20
- name: `MongoDB ${name}`,
21
- type: 'db',
22
- startTime: performance.now() - traceStart - duration, // Adjust start time to when op actually started
23
- duration,
24
- status: err ? 500 : 0,
25
- meta: { collection, operation, error: err ? err.message : undefined }
26
- });
27
- if (debug) console.log(`[Senzor] Captured Mongo: ${name} (${duration.toFixed(2)}ms)`);
28
- };
29
-
30
- // --- 1. Instrument Immediate Operations (Insert/Update/Delete) ---
31
- const immediateMethods = ['insertOne', 'insertMany', 'updateOne', 'updateMany', 'deleteOne', 'deleteMany', 'countDocuments'];
32
-
33
- immediateMethods.forEach((method) => {
34
- if (!Collection.prototype[method]) return;
35
- const original = Collection.prototype[method];
36
-
37
- Collection.prototype[method] = function (...args: any[]) {
38
- const trace = Context.current();
39
- if (!trace) return original.apply(this, args);
40
-
41
- const spanStartAbs = performance.now();
42
- const traceStart = trace.startTime;
43
- const collName = this.collectionName;
44
-
45
- try {
46
- const result = original.apply(this, args);
47
- if (result && typeof result.then === 'function') {
48
- return result.then(
49
- (res: any) => { recordSpan(method, method, collName, spanStartAbs, traceStart); return res; },
50
- (err: any) => { recordSpan(method, method, collName, spanStartAbs, traceStart, err); throw err; }
51
- );
52
- }
53
- return result;
54
- } catch (err: any) {
55
- recordSpan(method, method, collName, spanStartAbs, traceStart, err);
56
- throw err;
57
- }
58
- };
59
- });
60
-
61
- // --- 2. Instrument Cursor Execution (find -> toArray) ---
62
- const patchCursor = (CursorClass: any, label: string) => {
63
- if (!CursorClass || !CursorClass.prototype.toArray) return;
64
-
65
- const originalToArray = CursorClass.prototype.toArray;
66
-
67
- CursorClass.prototype.toArray = function (...args: any[]) {
68
- const trace = Context.current();
69
- // Cursors are often created in context but executed later.
70
- // We check context at execution time.
71
- if (!trace) return originalToArray.apply(this, args);
72
-
73
- const spanStartAbs = performance.now();
74
- const traceStart = trace.startTime;
75
- // Attempt to get collection name from cursor internal state
76
- const collName = this.namespace?.collection || 'unknown';
77
-
78
- const onSuccess = (res: any) => {
79
- recordSpan(label, label, collName, spanStartAbs, traceStart);
80
- return res;
81
- };
82
- const onError = (err: any) => {
83
- recordSpan(label, label, collName, spanStartAbs, traceStart, err);
84
- throw err;
85
- };
86
-
87
- try {
88
- const result = originalToArray.apply(this, args);
89
- if (result && typeof result.then === 'function') {
90
- return result.then(onSuccess, onError);
91
- }
92
- return onSuccess(result);
93
- } catch (e) {
94
- onError(e);
95
- }
96
- };
97
- };
98
-
99
- patchCursor(FindCursor, 'find');
100
- patchCursor(AggregationCursor, 'aggregate');
101
-
102
- } catch (e: any) {
103
- if (debug) console.warn('[Senzor] MongoDB instrumentation warning:', e.message);
104
- }
105
- };
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 collectionName = (collection: any): string =>
7
+ collection?.collectionName ||
8
+ collection?.s?.namespace?.collection ||
9
+ collection?.namespace?.collection ||
10
+ 'unknown';
11
+
12
+ const databaseName = (collection: any): string | undefined =>
13
+ collection?.dbName ||
14
+ collection?.s?.namespace?.db ||
15
+ collection?.namespace?.db;
16
+
17
+ const cursorCollectionName = (cursor: any): string =>
18
+ cursor?.namespace?.collection ||
19
+ cursor?.ns?.collection ||
20
+ cursor?.cursorNamespace?.collection ||
21
+ 'unknown';
22
+
23
+ const patchCollectionMethod = (
24
+ proto: any,
25
+ method: string,
26
+ options?: SenzorOptions
27
+ ) => {
28
+ patchMethod(
29
+ proto,
30
+ method,
31
+ `senzor.mongodb.collection.${method}`,
32
+ (original) =>
33
+ function patchedMongoCollection(this: any, ...args: any[]) {
34
+ const collection = collectionName(this);
35
+ const span = startCapturedSpan(
36
+ `MongoDB ${method}`,
37
+ 'db',
38
+ {
39
+ collection,
40
+ operation: method,
41
+ 'db.system.name': 'mongodb',
42
+ 'db.collection.name': collection,
43
+ 'db.namespace': databaseName(this)
44
+ ? `${databaseName(this)}.${collection}`
45
+ : collection,
46
+ 'db.operation.name': method,
47
+ library: 'mongodb'
48
+ },
49
+ options
50
+ );
51
+
52
+ if (!span) return original.apply(this, args);
53
+
54
+ return runWithCapturedSpan(span, () => {
55
+ try {
56
+ const result = original.apply(this, args);
57
+ if (result && typeof result.then === 'function') {
58
+ return result.then(
59
+ (value: any) => {
60
+ span.end(0, {
61
+ matchedCount: value?.matchedCount,
62
+ modifiedCount: value?.modifiedCount,
63
+ deletedCount: value?.deletedCount,
64
+ insertedCount: value?.insertedCount
65
+ });
66
+ return value;
67
+ },
68
+ (error: any) => {
69
+ span.end(500, {
70
+ error: error?.message,
71
+ 'error.type': error?.name || 'Error'
72
+ });
73
+ throw error;
74
+ }
75
+ );
76
+ }
77
+
78
+ span.end(0);
79
+ return result;
80
+ } catch (error: any) {
81
+ span.end(500, {
82
+ error: error?.message,
83
+ 'error.type': error?.name || 'Error'
84
+ });
85
+ throw error;
86
+ }
87
+ });
88
+ }
89
+ );
90
+ };
91
+
92
+ const patchCursorMethod = (
93
+ proto: any,
94
+ method: string,
95
+ operation: string,
96
+ options?: SenzorOptions
97
+ ) => {
98
+ patchMethod(
99
+ proto,
100
+ method,
101
+ `senzor.mongodb.cursor.${operation}.${method}`,
102
+ (original) =>
103
+ function patchedMongoCursor(this: any, ...args: any[]) {
104
+ const collection = cursorCollectionName(this);
105
+ const span = startCapturedSpan(
106
+ `MongoDB ${operation}`,
107
+ 'db',
108
+ {
109
+ collection,
110
+ operation,
111
+ 'db.system.name': 'mongodb',
112
+ 'db.collection.name': collection,
113
+ 'db.operation.name': operation,
114
+ library: 'mongodb'
115
+ },
116
+ options
117
+ );
118
+
119
+ if (!span) return original.apply(this, args);
120
+
121
+ return runWithCapturedSpan(span, () => {
122
+ try {
123
+ const result = original.apply(this, args);
124
+ if (result && typeof result.then === 'function') {
125
+ return result.then(
126
+ (value: any) => {
127
+ span.end(0, {
128
+ resultCount: Array.isArray(value) ? value.length : undefined
129
+ });
130
+ return value;
131
+ },
132
+ (error: any) => {
133
+ span.end(500, {
134
+ error: error?.message,
135
+ 'error.type': error?.name || 'Error'
136
+ });
137
+ throw error;
138
+ }
139
+ );
140
+ }
141
+
142
+ span.end(0);
143
+ return result;
144
+ } catch (error: any) {
145
+ span.end(500, {
146
+ error: error?.message,
147
+ 'error.type': error?.name || 'Error'
148
+ });
149
+ throw error;
150
+ }
151
+ });
152
+ }
153
+ );
154
+ };
155
+
156
+ const patchMongo = (mongodb: any, options?: SenzorOptions) => {
157
+ const Collection = mongodb?.Collection || mongodb?.default?.Collection;
158
+ const collectionProto = Collection?.prototype;
159
+
160
+ [
161
+ 'insertOne',
162
+ 'insertMany',
163
+ 'updateOne',
164
+ 'updateMany',
165
+ 'replaceOne',
166
+ 'deleteOne',
167
+ 'deleteMany',
168
+ 'findOne',
169
+ 'findOneAndUpdate',
170
+ 'findOneAndDelete',
171
+ 'findOneAndReplace',
172
+ 'countDocuments',
173
+ 'estimatedDocumentCount',
174
+ 'distinct',
175
+ 'bulkWrite',
176
+ 'createIndex',
177
+ 'dropIndex'
178
+ ].forEach((method) =>
179
+ patchCollectionMethod(collectionProto, method, options)
180
+ );
181
+
182
+ const FindCursor =
183
+ mongodb?.FindCursor || mongodb?.default?.FindCursor;
184
+ const AggregationCursor =
185
+ mongodb?.AggregationCursor || mongodb?.default?.AggregationCursor;
186
+
187
+ ['toArray', 'next', 'forEach'].forEach((method) =>
188
+ patchCursorMethod(FindCursor?.prototype, method, 'find', options)
189
+ );
190
+ ['toArray', 'next', 'forEach'].forEach((method) =>
191
+ patchCursorMethod(
192
+ AggregationCursor?.prototype,
193
+ method,
194
+ 'aggregate',
195
+ options
196
+ )
197
+ );
198
+ };
199
+
200
+ export const instrumentMongo = (options?: SenzorOptions) => {
201
+ hookRequire('mongodb', (exports: any) => patchMongo(exports, options));
202
+ };
@@ -0,0 +1,156 @@
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 modelName = (target: any): string =>
7
+ target?.model?.modelName ||
8
+ target?.constructor?.modelName ||
9
+ target?.modelName ||
10
+ 'unknown';
11
+
12
+ const collectionName = (target: any): string =>
13
+ target?.mongooseCollection?.name ||
14
+ target?.collection?.name ||
15
+ target?.model?.collection?.name ||
16
+ 'unknown';
17
+
18
+ const patchExec = (
19
+ proto: any,
20
+ label: 'query' | 'aggregate',
21
+ options?: SenzorOptions
22
+ ) => {
23
+ patchMethod(
24
+ proto,
25
+ 'exec',
26
+ `senzor.mongoose.${label}.exec`,
27
+ (original) =>
28
+ function patchedMongooseExec(this: any, ...args: any[]) {
29
+ const operation =
30
+ String(this?.op || this?._op || label).toUpperCase();
31
+ const collection = collectionName(this);
32
+ const span = startCapturedSpan(
33
+ `Mongoose ${operation}`,
34
+ 'db',
35
+ {
36
+ collection,
37
+ model: modelName(this),
38
+ operation,
39
+ 'db.system.name': 'mongodb',
40
+ 'db.collection.name': collection,
41
+ 'db.operation.name': operation,
42
+ library: 'mongoose'
43
+ },
44
+ options
45
+ );
46
+
47
+ if (!span) return original.apply(this, args);
48
+
49
+ return runWithCapturedSpan(span, () => {
50
+ try {
51
+ const result = original.apply(this, args);
52
+ if (result && typeof result.then === 'function') {
53
+ return result.then(
54
+ (value: any) => {
55
+ span.end(0, {
56
+ resultCount: Array.isArray(value) ? value.length : undefined
57
+ });
58
+ return value;
59
+ },
60
+ (error: any) => {
61
+ span.end(500, {
62
+ error: error?.message,
63
+ 'error.type': error?.name || 'Error'
64
+ });
65
+ throw error;
66
+ }
67
+ );
68
+ }
69
+
70
+ span.end(0);
71
+ return result;
72
+ } catch (error: any) {
73
+ span.end(500, {
74
+ error: error?.message,
75
+ 'error.type': error?.name || 'Error'
76
+ });
77
+ throw error;
78
+ }
79
+ });
80
+ }
81
+ );
82
+ };
83
+
84
+ const patchSave = (modelProto: any, options?: SenzorOptions) => {
85
+ patchMethod(
86
+ modelProto,
87
+ 'save',
88
+ 'senzor.mongoose.model.save',
89
+ (original) =>
90
+ function patchedMongooseSave(this: any, ...args: any[]) {
91
+ const collection = collectionName(this);
92
+ const span = startCapturedSpan(
93
+ 'Mongoose SAVE',
94
+ 'db',
95
+ {
96
+ collection,
97
+ model: modelName(this),
98
+ operation: 'SAVE',
99
+ 'db.system.name': 'mongodb',
100
+ 'db.collection.name': collection,
101
+ 'db.operation.name': 'SAVE',
102
+ library: 'mongoose'
103
+ },
104
+ options
105
+ );
106
+
107
+ if (!span) return original.apply(this, args);
108
+
109
+ return runWithCapturedSpan(span, () => {
110
+ try {
111
+ const result = original.apply(this, args);
112
+ if (result && typeof result.then === 'function') {
113
+ return result.then(
114
+ (value: any) => {
115
+ span.end(0);
116
+ return value;
117
+ },
118
+ (error: any) => {
119
+ span.end(500, {
120
+ error: error?.message,
121
+ 'error.type': error?.name || 'Error'
122
+ });
123
+ throw error;
124
+ }
125
+ );
126
+ }
127
+
128
+ span.end(0);
129
+ return result;
130
+ } catch (error: any) {
131
+ span.end(500, {
132
+ error: error?.message,
133
+ 'error.type': error?.name || 'Error'
134
+ });
135
+ throw error;
136
+ }
137
+ });
138
+ }
139
+ );
140
+ };
141
+
142
+ const patchMongoose = (mongoose: any, options?: SenzorOptions) => {
143
+ patchExec(mongoose?.Query?.prototype, 'query', options);
144
+ patchExec(mongoose?.Aggregate?.prototype, 'aggregate', options);
145
+ patchSave(mongoose?.Model?.prototype, options);
146
+
147
+ if (mongoose?.default) {
148
+ patchExec(mongoose.default?.Query?.prototype, 'query', options);
149
+ patchExec(mongoose.default?.Aggregate?.prototype, 'aggregate', options);
150
+ patchSave(mongoose.default?.Model?.prototype, options);
151
+ }
152
+ };
153
+
154
+ export const instrumentMongoose = (options?: SenzorOptions) => {
155
+ hookRequire('mongoose', (exports: any) => patchMongoose(exports, options));
156
+ };