@sentienguard/apm 1.0.8 → 1.0.10
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.
- package/README.md +141 -141
- package/package.json +12 -2
- package/src/aggregator.js +465 -463
- package/src/browser/instrumentation.js +8 -3
- package/src/config.js +40 -0
- package/src/dependencies.js +374 -231
- package/src/errors.js +132 -132
- package/src/index.d.ts +113 -102
- package/src/index.js +233 -225
- package/src/instrumentation.js +10 -0
- package/src/mongodb.js +579 -397
- package/src/normalizer.js +9 -2
- package/src/openai.js +520 -520
- package/src/spanExporter.js +232 -0
- package/src/tracing.js +114 -0
package/src/mongodb.js
CHANGED
|
@@ -1,397 +1,579 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MongoDB Instrumentation Module
|
|
3
|
-
*
|
|
4
|
-
* Hooks into MongoDB driver's command monitoring events to track:
|
|
5
|
-
* - Query operations (find, insert, update, delete, aggregate)
|
|
6
|
-
* - Latency and error rates per collection
|
|
7
|
-
* - Connection pool statistics
|
|
8
|
-
* - Slow query detection
|
|
9
|
-
*
|
|
10
|
-
* Uses MongoDB driver's built-in APM events (no monkey-patching):
|
|
11
|
-
* - commandStarted: Record start time
|
|
12
|
-
* - commandSucceeded: Calculate latency, record success
|
|
13
|
-
* - commandFailed: Record failure with error info
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import config, { debug, warn } from './config.js';
|
|
17
|
-
import { getAggregator } from './aggregator.js';
|
|
18
|
-
|
|
19
|
-
// Track pending commands by requestId
|
|
20
|
-
const pendingCommands = new Map();
|
|
21
|
-
|
|
22
|
-
// Track
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
return
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (name.includes('
|
|
91
|
-
|
|
92
|
-
return
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
poolStats.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
1
|
+
/**
|
|
2
|
+
* MongoDB Instrumentation Module
|
|
3
|
+
*
|
|
4
|
+
* Hooks into MongoDB driver's command monitoring events to track:
|
|
5
|
+
* - Query operations (find, insert, update, delete, aggregate)
|
|
6
|
+
* - Latency and error rates per collection
|
|
7
|
+
* - Connection pool statistics
|
|
8
|
+
* - Slow query detection
|
|
9
|
+
*
|
|
10
|
+
* Uses MongoDB driver's built-in APM events (no monkey-patching):
|
|
11
|
+
* - commandStarted: Record start time
|
|
12
|
+
* - commandSucceeded: Calculate latency, record success
|
|
13
|
+
* - commandFailed: Record failure with error info
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import config, { debug, warn } from './config.js';
|
|
17
|
+
import { getAggregator } from './aggregator.js';
|
|
18
|
+
|
|
19
|
+
// Track pending commands by requestId
|
|
20
|
+
const pendingCommands = new Map();
|
|
21
|
+
|
|
22
|
+
// Track whether we've already instrumented to avoid double-attaching
|
|
23
|
+
let isInstrumented = false;
|
|
24
|
+
|
|
25
|
+
// Deferred detection timer (so we can clean it up on shutdown)
|
|
26
|
+
let detectionTimer = null;
|
|
27
|
+
|
|
28
|
+
// Track connection pool stats via CMAP events
|
|
29
|
+
const poolStats = {
|
|
30
|
+
totalCreated: 0,
|
|
31
|
+
totalClosed: 0,
|
|
32
|
+
checkedOut: 0,
|
|
33
|
+
checkedIn: 0,
|
|
34
|
+
waitQueueSize: 0
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Operations we want to track (skip admin/internal commands)
|
|
38
|
+
const TRACKED_OPERATIONS = new Set([
|
|
39
|
+
'find', 'findOne', 'findAndModify',
|
|
40
|
+
'insert', 'insertOne', 'insertMany',
|
|
41
|
+
'update', 'updateOne', 'updateMany',
|
|
42
|
+
'delete', 'deleteOne', 'deleteMany',
|
|
43
|
+
'aggregate', 'count', 'countDocuments', 'estimatedDocumentCount',
|
|
44
|
+
'distinct', 'mapReduce',
|
|
45
|
+
'createIndexes', 'dropIndexes',
|
|
46
|
+
'bulkWrite'
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
// Commands to ignore (internal/admin)
|
|
50
|
+
const IGNORED_COMMANDS = new Set([
|
|
51
|
+
'hello', 'ismaster', 'isMaster', 'buildInfo', 'getLastError',
|
|
52
|
+
'ping', 'serverStatus', 'replSetGetStatus', 'hostInfo',
|
|
53
|
+
'listCollections', 'listIndexes', 'collStats', 'dbStats',
|
|
54
|
+
'saslStart', 'saslContinue', 'getnonce', 'authenticate'
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
let mongoClient = null;
|
|
58
|
+
let poolStatsInterval = null;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract collection name from command
|
|
62
|
+
*/
|
|
63
|
+
function getCollectionName(command) {
|
|
64
|
+
// Most commands have collection name as the value of the command key
|
|
65
|
+
const commandName = Object.keys(command)[0];
|
|
66
|
+
const value = command[commandName];
|
|
67
|
+
|
|
68
|
+
// For aggregate, the collection is the value
|
|
69
|
+
if (typeof value === 'string' && value.length > 0 && value !== '1') {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check common collection name fields
|
|
74
|
+
if (command.collection) return command.collection;
|
|
75
|
+
if (command.ns) {
|
|
76
|
+
const parts = command.ns.split('.');
|
|
77
|
+
return parts.length > 1 ? parts.slice(1).join('.') : parts[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return 'unknown';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalize operation name
|
|
85
|
+
*/
|
|
86
|
+
function normalizeOperation(commandName) {
|
|
87
|
+
const name = commandName.toLowerCase();
|
|
88
|
+
|
|
89
|
+
// Group similar operations
|
|
90
|
+
if (name.includes('find')) return 'find';
|
|
91
|
+
if (name.includes('insert')) return 'insert';
|
|
92
|
+
if (name.includes('update')) return 'update';
|
|
93
|
+
if (name.includes('delete') || name.includes('remove')) return 'delete';
|
|
94
|
+
if (name === 'aggregate') return 'aggregate';
|
|
95
|
+
if (name.includes('count')) return 'count';
|
|
96
|
+
if (name.includes('index')) return 'index';
|
|
97
|
+
|
|
98
|
+
return name;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle command started event
|
|
103
|
+
*/
|
|
104
|
+
function handleCommandStarted(event) {
|
|
105
|
+
if (!config.mongodb.enabled) return;
|
|
106
|
+
|
|
107
|
+
const commandName = event.commandName;
|
|
108
|
+
|
|
109
|
+
// Skip ignored commands
|
|
110
|
+
if (IGNORED_COMMANDS.has(commandName)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Store start time for latency calculation
|
|
115
|
+
pendingCommands.set(event.requestId, {
|
|
116
|
+
startTime: process.hrtime.bigint(),
|
|
117
|
+
commandName,
|
|
118
|
+
collection: getCollectionName(event.command),
|
|
119
|
+
databaseName: event.databaseName
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
debug(`MongoDB command started: ${commandName} on ${event.databaseName}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Handle command succeeded event
|
|
127
|
+
*/
|
|
128
|
+
function handleCommandSucceeded(event) {
|
|
129
|
+
if (!config.mongodb.enabled) return;
|
|
130
|
+
|
|
131
|
+
const pending = pendingCommands.get(event.requestId);
|
|
132
|
+
if (!pending) return;
|
|
133
|
+
|
|
134
|
+
pendingCommands.delete(event.requestId);
|
|
135
|
+
|
|
136
|
+
// Calculate latency in milliseconds
|
|
137
|
+
const latencyNs = process.hrtime.bigint() - pending.startTime;
|
|
138
|
+
const latencyMs = Number(latencyNs) / 1e6;
|
|
139
|
+
|
|
140
|
+
// Record the operation
|
|
141
|
+
const aggregator = getAggregator();
|
|
142
|
+
const operation = normalizeOperation(pending.commandName);
|
|
143
|
+
|
|
144
|
+
aggregator.recordMongoOperation(
|
|
145
|
+
pending.collection,
|
|
146
|
+
operation,
|
|
147
|
+
latencyMs,
|
|
148
|
+
false // not an error
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Check for slow query
|
|
152
|
+
if (latencyMs > config.mongodb.slowQueryMs) {
|
|
153
|
+
aggregator.recordSlowQuery(
|
|
154
|
+
pending.collection,
|
|
155
|
+
operation,
|
|
156
|
+
latencyMs
|
|
157
|
+
);
|
|
158
|
+
debug(`Slow MongoDB query: ${operation} on ${pending.collection} took ${latencyMs.toFixed(2)}ms`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
debug(`MongoDB command succeeded: ${pending.commandName} (${latencyMs.toFixed(2)}ms)`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Handle command failed event
|
|
166
|
+
*/
|
|
167
|
+
function handleCommandFailed(event) {
|
|
168
|
+
if (!config.mongodb.enabled) return;
|
|
169
|
+
|
|
170
|
+
const pending = pendingCommands.get(event.requestId);
|
|
171
|
+
if (!pending) return;
|
|
172
|
+
|
|
173
|
+
pendingCommands.delete(event.requestId);
|
|
174
|
+
|
|
175
|
+
// Calculate latency in milliseconds
|
|
176
|
+
const latencyNs = process.hrtime.bigint() - pending.startTime;
|
|
177
|
+
const latencyMs = Number(latencyNs) / 1e6;
|
|
178
|
+
|
|
179
|
+
// Record the failed operation
|
|
180
|
+
const aggregator = getAggregator();
|
|
181
|
+
const operation = normalizeOperation(pending.commandName);
|
|
182
|
+
|
|
183
|
+
aggregator.recordMongoOperation(
|
|
184
|
+
pending.collection,
|
|
185
|
+
operation,
|
|
186
|
+
latencyMs,
|
|
187
|
+
true // is an error
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
warn(`MongoDB command failed: ${pending.commandName} - ${event.failure?.message || 'Unknown error'}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* CMAP Event Handlers for Connection Pool Monitoring
|
|
195
|
+
*/
|
|
196
|
+
function handleConnectionCreated(event) {
|
|
197
|
+
poolStats.totalCreated++;
|
|
198
|
+
debug(`MongoDB connection created (total: ${poolStats.totalCreated})`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function handleConnectionClosed(event) {
|
|
202
|
+
poolStats.totalClosed++;
|
|
203
|
+
debug(`MongoDB connection closed (total closed: ${poolStats.totalClosed})`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function handleConnectionCheckedOut(event) {
|
|
207
|
+
poolStats.checkedOut++;
|
|
208
|
+
debug(`MongoDB connection checked out (active: ${poolStats.checkedOut - poolStats.checkedIn})`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleConnectionCheckedIn(event) {
|
|
212
|
+
poolStats.checkedIn++;
|
|
213
|
+
debug(`MongoDB connection checked in (active: ${poolStats.checkedOut - poolStats.checkedIn})`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function handleConnectionCheckOutStarted(event) {
|
|
217
|
+
poolStats.waitQueueSize++;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function handleConnectionCheckOutFailed(event) {
|
|
221
|
+
poolStats.waitQueueSize = Math.max(0, poolStats.waitQueueSize - 1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleConnectionPoolCreated(event) {
|
|
225
|
+
debug(`MongoDB connection pool created: ${event.address}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Collect connection pool statistics
|
|
230
|
+
* Now uses tracked CMAP events for accurate stats
|
|
231
|
+
*/
|
|
232
|
+
function collectPoolStats() {
|
|
233
|
+
if (!mongoClient || !config.mongodb.enabled) return;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const aggregator = getAggregator();
|
|
237
|
+
|
|
238
|
+
// Calculate current pool state from tracked events
|
|
239
|
+
const totalConnections = poolStats.totalCreated - poolStats.totalClosed;
|
|
240
|
+
const activeConnections = poolStats.checkedOut - poolStats.checkedIn;
|
|
241
|
+
const idleConnections = Math.max(0, totalConnections - activeConnections);
|
|
242
|
+
|
|
243
|
+
// Reset wait queue after reporting (it's instantaneous)
|
|
244
|
+
const currentWaitQueue = poolStats.waitQueueSize;
|
|
245
|
+
poolStats.waitQueueSize = 0;
|
|
246
|
+
|
|
247
|
+
aggregator.updatePoolStats({
|
|
248
|
+
active: activeConnections,
|
|
249
|
+
idle: idleConnections,
|
|
250
|
+
waitQueueSize: currentWaitQueue,
|
|
251
|
+
total: totalConnections
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
debug(`MongoDB pool stats: active=${activeConnections}, idle=${idleConnections}, total=${totalConnections}, waitQueue=${currentWaitQueue}`);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
debug(`Failed to collect pool stats: ${err.message}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Try to detect mongoose from Node.js module cache.
|
|
262
|
+
* Works for both require() and import usage since ESM modules
|
|
263
|
+
* also populate require.cache via the CJS interop layer.
|
|
264
|
+
*/
|
|
265
|
+
function tryDetectMongoose() {
|
|
266
|
+
try {
|
|
267
|
+
// Strategy 1: Scan require.cache for mongoose
|
|
268
|
+
const cache = require.cache || {};
|
|
269
|
+
const cacheKeys = Object.keys(cache);
|
|
270
|
+
const mongooseKey = cacheKeys.find(
|
|
271
|
+
k => /[\\/]mongoose[\\/](?:lib[\\/])?index\.js$/.test(k) &&
|
|
272
|
+
!k.includes('node_modules/mongoose/node_modules')
|
|
273
|
+
);
|
|
274
|
+
if (mongooseKey && cache[mongooseKey]?.exports) {
|
|
275
|
+
const mod = cache[mongooseKey].exports;
|
|
276
|
+
// Verify it's actually mongoose (has connection property)
|
|
277
|
+
if (mod.connection) {
|
|
278
|
+
debug('Detected mongoose via require.cache');
|
|
279
|
+
return mod;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
// require.cache not available (unlikely in Node.js)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Strategy 2: Fallback — check globalThis (in case user set it manually)
|
|
287
|
+
try {
|
|
288
|
+
if (globalThis.mongoose?.connection) {
|
|
289
|
+
debug('Detected mongoose via globalThis');
|
|
290
|
+
return globalThis.mongoose;
|
|
291
|
+
}
|
|
292
|
+
} catch (e) { /* not available */ }
|
|
293
|
+
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Try to enable monitorCommands on a MongoDB client.
|
|
299
|
+
* MongoDB driver v4.0+ requires this option for command events to fire.
|
|
300
|
+
* Returns true if monitorCommands is (or was made) enabled.
|
|
301
|
+
*/
|
|
302
|
+
function ensureMonitorCommands(client) {
|
|
303
|
+
try {
|
|
304
|
+
// Check if already enabled
|
|
305
|
+
const alreadyEnabled =
|
|
306
|
+
client.options?.monitorCommands ||
|
|
307
|
+
client.s?.options?.monitorCommands;
|
|
308
|
+
|
|
309
|
+
if (alreadyEnabled) {
|
|
310
|
+
debug('monitorCommands already enabled');
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Try to enable it on the client options
|
|
315
|
+
let enabled = false;
|
|
316
|
+
|
|
317
|
+
if (client.options && typeof client.options === 'object') {
|
|
318
|
+
try {
|
|
319
|
+
client.options.monitorCommands = true;
|
|
320
|
+
enabled = true;
|
|
321
|
+
} catch (e) { /* frozen or read-only */ }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Also try internal options (some driver versions store config here)
|
|
325
|
+
if (client.s?.options && typeof client.s.options === 'object') {
|
|
326
|
+
try {
|
|
327
|
+
client.s.options.monitorCommands = true;
|
|
328
|
+
enabled = true;
|
|
329
|
+
} catch (e) { /* frozen or read-only */ }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (enabled) {
|
|
333
|
+
debug('Enabled monitorCommands on MongoDB client');
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Could not enable — warn prominently
|
|
338
|
+
warn(
|
|
339
|
+
'MongoDB client does not have monitorCommands enabled. ' +
|
|
340
|
+
'Command monitoring events will not fire. ' +
|
|
341
|
+
'For full MongoDB monitoring, connect with: mongoose.connect(uri, { monitorCommands: true })'
|
|
342
|
+
);
|
|
343
|
+
return false;
|
|
344
|
+
} catch (err) {
|
|
345
|
+
debug(`Error checking monitorCommands: ${err.message}`);
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Attach instrumentation to a detected mongoose instance.
|
|
352
|
+
* Handles waiting for the connection to be ready.
|
|
353
|
+
*/
|
|
354
|
+
function attachToMongoose(mongoose) {
|
|
355
|
+
if (isInstrumented) {
|
|
356
|
+
debug('MongoDB already instrumented, skipping');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (mongoose.connection.readyState === 1) {
|
|
361
|
+
// Already connected
|
|
362
|
+
instrumentMongoDB(mongoose);
|
|
363
|
+
} else if (mongoose.connection.readyState === 2) {
|
|
364
|
+
// Connecting — wait for it
|
|
365
|
+
mongoose.connection.once('connected', () => {
|
|
366
|
+
instrumentMongoDB(mongoose);
|
|
367
|
+
});
|
|
368
|
+
debug('Waiting for Mongoose connection to complete before instrumenting');
|
|
369
|
+
} else {
|
|
370
|
+
// Not connected yet — listen for future connection
|
|
371
|
+
mongoose.connection.once('connected', () => {
|
|
372
|
+
instrumentMongoDB(mongoose);
|
|
373
|
+
});
|
|
374
|
+
debug('Mongoose not yet connecting, will instrument when connected');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Instrument a MongoDB client or Mongoose connection
|
|
380
|
+
*
|
|
381
|
+
* @param {MongoClient|Mongoose} clientOrMongoose - MongoDB client or Mongoose instance
|
|
382
|
+
*/
|
|
383
|
+
export function instrumentMongoDB(clientOrMongoose) {
|
|
384
|
+
if (!config.mongodb.enabled) {
|
|
385
|
+
debug('MongoDB instrumentation disabled');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (isInstrumented) {
|
|
390
|
+
debug('MongoDB already instrumented, skipping');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
let client;
|
|
396
|
+
|
|
397
|
+
// Check if it's Mongoose
|
|
398
|
+
if (clientOrMongoose?.connection?.getClient) {
|
|
399
|
+
client = clientOrMongoose.connection.getClient();
|
|
400
|
+
debug('Detected Mongoose, getting underlying MongoDB client');
|
|
401
|
+
} else if (clientOrMongoose?.getClient) {
|
|
402
|
+
// Mongoose connection object
|
|
403
|
+
client = clientOrMongoose.getClient();
|
|
404
|
+
} else if (typeof clientOrMongoose?.on === 'function') {
|
|
405
|
+
// Native MongoDB client
|
|
406
|
+
client = clientOrMongoose;
|
|
407
|
+
} else {
|
|
408
|
+
warn('Unable to instrument MongoDB: unrecognized client type');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!client) {
|
|
413
|
+
warn('MongoDB client not available for instrumentation');
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Ensure monitorCommands is enabled (required for command events in driver v4+)
|
|
418
|
+
ensureMonitorCommands(client);
|
|
419
|
+
|
|
420
|
+
// Store reference for pool stats
|
|
421
|
+
mongoClient = client;
|
|
422
|
+
isInstrumented = true;
|
|
423
|
+
|
|
424
|
+
// Stop detection timer if still running
|
|
425
|
+
if (detectionTimer) {
|
|
426
|
+
clearInterval(detectionTimer);
|
|
427
|
+
detectionTimer = null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Attach command monitoring listeners
|
|
431
|
+
client.on('commandStarted', handleCommandStarted);
|
|
432
|
+
client.on('commandSucceeded', handleCommandSucceeded);
|
|
433
|
+
client.on('commandFailed', handleCommandFailed);
|
|
434
|
+
|
|
435
|
+
// Attach CMAP (Connection Pool Monitoring) event listeners
|
|
436
|
+
client.on('connectionPoolCreated', handleConnectionPoolCreated);
|
|
437
|
+
client.on('connectionCreated', handleConnectionCreated);
|
|
438
|
+
client.on('connectionClosed', handleConnectionClosed);
|
|
439
|
+
client.on('connectionCheckedOut', handleConnectionCheckedOut);
|
|
440
|
+
client.on('connectionCheckedIn', handleConnectionCheckedIn);
|
|
441
|
+
client.on('connectionCheckOutStarted', handleConnectionCheckOutStarted);
|
|
442
|
+
client.on('connectionCheckOutFailed', handleConnectionCheckOutFailed);
|
|
443
|
+
|
|
444
|
+
// Start pool stats collection
|
|
445
|
+
if (poolStatsInterval) {
|
|
446
|
+
clearInterval(poolStatsInterval);
|
|
447
|
+
}
|
|
448
|
+
poolStatsInterval = setInterval(collectPoolStats, config.mongodb.poolStatsInterval);
|
|
449
|
+
|
|
450
|
+
// Don't keep process alive just for pool stats
|
|
451
|
+
if (poolStatsInterval.unref) {
|
|
452
|
+
poolStatsInterval.unref();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
debug('MongoDB instrumentation attached successfully');
|
|
456
|
+
} catch (err) {
|
|
457
|
+
warn(`Failed to instrument MongoDB: ${err.message}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Auto-detect and instrument MongoDB
|
|
463
|
+
* Called automatically during SDK initialization.
|
|
464
|
+
*
|
|
465
|
+
* Uses require.cache scanning to detect mongoose, with a deferred
|
|
466
|
+
* retry mechanism for cases where mongoose loads after the SDK.
|
|
467
|
+
*/
|
|
468
|
+
export function autoInstrumentMongoDB() {
|
|
469
|
+
if (!config.mongodb.enabled) {
|
|
470
|
+
debug('MongoDB instrumentation disabled');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (isInstrumented) {
|
|
475
|
+
debug('MongoDB already instrumented');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Try immediate detection
|
|
480
|
+
const mongoose = tryDetectMongoose();
|
|
481
|
+
if (mongoose) {
|
|
482
|
+
attachToMongoose(mongoose);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Deferred detection: retry periodically as the app boots up.
|
|
487
|
+
// Mongoose may not be loaded yet when the SDK initializes.
|
|
488
|
+
let attempts = 0;
|
|
489
|
+
const maxAttempts = 6; // 6 retries
|
|
490
|
+
const retryInterval = 5000; // every 5s → 30s total detection window
|
|
491
|
+
|
|
492
|
+
detectionTimer = setInterval(() => {
|
|
493
|
+
attempts++;
|
|
494
|
+
|
|
495
|
+
if (isInstrumented) {
|
|
496
|
+
// Instrumented via manual call while we were waiting
|
|
497
|
+
clearInterval(detectionTimer);
|
|
498
|
+
detectionTimer = null;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const m = tryDetectMongoose();
|
|
503
|
+
if (m) {
|
|
504
|
+
clearInterval(detectionTimer);
|
|
505
|
+
detectionTimer = null;
|
|
506
|
+
attachToMongoose(m);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (attempts >= maxAttempts) {
|
|
511
|
+
clearInterval(detectionTimer);
|
|
512
|
+
detectionTimer = null;
|
|
513
|
+
warn(
|
|
514
|
+
'MongoDB not auto-detected after 30s. ' +
|
|
515
|
+
'For MongoDB monitoring, call: instrumentMongoDB(mongoose) after connecting.'
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}, retryInterval);
|
|
519
|
+
|
|
520
|
+
// Don't keep process alive just for detection
|
|
521
|
+
if (detectionTimer.unref) {
|
|
522
|
+
detectionTimer.unref();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
debug('MongoDB auto-detection started (will retry for 30s)');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Stop MongoDB instrumentation
|
|
530
|
+
*/
|
|
531
|
+
export function stopMongoDBInstrumentation() {
|
|
532
|
+
// Stop deferred detection timer
|
|
533
|
+
if (detectionTimer) {
|
|
534
|
+
clearInterval(detectionTimer);
|
|
535
|
+
detectionTimer = null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (poolStatsInterval) {
|
|
539
|
+
clearInterval(poolStatsInterval);
|
|
540
|
+
poolStatsInterval = null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (mongoClient) {
|
|
544
|
+
// Remove command monitoring listeners
|
|
545
|
+
mongoClient.removeListener('commandStarted', handleCommandStarted);
|
|
546
|
+
mongoClient.removeListener('commandSucceeded', handleCommandSucceeded);
|
|
547
|
+
mongoClient.removeListener('commandFailed', handleCommandFailed);
|
|
548
|
+
|
|
549
|
+
// Remove CMAP listeners
|
|
550
|
+
mongoClient.removeListener('connectionPoolCreated', handleConnectionPoolCreated);
|
|
551
|
+
mongoClient.removeListener('connectionCreated', handleConnectionCreated);
|
|
552
|
+
mongoClient.removeListener('connectionClosed', handleConnectionClosed);
|
|
553
|
+
mongoClient.removeListener('connectionCheckedOut', handleConnectionCheckedOut);
|
|
554
|
+
mongoClient.removeListener('connectionCheckedIn', handleConnectionCheckedIn);
|
|
555
|
+
mongoClient.removeListener('connectionCheckOutStarted', handleConnectionCheckOutStarted);
|
|
556
|
+
mongoClient.removeListener('connectionCheckOutFailed', handleConnectionCheckOutFailed);
|
|
557
|
+
|
|
558
|
+
mongoClient = null;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Reset state
|
|
562
|
+
isInstrumented = false;
|
|
563
|
+
|
|
564
|
+
// Reset pool stats
|
|
565
|
+
poolStats.totalCreated = 0;
|
|
566
|
+
poolStats.totalClosed = 0;
|
|
567
|
+
poolStats.checkedOut = 0;
|
|
568
|
+
poolStats.checkedIn = 0;
|
|
569
|
+
poolStats.waitQueueSize = 0;
|
|
570
|
+
|
|
571
|
+
pendingCommands.clear();
|
|
572
|
+
debug('MongoDB instrumentation stopped');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export default {
|
|
576
|
+
instrumentMongoDB,
|
|
577
|
+
autoInstrumentMongoDB,
|
|
578
|
+
stopMongoDBInstrumentation
|
|
579
|
+
};
|