@sentienguard/apm 1.0.7 → 1.0.9

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/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 connection pool stats via CMAP events
23
- const poolStats = {
24
- totalCreated: 0,
25
- totalClosed: 0,
26
- checkedOut: 0,
27
- checkedIn: 0,
28
- waitQueueSize: 0
29
- };
30
-
31
- // Operations we want to track (skip admin/internal commands)
32
- const TRACKED_OPERATIONS = new Set([
33
- 'find', 'findOne', 'findAndModify',
34
- 'insert', 'insertOne', 'insertMany',
35
- 'update', 'updateOne', 'updateMany',
36
- 'delete', 'deleteOne', 'deleteMany',
37
- 'aggregate', 'count', 'countDocuments', 'estimatedDocumentCount',
38
- 'distinct', 'mapReduce',
39
- 'createIndexes', 'dropIndexes',
40
- 'bulkWrite'
41
- ]);
42
-
43
- // Commands to ignore (internal/admin)
44
- const IGNORED_COMMANDS = new Set([
45
- 'hello', 'ismaster', 'isMaster', 'buildInfo', 'getLastError',
46
- 'ping', 'serverStatus', 'replSetGetStatus', 'hostInfo',
47
- 'listCollections', 'listIndexes', 'collStats', 'dbStats',
48
- 'saslStart', 'saslContinue', 'getnonce', 'authenticate'
49
- ]);
50
-
51
- let mongoClient = null;
52
- let poolStatsInterval = null;
53
-
54
- /**
55
- * Extract collection name from command
56
- */
57
- function getCollectionName(command) {
58
- // Most commands have collection name as the value of the command key
59
- const commandName = Object.keys(command)[0];
60
- const value = command[commandName];
61
-
62
- // For aggregate, the collection is the value
63
- if (typeof value === 'string' && value.length > 0 && value !== '1') {
64
- return value;
65
- }
66
-
67
- // Check common collection name fields
68
- if (command.collection) return command.collection;
69
- if (command.ns) {
70
- const parts = command.ns.split('.');
71
- return parts.length > 1 ? parts.slice(1).join('.') : parts[0];
72
- }
73
-
74
- return 'unknown';
75
- }
76
-
77
- /**
78
- * Normalize operation name
79
- */
80
- function normalizeOperation(commandName) {
81
- const name = commandName.toLowerCase();
82
-
83
- // Group similar operations
84
- if (name.includes('find')) return 'find';
85
- if (name.includes('insert')) return 'insert';
86
- if (name.includes('update')) return 'update';
87
- if (name.includes('delete') || name.includes('remove')) return 'delete';
88
- if (name === 'aggregate') return 'aggregate';
89
- if (name.includes('count')) return 'count';
90
- if (name.includes('index')) return 'index';
91
-
92
- return name;
93
- }
94
-
95
- /**
96
- * Handle command started event
97
- */
98
- function handleCommandStarted(event) {
99
- if (!config.mongodb.enabled) return;
100
-
101
- const commandName = event.commandName;
102
-
103
- // Skip ignored commands
104
- if (IGNORED_COMMANDS.has(commandName)) {
105
- return;
106
- }
107
-
108
- // Store start time for latency calculation
109
- pendingCommands.set(event.requestId, {
110
- startTime: process.hrtime.bigint(),
111
- commandName,
112
- collection: getCollectionName(event.command),
113
- databaseName: event.databaseName
114
- });
115
-
116
- debug(`MongoDB command started: ${commandName} on ${event.databaseName}`);
117
- }
118
-
119
- /**
120
- * Handle command succeeded event
121
- */
122
- function handleCommandSucceeded(event) {
123
- if (!config.mongodb.enabled) return;
124
-
125
- const pending = pendingCommands.get(event.requestId);
126
- if (!pending) return;
127
-
128
- pendingCommands.delete(event.requestId);
129
-
130
- // Calculate latency in milliseconds
131
- const latencyNs = process.hrtime.bigint() - pending.startTime;
132
- const latencyMs = Number(latencyNs) / 1e6;
133
-
134
- // Record the operation
135
- const aggregator = getAggregator();
136
- const operation = normalizeOperation(pending.commandName);
137
-
138
- aggregator.recordMongoOperation(
139
- pending.collection,
140
- operation,
141
- latencyMs,
142
- false // not an error
143
- );
144
-
145
- // Check for slow query
146
- if (latencyMs > config.mongodb.slowQueryMs) {
147
- aggregator.recordSlowQuery(
148
- pending.collection,
149
- operation,
150
- latencyMs
151
- );
152
- debug(`Slow MongoDB query: ${operation} on ${pending.collection} took ${latencyMs.toFixed(2)}ms`);
153
- }
154
-
155
- debug(`MongoDB command succeeded: ${pending.commandName} (${latencyMs.toFixed(2)}ms)`);
156
- }
157
-
158
- /**
159
- * Handle command failed event
160
- */
161
- function handleCommandFailed(event) {
162
- if (!config.mongodb.enabled) return;
163
-
164
- const pending = pendingCommands.get(event.requestId);
165
- if (!pending) return;
166
-
167
- pendingCommands.delete(event.requestId);
168
-
169
- // Calculate latency in milliseconds
170
- const latencyNs = process.hrtime.bigint() - pending.startTime;
171
- const latencyMs = Number(latencyNs) / 1e6;
172
-
173
- // Record the failed operation
174
- const aggregator = getAggregator();
175
- const operation = normalizeOperation(pending.commandName);
176
-
177
- aggregator.recordMongoOperation(
178
- pending.collection,
179
- operation,
180
- latencyMs,
181
- true // is an error
182
- );
183
-
184
- warn(`MongoDB command failed: ${pending.commandName} - ${event.failure?.message || 'Unknown error'}`);
185
- }
186
-
187
- /**
188
- * CMAP Event Handlers for Connection Pool Monitoring
189
- */
190
- function handleConnectionCreated(event) {
191
- poolStats.totalCreated++;
192
- debug(`MongoDB connection created (total: ${poolStats.totalCreated})`);
193
- }
194
-
195
- function handleConnectionClosed(event) {
196
- poolStats.totalClosed++;
197
- debug(`MongoDB connection closed (total closed: ${poolStats.totalClosed})`);
198
- }
199
-
200
- function handleConnectionCheckedOut(event) {
201
- poolStats.checkedOut++;
202
- debug(`MongoDB connection checked out (active: ${poolStats.checkedOut - poolStats.checkedIn})`);
203
- }
204
-
205
- function handleConnectionCheckedIn(event) {
206
- poolStats.checkedIn++;
207
- debug(`MongoDB connection checked in (active: ${poolStats.checkedOut - poolStats.checkedIn})`);
208
- }
209
-
210
- function handleConnectionCheckOutStarted(event) {
211
- poolStats.waitQueueSize++;
212
- }
213
-
214
- function handleConnectionCheckOutFailed(event) {
215
- poolStats.waitQueueSize = Math.max(0, poolStats.waitQueueSize - 1);
216
- }
217
-
218
- function handleConnectionPoolCreated(event) {
219
- debug(`MongoDB connection pool created: ${event.address}`);
220
- }
221
-
222
- /**
223
- * Collect connection pool statistics
224
- * Now uses tracked CMAP events for accurate stats
225
- */
226
- function collectPoolStats() {
227
- if (!mongoClient || !config.mongodb.enabled) return;
228
-
229
- try {
230
- const aggregator = getAggregator();
231
-
232
- // Calculate current pool state from tracked events
233
- const totalConnections = poolStats.totalCreated - poolStats.totalClosed;
234
- const activeConnections = poolStats.checkedOut - poolStats.checkedIn;
235
- const idleConnections = Math.max(0, totalConnections - activeConnections);
236
-
237
- // Reset wait queue after reporting (it's instantaneous)
238
- const currentWaitQueue = poolStats.waitQueueSize;
239
- poolStats.waitQueueSize = 0;
240
-
241
- aggregator.updatePoolStats({
242
- active: activeConnections,
243
- idle: idleConnections,
244
- waitQueueSize: currentWaitQueue,
245
- total: totalConnections
246
- });
247
-
248
- debug(`MongoDB pool stats: active=${activeConnections}, idle=${idleConnections}, total=${totalConnections}, waitQueue=${currentWaitQueue}`);
249
- } catch (err) {
250
- debug(`Failed to collect pool stats: ${err.message}`);
251
- }
252
- }
253
-
254
- /**
255
- * Instrument a MongoDB client or Mongoose connection
256
- *
257
- * @param {MongoClient|Mongoose} clientOrMongoose - MongoDB client or Mongoose instance
258
- */
259
- export function instrumentMongoDB(clientOrMongoose) {
260
- if (!config.mongodb.enabled) {
261
- debug('MongoDB instrumentation disabled');
262
- return;
263
- }
264
-
265
- try {
266
- let client;
267
-
268
- // Check if it's Mongoose
269
- if (clientOrMongoose?.connection?.getClient) {
270
- client = clientOrMongoose.connection.getClient();
271
- debug('Detected Mongoose, getting underlying MongoDB client');
272
- } else if (clientOrMongoose?.getClient) {
273
- // Mongoose connection object
274
- client = clientOrMongoose.getClient();
275
- } else if (typeof clientOrMongoose?.on === 'function') {
276
- // Native MongoDB client
277
- client = clientOrMongoose;
278
- } else {
279
- warn('Unable to instrument MongoDB: unrecognized client type');
280
- return;
281
- }
282
-
283
- if (!client) {
284
- warn('MongoDB client not available for instrumentation');
285
- return;
286
- }
287
-
288
- // Store reference for pool stats
289
- mongoClient = client;
290
-
291
- // Attach command monitoring listeners
292
- client.on('commandStarted', handleCommandStarted);
293
- client.on('commandSucceeded', handleCommandSucceeded);
294
- client.on('commandFailed', handleCommandFailed);
295
-
296
- // Attach CMAP (Connection Pool Monitoring) event listeners
297
- client.on('connectionPoolCreated', handleConnectionPoolCreated);
298
- client.on('connectionCreated', handleConnectionCreated);
299
- client.on('connectionClosed', handleConnectionClosed);
300
- client.on('connectionCheckedOut', handleConnectionCheckedOut);
301
- client.on('connectionCheckedIn', handleConnectionCheckedIn);
302
- client.on('connectionCheckOutStarted', handleConnectionCheckOutStarted);
303
- client.on('connectionCheckOutFailed', handleConnectionCheckOutFailed);
304
-
305
- // Start pool stats collection
306
- if (poolStatsInterval) {
307
- clearInterval(poolStatsInterval);
308
- }
309
- poolStatsInterval = setInterval(collectPoolStats, config.mongodb.poolStatsInterval);
310
-
311
- // Don't keep process alive just for pool stats
312
- if (poolStatsInterval.unref) {
313
- poolStatsInterval.unref();
314
- }
315
-
316
- debug('MongoDB instrumentation attached successfully');
317
- } catch (err) {
318
- warn(`Failed to instrument MongoDB: ${err.message}`);
319
- }
320
- }
321
-
322
- /**
323
- * Auto-detect and instrument MongoDB
324
- * Called automatically during SDK initialization
325
- */
326
- export function autoInstrumentMongoDB() {
327
- if (!config.mongodb.enabled) {
328
- debug('MongoDB instrumentation disabled');
329
- return;
330
- }
331
-
332
- // Try to detect mongoose
333
- try {
334
- // Dynamic import to avoid requiring mongoose as a dependency
335
- const mongoose = globalThis.mongoose;
336
- if (mongoose?.connection) {
337
- // Wait for connection to be ready
338
- if (mongoose.connection.readyState === 1) {
339
- instrumentMongoDB(mongoose);
340
- } else {
341
- mongoose.connection.once('connected', () => {
342
- instrumentMongoDB(mongoose);
343
- });
344
- debug('Waiting for Mongoose connection to instrument');
345
- }
346
- return;
347
- }
348
- } catch (e) {
349
- // Mongoose not available
350
- }
351
-
352
- debug('No MongoDB client detected for auto-instrumentation. Use instrumentMongoDB() manually.');
353
- }
354
-
355
- /**
356
- * Stop MongoDB instrumentation
357
- */
358
- export function stopMongoDBInstrumentation() {
359
- if (poolStatsInterval) {
360
- clearInterval(poolStatsInterval);
361
- poolStatsInterval = null;
362
- }
363
-
364
- if (mongoClient) {
365
- // Remove command monitoring listeners
366
- mongoClient.removeListener('commandStarted', handleCommandStarted);
367
- mongoClient.removeListener('commandSucceeded', handleCommandSucceeded);
368
- mongoClient.removeListener('commandFailed', handleCommandFailed);
369
-
370
- // Remove CMAP listeners
371
- mongoClient.removeListener('connectionPoolCreated', handleConnectionPoolCreated);
372
- mongoClient.removeListener('connectionCreated', handleConnectionCreated);
373
- mongoClient.removeListener('connectionClosed', handleConnectionClosed);
374
- mongoClient.removeListener('connectionCheckedOut', handleConnectionCheckedOut);
375
- mongoClient.removeListener('connectionCheckedIn', handleConnectionCheckedIn);
376
- mongoClient.removeListener('connectionCheckOutStarted', handleConnectionCheckOutStarted);
377
- mongoClient.removeListener('connectionCheckOutFailed', handleConnectionCheckOutFailed);
378
-
379
- mongoClient = null;
380
- }
381
-
382
- // Reset pool stats
383
- poolStats.totalCreated = 0;
384
- poolStats.totalClosed = 0;
385
- poolStats.checkedOut = 0;
386
- poolStats.checkedIn = 0;
387
- poolStats.waitQueueSize = 0;
388
-
389
- pendingCommands.clear();
390
- debug('MongoDB instrumentation stopped');
391
- }
392
-
393
- export default {
394
- instrumentMongoDB,
395
- autoInstrumentMongoDB,
396
- stopMongoDBInstrumentation
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
+ };