autotel-mongoose 0.0.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.
@@ -0,0 +1,880 @@
1
+ // Note: `any` is only used for dynamic method wrapping on runtime objects.
2
+ // Type-safe interfaces are used for all public APIs.
3
+ // Mongoose is a devDependency so we type-check against the real API; consumers use the peer.
4
+
5
+ import type { Mongoose } from 'mongoose';
6
+ import { otelTrace as trace, SpanKind } from 'autotel';
7
+ import type { Span, Tracer } from 'autotel';
8
+ import {
9
+ runWithSpan,
10
+ finalizeSpan,
11
+ getActiveSpan,
12
+ } from 'autotel/trace-helpers';
13
+
14
+ import {
15
+ ATTR_DB_SYSTEM_NAME,
16
+ ATTR_DB_OPERATION_NAME,
17
+ ATTR_DB_COLLECTION_NAME,
18
+ ATTR_DB_NAMESPACE,
19
+ ATTR_DB_QUERY_TEXT,
20
+ ATTR_SERVER_ADDRESS,
21
+ ATTR_SERVER_PORT,
22
+ DB_SYSTEM_NAME_VALUE_MONGODB,
23
+ } from './constants';
24
+ import type {
25
+ InstrumentMongooseConfig,
26
+ ResolvedConfig,
27
+ SerializerPayload,
28
+ } from './types';
29
+ import { DEFAULT_TRACER_NAME } from './types';
30
+ import {
31
+ createStatementCapture,
32
+ defaultSerializer,
33
+ type StatementCaptureFn,
34
+ } from './statement';
35
+
36
+ const INSTRUMENTED_FLAG = '__autotelMongooseInstrumented' as const;
37
+ const WRAPPED_HOOK_FLAG = '__autotelWrappedHook' as const;
38
+
39
+ /**
40
+ * Symbol used to store the parent span on Query/Aggregate objects.
41
+ * This preserves context across chainable query methods.
42
+ */
43
+ export const _STORED_PARENT_SPAN: unique symbol = Symbol('stored-parent-span');
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Span creation
47
+ // ---------------------------------------------------------------------------
48
+
49
+ /**
50
+ * Creates a span for a Mongoose operation.
51
+ * Note: db.query.text is NOT set here — callers set it after payload extraction.
52
+ */
53
+ function createSpan(
54
+ tracer: Tracer,
55
+ operation: string,
56
+ modelName: string | undefined,
57
+ collectionName: string | undefined,
58
+ config: ResolvedConfig,
59
+ ): Span {
60
+ const spanName = collectionName
61
+ ? `${operation} ${collectionName}`
62
+ : modelName
63
+ ? `${operation} ${modelName}`
64
+ : `mongoose.${operation}`;
65
+
66
+ const attributes: Record<string, any> = {
67
+ [ATTR_DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB,
68
+ [ATTR_DB_OPERATION_NAME]: operation,
69
+ };
70
+
71
+ if (collectionName && config.captureCollectionName) {
72
+ attributes[ATTR_DB_COLLECTION_NAME] = collectionName;
73
+ }
74
+ if (config.dbName) {
75
+ attributes[ATTR_DB_NAMESPACE] = config.dbName;
76
+ }
77
+ if (config.peerName) {
78
+ attributes[ATTR_SERVER_ADDRESS] = config.peerName;
79
+ }
80
+ if (config.peerPort) {
81
+ attributes[ATTR_SERVER_PORT] = config.peerPort;
82
+ }
83
+
84
+ return tracer.startSpan(spanName, { kind: SpanKind.CLIENT, attributes });
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Wrapper helpers
89
+ // ---------------------------------------------------------------------------
90
+
91
+ /**
92
+ * Wraps Model methods that return Query objects (find, findOne, findById,
93
+ * findOneAndUpdate, findOneAndDelete, findOneAndReplace, deleteOne, deleteMany,
94
+ * updateOne, updateMany, countDocuments, estimatedDocumentCount).
95
+ *
96
+ * Creates span FIRST, calls original, extracts payload from the returned Query,
97
+ * sets db.query.text AFTER extraction, then wraps exec() to finalize span.
98
+ */
99
+ function wrapQueryReturningMethod(
100
+ target: any,
101
+ methodName: string,
102
+ operation: string,
103
+ getCollectionName: (obj: any) => string | undefined,
104
+ getModelName: (obj: any) => string | undefined,
105
+ tracer: Tracer,
106
+ config: ResolvedConfig,
107
+ captureStatement: StatementCaptureFn,
108
+ ): void {
109
+ const original = target[methodName];
110
+ if (typeof original !== 'function') {
111
+ return;
112
+ }
113
+
114
+ target[methodName] = function instrumented(this: any, ...args: any[]): any {
115
+ const collectionName = getCollectionName(this);
116
+ const modelName = getModelName(this);
117
+ const span = createSpan(
118
+ tracer,
119
+ operation,
120
+ modelName,
121
+ collectionName,
122
+ config,
123
+ );
124
+
125
+ return runWithSpan(span, () => {
126
+ try {
127
+ const result = original.apply(this, args);
128
+
129
+ // Extract payload from the returned Query object
130
+ if (result && typeof result.exec === 'function') {
131
+ try {
132
+ const payload: SerializerPayload = {};
133
+ if (typeof result.getFilter === 'function') {
134
+ payload.condition = result.getFilter();
135
+ }
136
+ if (result._update !== undefined) {
137
+ payload.updates = result._update;
138
+ }
139
+ if (typeof result.getOptions === 'function') {
140
+ payload.options = result.getOptions();
141
+ }
142
+ if (result._fields !== undefined) {
143
+ payload.fields = result._fields;
144
+ }
145
+ const statementText = captureStatement(operation, payload);
146
+ if (statementText) {
147
+ span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
148
+ }
149
+ } catch {
150
+ // Ignore errors in payload extraction
151
+ }
152
+
153
+ // Wrap exec() to finalize span
154
+ const originalExec = result.exec.bind(result);
155
+ result.exec = function wrappedExec(): Promise<any> {
156
+ try {
157
+ const execPromise = originalExec();
158
+ return Promise.resolve(execPromise)
159
+ .then((value: any) => {
160
+ finalizeSpan(span);
161
+ return value;
162
+ })
163
+ .catch((error: unknown) => {
164
+ finalizeSpan(
165
+ span,
166
+ error instanceof Error ? error : new Error(String(error)),
167
+ );
168
+ throw error;
169
+ });
170
+ } catch (error) {
171
+ finalizeSpan(
172
+ span,
173
+ error instanceof Error ? error : new Error(String(error)),
174
+ );
175
+ throw error;
176
+ }
177
+ };
178
+
179
+ return result; // Return Query, not Promise
180
+ }
181
+
182
+ // Fallback for unexpected non-query results
183
+ finalizeSpan(span);
184
+ return result;
185
+ } catch (error) {
186
+ finalizeSpan(
187
+ span,
188
+ error instanceof Error ? error : new Error(String(error)),
189
+ );
190
+ throw error;
191
+ }
192
+ });
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Wraps Model static methods (create, insertMany, aggregate, bulkWrite).
198
+ *
199
+ * Builds payload from args BEFORE calling original (args are available
200
+ * immediately), creates span, sets db.query.text, calls original, then wraps
201
+ * exec() or promise for span finalization.
202
+ */
203
+ function wrapStaticMethod(
204
+ target: any,
205
+ methodName: string,
206
+ operation: string,
207
+ getCollectionName: (obj: any) => string | undefined,
208
+ getModelName: (obj: any) => string | undefined,
209
+ tracer: Tracer,
210
+ config: ResolvedConfig,
211
+ captureStatement: StatementCaptureFn,
212
+ ): void {
213
+ const original = target[methodName];
214
+ if (typeof original !== 'function') {
215
+ return;
216
+ }
217
+
218
+ target[methodName] = function instrumented(this: any, ...args: any[]): any {
219
+ const collectionName = getCollectionName(this);
220
+ const modelName = getModelName(this);
221
+
222
+ // Build payload from args before calling original
223
+ const payload: SerializerPayload = {};
224
+ try {
225
+ switch (operation) {
226
+ case 'create': {
227
+ payload.document = args[0];
228
+ break;
229
+ }
230
+ case 'insertMany': {
231
+ payload.documents = args[0];
232
+ break;
233
+ }
234
+ case 'aggregate': {
235
+ payload.aggregatePipeline = args[0];
236
+ break;
237
+ }
238
+ case 'bulkWrite': {
239
+ payload.operations = args[0];
240
+ break;
241
+ }
242
+ default: {
243
+ break;
244
+ }
245
+ }
246
+ } catch {
247
+ // Ignore errors in payload extraction
248
+ }
249
+
250
+ const span = createSpan(
251
+ tracer,
252
+ operation,
253
+ modelName,
254
+ collectionName,
255
+ config,
256
+ );
257
+
258
+ try {
259
+ const statementText = captureStatement(operation, payload);
260
+ if (statementText) {
261
+ span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
262
+ }
263
+ } catch {
264
+ // Ignore serialization errors
265
+ }
266
+
267
+ return runWithSpan(span, () => {
268
+ try {
269
+ const result = original.apply(this, args);
270
+
271
+ // If result has exec() (e.g., aggregate), wrap it
272
+ if (result && typeof result.exec === 'function') {
273
+ const originalExec = result.exec.bind(result);
274
+ result.exec = function wrappedExec(): Promise<any> {
275
+ try {
276
+ const execPromise = originalExec();
277
+ return Promise.resolve(execPromise)
278
+ .then((value: any) => {
279
+ finalizeSpan(span);
280
+ return value;
281
+ })
282
+ .catch((error: unknown) => {
283
+ finalizeSpan(
284
+ span,
285
+ error instanceof Error ? error : new Error(String(error)),
286
+ );
287
+ throw error;
288
+ });
289
+ } catch (error) {
290
+ finalizeSpan(
291
+ span,
292
+ error instanceof Error ? error : new Error(String(error)),
293
+ );
294
+ throw error;
295
+ }
296
+ };
297
+ return result;
298
+ }
299
+
300
+ // For direct promise results (e.g., create, insertMany)
301
+ if (result && typeof result.then === 'function') {
302
+ return Promise.resolve(result as Promise<any>)
303
+ .then((value) => {
304
+ finalizeSpan(span);
305
+ return value;
306
+ })
307
+ .catch((error: unknown) => {
308
+ finalizeSpan(
309
+ span,
310
+ error instanceof Error ? error : new Error(String(error)),
311
+ );
312
+ throw error;
313
+ });
314
+ }
315
+
316
+ finalizeSpan(span);
317
+ return result;
318
+ } catch (error) {
319
+ finalizeSpan(
320
+ span,
321
+ error instanceof Error ? error : new Error(String(error)),
322
+ );
323
+ throw error;
324
+ }
325
+ });
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Wraps Model instance methods (save, deleteOne on prototype).
331
+ *
332
+ * Extracts document via `this.toObject()` BEFORE calling original,
333
+ * creates span, sets db.query.text, calls original, wraps promise
334
+ * for span finalization.
335
+ */
336
+ function wrapInstanceMethod(
337
+ target: any,
338
+ methodName: string,
339
+ operation: string,
340
+ getCollectionName: (obj: any) => string | undefined,
341
+ getModelName: (obj: any) => string | undefined,
342
+ tracer: Tracer,
343
+ config: ResolvedConfig,
344
+ captureStatement: StatementCaptureFn,
345
+ ): void {
346
+ const original = target[methodName];
347
+ if (typeof original !== 'function') {
348
+ return;
349
+ }
350
+
351
+ target[methodName] = function instrumented(this: any, ...args: any[]): any {
352
+ const collectionName = getCollectionName(this);
353
+ const modelName = getModelName(this);
354
+
355
+ // Extract document before calling original
356
+ const payload: SerializerPayload = {};
357
+ try {
358
+ if (typeof this.toObject === 'function') {
359
+ payload.document = this.toObject();
360
+ }
361
+ } catch {
362
+ // Ignore errors in document extraction
363
+ }
364
+
365
+ const span = createSpan(
366
+ tracer,
367
+ operation,
368
+ modelName,
369
+ collectionName,
370
+ config,
371
+ );
372
+
373
+ try {
374
+ const statementText = captureStatement(operation, payload);
375
+ if (statementText) {
376
+ span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
377
+ }
378
+ } catch {
379
+ // Ignore serialization errors
380
+ }
381
+
382
+ return runWithSpan(span, () => {
383
+ try {
384
+ const result = original.apply(this, args);
385
+
386
+ // Instance methods return promises
387
+ if (result && typeof result.then === 'function') {
388
+ return Promise.resolve(result as Promise<any>)
389
+ .then((value) => {
390
+ finalizeSpan(span);
391
+ return value;
392
+ })
393
+ .catch((error: unknown) => {
394
+ finalizeSpan(
395
+ span,
396
+ error instanceof Error ? error : new Error(String(error)),
397
+ );
398
+ throw error;
399
+ });
400
+ }
401
+
402
+ finalizeSpan(span);
403
+ return result;
404
+ } catch (error) {
405
+ finalizeSpan(
406
+ span,
407
+ error instanceof Error ? error : new Error(String(error)),
408
+ );
409
+ throw error;
410
+ }
411
+ });
412
+ };
413
+ }
414
+
415
+ // ---------------------------------------------------------------------------
416
+ // Chainable method wrapping (copied from original)
417
+ // ---------------------------------------------------------------------------
418
+
419
+ /**
420
+ * Wraps chainable Query methods (populate, select, lean, etc.) to capture span context.
421
+ */
422
+ function wrapChainableMethod(target: any, methodName: string): void {
423
+ const original = target[methodName];
424
+ if (typeof original !== 'function') {
425
+ return;
426
+ }
427
+
428
+ target[methodName] = function captureContext(this: any, ...args: any[]): any {
429
+ const currentSpan = getActiveSpan();
430
+ const result = original.apply(this, args);
431
+
432
+ // Store parent span on returned Query for exec() calls
433
+ if (result && typeof result.exec === 'function') {
434
+ (result as any)[_STORED_PARENT_SPAN] = currentSpan;
435
+ }
436
+
437
+ return result;
438
+ };
439
+ }
440
+
441
+ // ---------------------------------------------------------------------------
442
+ // Schema hook instrumentation (copied from original, updated semconv)
443
+ // ---------------------------------------------------------------------------
444
+
445
+ /**
446
+ * Patches Mongoose Schema hooks (pre/post) to automatically trace them.
447
+ * Only wraps user-defined hooks, skipping Mongoose's internal hooks.
448
+ */
449
+ function patchSchemaHooks(
450
+ Schema: any,
451
+ tracer: Tracer,
452
+ config: ResolvedConfig,
453
+ ): void {
454
+ if (!Schema?.prototype) {
455
+ return;
456
+ }
457
+
458
+ const HOOK_FLAG = '__autotelHookInstrumented' as const;
459
+ if ((Schema.prototype as any)[HOOK_FLAG]) {
460
+ return;
461
+ }
462
+
463
+ const originalPre = Schema.prototype.pre;
464
+ if (typeof originalPre === 'function') {
465
+ Schema.prototype.pre = function (hookName: string, ...args: any[]): any {
466
+ const handler =
467
+ typeof args[0] === 'function'
468
+ ? args[0]
469
+ : typeof args[1] === 'function'
470
+ ? args[1]
471
+ : null;
472
+
473
+ // Only wrap user-defined hooks, skip Mongoose internals
474
+ if (handler && !isMongooseInternalHook(handler)) {
475
+ const wrapped = wrapHookHandler(
476
+ handler,
477
+ hookName,
478
+ 'pre',
479
+ tracer,
480
+ config,
481
+ );
482
+ if (typeof args[0] === 'function') {
483
+ args[0] = wrapped;
484
+ } else if (typeof args[1] === 'function') {
485
+ args[1] = wrapped;
486
+ }
487
+ }
488
+
489
+ return Reflect.apply(originalPre, this, [hookName, ...args]);
490
+ };
491
+ }
492
+
493
+ const originalPost = Schema.prototype.post;
494
+ if (typeof originalPost === 'function') {
495
+ Schema.prototype.post = function (hookName: string, ...args: any[]): any {
496
+ const handler =
497
+ typeof args[0] === 'function'
498
+ ? args[0]
499
+ : typeof args[1] === 'function'
500
+ ? args[1]
501
+ : null;
502
+
503
+ // Only wrap user-defined hooks, skip Mongoose internals
504
+ if (handler && !isMongooseInternalHook(handler)) {
505
+ const wrapped = wrapHookHandler(
506
+ handler,
507
+ hookName,
508
+ 'post',
509
+ tracer,
510
+ config,
511
+ );
512
+ if (typeof args[0] === 'function') {
513
+ args[0] = wrapped;
514
+ } else if (typeof args[1] === 'function') {
515
+ args[1] = wrapped;
516
+ }
517
+ }
518
+
519
+ return Reflect.apply(originalPost, this, [hookName, ...args]);
520
+ };
521
+ }
522
+
523
+ (Schema.prototype as any)[HOOK_FLAG] = true;
524
+ }
525
+
526
+ /**
527
+ * Detects if a hook handler is from Mongoose's internal code.
528
+ * Skips private methods, known internal patterns, and functions with
529
+ * Mongoose-internal source code signatures.
530
+ *
531
+ * Note: We intentionally allow anonymous functions because user-defined
532
+ * hooks are often anonymous (e.g., `schema.pre('save', async function() {...})`).
533
+ */
534
+ function isMongooseInternalHook(handler: any): boolean {
535
+ if (typeof handler !== 'function') {
536
+ return false;
537
+ }
538
+
539
+ const funcName = handler.name || '';
540
+
541
+ // Skip private/internal methods (starting with _ or $)
542
+ if (funcName.startsWith('_') || funcName.startsWith('$')) {
543
+ return true;
544
+ }
545
+
546
+ // Skip known Mongoose internal hook patterns by name
547
+ const mongooseInternalNamePatterns = [
548
+ 'shardingPlugin',
549
+ 'mongooseInternalHook',
550
+ 'noop',
551
+ 'wrapped',
552
+ 'bound ',
553
+ ];
554
+
555
+ if (
556
+ mongooseInternalNamePatterns.some((pattern) => funcName.includes(pattern))
557
+ ) {
558
+ return true;
559
+ }
560
+
561
+ // Check function source for Mongoose-internal patterns
562
+ // These patterns appear in Mongoose's auto-generated validation/transform hooks
563
+ try {
564
+ const source = handler.toString();
565
+ const mongooseInternalSourcePatterns = [
566
+ 'this.$__', // Mongoose internal document methods
567
+ 'this.$isValid', // Mongoose validation
568
+ 'this.$locals', // Mongoose local properties
569
+ '_this.$__', // Mongoose internal with closure
570
+ 'schema.s.hooks', // Mongoose hooks system
571
+ 'kareem', // Mongoose's hooks library
572
+ ];
573
+
574
+ if (
575
+ mongooseInternalSourcePatterns.some((pattern) => source.includes(pattern))
576
+ ) {
577
+ return true;
578
+ }
579
+ } catch {
580
+ // If we can't get source, allow the hook through
581
+ }
582
+
583
+ return false;
584
+ }
585
+
586
+ /**
587
+ * Wraps a hook handler to trace its execution.
588
+ * Handles both callback-style (with next) and promise-style hooks.
589
+ */
590
+ function wrapHookHandler(
591
+ handler: any,
592
+ hookName: string,
593
+ hookType: 'pre' | 'post',
594
+ tracer: Tracer,
595
+ config: ResolvedConfig,
596
+ ): any {
597
+ if (typeof handler !== 'function') {
598
+ return handler;
599
+ }
600
+
601
+ // Skip if already wrapped to prevent duplicate spans
602
+ if ((handler as any)[WRAPPED_HOOK_FLAG]) {
603
+ return handler;
604
+ }
605
+
606
+ const wrappedHook = function wrappedHook(this: any, ...args: any[]): any {
607
+ let modelName: string | undefined;
608
+ let collectionName: string | undefined;
609
+
610
+ try {
611
+ if (this.constructor?.modelName) {
612
+ modelName = this.constructor.modelName;
613
+ collectionName =
614
+ this.constructor.collection?.collectionName || modelName;
615
+ } else if (this.model?.modelName) {
616
+ modelName = this.model.modelName;
617
+ collectionName = this.model.collection?.collectionName || modelName;
618
+ }
619
+ } catch {
620
+ // Ignore errors in extracting context
621
+ }
622
+
623
+ const spanName = collectionName
624
+ ? `mongoose.${collectionName}.${hookType}.${hookName}`
625
+ : `mongoose.hook.${hookType}.${hookName}`;
626
+
627
+ const span = tracer.startSpan(spanName, { kind: SpanKind.INTERNAL });
628
+ span.setAttribute('hook.type', hookType);
629
+ span.setAttribute('hook.operation', hookName);
630
+ if (modelName) {
631
+ span.setAttribute('hook.model', modelName);
632
+ }
633
+ if (collectionName && config.captureCollectionName) {
634
+ span.setAttribute(ATTR_DB_COLLECTION_NAME, collectionName);
635
+ }
636
+ span.setAttribute(ATTR_DB_SYSTEM_NAME, DB_SYSTEM_NAME_VALUE_MONGODB);
637
+ if (config.dbName) {
638
+ span.setAttribute(ATTR_DB_NAMESPACE, config.dbName);
639
+ }
640
+
641
+ return runWithSpan(span, () => {
642
+ try {
643
+ const result = handler.apply(this, args);
644
+
645
+ if (result && typeof result.then === 'function') {
646
+ return Promise.resolve(result as Promise<any>)
647
+ .then((value) => {
648
+ finalizeSpan(span);
649
+ return value;
650
+ })
651
+ .catch((error: unknown) => {
652
+ finalizeSpan(
653
+ span,
654
+ error instanceof Error ? error : new Error(String(error)),
655
+ );
656
+ throw error;
657
+ });
658
+ }
659
+
660
+ finalizeSpan(span);
661
+ return result;
662
+ } catch (error) {
663
+ finalizeSpan(
664
+ span,
665
+ error instanceof Error ? error : new Error(String(error)),
666
+ );
667
+ throw error;
668
+ }
669
+ });
670
+ };
671
+
672
+ // Mark as wrapped to prevent double-wrapping
673
+ (wrappedHook as any)[WRAPPED_HOOK_FLAG] = true;
674
+ return wrappedHook;
675
+ }
676
+
677
+ // ---------------------------------------------------------------------------
678
+ // Main instrumentation function
679
+ // ---------------------------------------------------------------------------
680
+
681
+ /**
682
+ * Instruments Mongoose with OpenTelemetry tracing.
683
+ *
684
+ * Supports Mongoose 8+ with promise-based API only.
685
+ * Patches Model methods, Query methods, and user-defined Schema hooks to create spans.
686
+ *
687
+ * **IMPORTANT:** Call `instrumentMongoose()` BEFORE defining schemas/models
688
+ * to ensure hooks are automatically instrumented.
689
+ *
690
+ * @example
691
+ * ```typescript
692
+ * import mongoose from 'mongoose';
693
+ * import { init } from 'autotel';
694
+ * import { instrumentMongoose } from 'autotel-mongoose';
695
+ *
696
+ * init({ service: 'my-app' });
697
+ *
698
+ * // Call BEFORE defining schemas
699
+ * instrumentMongoose(mongoose, { dbName: 'myapp' });
700
+ *
701
+ * const userSchema = new mongoose.Schema({ name: String });
702
+ * const User = mongoose.model('User', userSchema);
703
+ *
704
+ * // All operations are automatically traced
705
+ * await User.findOne({}).populate('posts').exec();
706
+ * ```
707
+ */
708
+ export function instrumentMongoose(
709
+ mongoose: Mongoose,
710
+ config?: InstrumentMongooseConfig,
711
+ ): Mongoose {
712
+ if (!mongoose?.Model) {
713
+ return mongoose;
714
+ }
715
+
716
+ const m = mongoose as any;
717
+ if (m[INSTRUMENTED_FLAG]) {
718
+ return mongoose;
719
+ }
720
+
721
+ // Resolve statement-related config separately (they accept undefined)
722
+ const resolvedSerializer = config?.dbStatementSerializer;
723
+ const resolvedRedactor = config?.statementRedactor ?? 'default';
724
+
725
+ const finalConfig: ResolvedConfig = {
726
+ dbName: config?.dbName || '',
727
+ peerName: config?.peerName || '',
728
+ peerPort: config?.peerPort || 27_017,
729
+ tracerName: config?.tracerName || DEFAULT_TRACER_NAME,
730
+ captureCollectionName: config?.captureCollectionName ?? true,
731
+ instrumentHooks: config?.instrumentHooks ?? false,
732
+ dbStatementSerializer:
733
+ resolvedSerializer === false
734
+ ? false
735
+ : (resolvedSerializer ?? defaultSerializer),
736
+ statementRedactor: resolvedRedactor,
737
+ };
738
+
739
+ const tracer = trace.getTracer(finalConfig.tracerName);
740
+
741
+ // Create statement capture function
742
+ const captureStatement = createStatementCapture({
743
+ dbStatementSerializer: resolvedSerializer,
744
+ statementRedactor: resolvedRedactor,
745
+ });
746
+
747
+ // Patch Schema hooks only if enabled
748
+ if (m.Schema && finalConfig.instrumentHooks) {
749
+ patchSchemaHooks(m.Schema, tracer, finalConfig);
750
+ }
751
+
752
+ // Helper functions
753
+ const getModelCollectionName = (model: any) => {
754
+ try {
755
+ return model.collection?.collectionName || model.modelName;
756
+ } catch {
757
+ return;
758
+ }
759
+ };
760
+
761
+ // Patch Query-returning methods on Model
762
+ const queryMethods: Array<{ method: string; operation: string }> = [
763
+ { method: 'find', operation: 'find' },
764
+ { method: 'findOne', operation: 'findOne' },
765
+ { method: 'findById', operation: 'findById' },
766
+ { method: 'findOneAndUpdate', operation: 'findOneAndUpdate' },
767
+ { method: 'findOneAndDelete', operation: 'findOneAndDelete' },
768
+ { method: 'findOneAndReplace', operation: 'findOneAndReplace' },
769
+ { method: 'deleteOne', operation: 'deleteOne' },
770
+ { method: 'deleteMany', operation: 'deleteMany' },
771
+ { method: 'updateOne', operation: 'updateOne' },
772
+ { method: 'updateMany', operation: 'updateMany' },
773
+ { method: 'countDocuments', operation: 'countDocuments' },
774
+ { method: 'estimatedDocumentCount', operation: 'estimatedDocumentCount' },
775
+ ];
776
+
777
+ for (const { method, operation } of queryMethods) {
778
+ wrapQueryReturningMethod(
779
+ m.Model,
780
+ method,
781
+ operation,
782
+ getModelCollectionName,
783
+ (model: any) => model.modelName,
784
+ tracer,
785
+ finalConfig,
786
+ captureStatement,
787
+ );
788
+
789
+ // Also patch chainable Query methods to capture context
790
+ if (m.Query?.prototype?.[method]) {
791
+ wrapChainableMethod(m.Query.prototype, method);
792
+ }
793
+ }
794
+
795
+ // Patch Model instance methods
796
+ const instanceMethods = ['save', 'deleteOne'];
797
+ for (const method of instanceMethods) {
798
+ if (m.Model.prototype[method]) {
799
+ wrapInstanceMethod(
800
+ m.Model.prototype,
801
+ method,
802
+ method,
803
+ (doc: any) => {
804
+ try {
805
+ return (
806
+ doc.constructor?.collection?.collectionName ||
807
+ doc.constructor?.modelName
808
+ );
809
+ } catch {
810
+ return;
811
+ }
812
+ },
813
+ (doc: any) => {
814
+ try {
815
+ return doc.constructor?.modelName;
816
+ } catch {
817
+ return;
818
+ }
819
+ },
820
+ tracer,
821
+ finalConfig,
822
+ captureStatement,
823
+ );
824
+ }
825
+ }
826
+
827
+ // Patch Model static methods
828
+ const staticMethods = ['create', 'insertMany', 'aggregate', 'bulkWrite'];
829
+ for (const method of staticMethods) {
830
+ if (m.Model[method]) {
831
+ wrapStaticMethod(
832
+ m.Model,
833
+ method,
834
+ method,
835
+ (model: any) => {
836
+ try {
837
+ return model.collection?.collectionName;
838
+ } catch {
839
+ return;
840
+ }
841
+ },
842
+ (model: any) => model.modelName,
843
+ tracer,
844
+ finalConfig,
845
+ captureStatement,
846
+ );
847
+ }
848
+ }
849
+
850
+ // Patch Query chainable methods
851
+ const chainableMethods = [
852
+ 'populate',
853
+ 'select',
854
+ 'lean',
855
+ 'where',
856
+ 'sort',
857
+ 'limit',
858
+ 'skip',
859
+ ];
860
+ for (const method of chainableMethods) {
861
+ if (m.Query?.prototype?.[method]) {
862
+ wrapChainableMethod(m.Query.prototype, method);
863
+ }
864
+ }
865
+
866
+ m[INSTRUMENTED_FLAG] = true;
867
+ return mongoose;
868
+ }
869
+
870
+ /**
871
+ * Legacy export for backwards compatibility.
872
+ * @deprecated Use `instrumentMongoose` instead.
873
+ */
874
+ export class MongooseInstrumentation {
875
+ constructor(private config?: InstrumentMongooseConfig) {}
876
+
877
+ enable(mongoose: Mongoose): void {
878
+ instrumentMongoose(mongoose, this.config);
879
+ }
880
+ }