@senzops/apm-node 1.1.18 → 1.2.0

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.
@@ -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
+ };
@@ -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
+ };