autotel-mongoose 8.1.0 → 9.0.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.
package/dist/index.cjs CHANGED
@@ -1,970 +1,852 @@
1
- 'use strict';
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let autotel = require("autotel");
3
+ let autotel_trace_helpers = require("autotel/trace-helpers");
2
4
 
3
- var autotel = require('autotel');
4
- var traceHelpers = require('autotel/trace-helpers');
5
+ //#region src/constants.ts
6
+ const ATTR_DB_QUERY_TEXT = "db.query.text";
7
+ const ATTR_DB_OPERATION_NAME = "db.operation.name";
8
+ const ATTR_DB_SYSTEM_NAME = "db.system.name";
9
+ const ATTR_DB_COLLECTION_NAME = "db.collection.name";
10
+ const ATTR_DB_NAMESPACE = "db.namespace";
11
+ const ATTR_SERVER_ADDRESS = "server.address";
12
+ const ATTR_SERVER_PORT = "server.port";
13
+ const ATTR_CODE_FUNCTION_NAME = "code.function.name";
14
+ const ATTR_MONGOOSE_METHOD_NAME = "mongoose.method.name";
15
+ /** One of: "static" | "instance" | "query". */
16
+ const ATTR_MONGOOSE_METHOD_TYPE = "mongoose.method.type";
17
+ const ATTR_MONGOOSE_METHOD_MODEL = "mongoose.method.model";
18
+ const ATTR_MONGOOSE_METHOD_PARAMETERS = "mongoose.method.parameters";
19
+ const ATTR_MONGOOSE_METHOD_PARAMETER_COUNT = "mongoose.method.parameter_count";
20
+ const DB_SYSTEM_NAME_VALUE_MONGODB = "mongodb";
5
21
 
6
- // src/instrumentation.ts
22
+ //#endregion
23
+ //#region src/types.ts
24
+ const DEFAULT_TRACER_NAME = "autotel-mongoose";
7
25
 
8
- // src/constants.ts
9
- var ATTR_DB_QUERY_TEXT = "db.query.text";
10
- var ATTR_DB_OPERATION_NAME = "db.operation.name";
11
- var ATTR_DB_SYSTEM_NAME = "db.system.name";
12
- var ATTR_DB_COLLECTION_NAME = "db.collection.name";
13
- var ATTR_DB_NAMESPACE = "db.namespace";
14
- var ATTR_SERVER_ADDRESS = "server.address";
15
- var ATTR_SERVER_PORT = "server.port";
16
- var ATTR_CODE_FUNCTION_NAME = "code.function.name";
17
- var ATTR_MONGOOSE_METHOD_NAME = "mongoose.method.name";
18
- var ATTR_MONGOOSE_METHOD_TYPE = "mongoose.method.type";
19
- var ATTR_MONGOOSE_METHOD_MODEL = "mongoose.method.model";
20
- var ATTR_MONGOOSE_METHOD_PARAMETERS = "mongoose.method.parameters";
21
- var ATTR_MONGOOSE_METHOD_PARAMETER_COUNT = "mongoose.method.parameter_count";
22
- var DB_SYSTEM_NAME_VALUE_MONGODB = "mongodb";
23
-
24
- // src/types.ts
25
- var DEFAULT_TRACER_NAME = "autotel-mongoose";
26
+ //#endregion
27
+ //#region src/statement.ts
28
+ /**
29
+ * Default serializer — JSON.stringify of the payload.
30
+ */
26
31
  function defaultSerializer(_operation, payload) {
27
- return JSON.stringify(payload);
32
+ return JSON.stringify(payload);
28
33
  }
34
+ /**
35
+ * Composes the serializer and redactor into a single capture function.
36
+ * Returns undefined if statement capture is disabled.
37
+ */
29
38
  function createStatementCapture(config) {
30
- if (config.dbStatementSerializer === false) {
31
- return () => {
32
- return;
33
- };
34
- }
35
- const serializer = typeof config.dbStatementSerializer === "function" ? config.dbStatementSerializer : defaultSerializer;
36
- let redact;
37
- if (config.statementRedactor !== false && config.statementRedactor !== void 0) {
38
- redact = autotel.createStringRedactor(config.statementRedactor);
39
- }
40
- return (operation, payload) => {
41
- const raw = serializer(operation, payload);
42
- if (raw === void 0) {
43
- return void 0;
44
- }
45
- return redact ? redact(raw) : raw;
46
- };
39
+ if (config.dbStatementSerializer === false) return () => {};
40
+ const serializer = typeof config.dbStatementSerializer === "function" ? config.dbStatementSerializer : defaultSerializer;
41
+ let redact;
42
+ if (config.statementRedactor !== false && config.statementRedactor !== void 0) redact = (0, autotel.createStringRedactor)(config.statementRedactor);
43
+ return (operation, payload) => {
44
+ const raw = serializer(operation, payload);
45
+ if (raw === void 0) return;
46
+ return redact ? redact(raw) : raw;
47
+ };
47
48
  }
48
- var DEFAULT_PARAMETER_MAX_LENGTH = 2048;
49
+ const DEFAULT_PARAMETER_MAX_LENGTH = 2048;
50
+ /**
51
+ * Default serializer for custom-method arguments. Safely JSON-encodes the
52
+ * argument list, resolving Mongoose documents via `toObject()` and handling
53
+ * BigInt, functions, and circular references without throwing.
54
+ */
49
55
  function defaultParameterSerializer(args) {
50
- if (args.length === 0) {
51
- return void 0;
52
- }
53
- const seen = /* @__PURE__ */ new WeakSet();
54
- const replacer = (_key, value) => {
55
- if (typeof value === "bigint") {
56
- return value.toString();
57
- }
58
- if (typeof value === "function") {
59
- return "[Function]";
60
- }
61
- if (typeof value === "object" && value !== null) {
62
- if (seen.has(value)) {
63
- return "[Circular]";
64
- }
65
- seen.add(value);
66
- }
67
- return value;
68
- };
69
- const normalized = args.map((arg) => {
70
- if (arg !== null && typeof arg === "object" && typeof arg.toObject === "function") {
71
- try {
72
- return arg.toObject();
73
- } catch {
74
- return arg;
75
- }
76
- }
77
- return arg;
78
- });
79
- try {
80
- const json = JSON.stringify(normalized, replacer);
81
- return json === void 0 ? void 0 : json;
82
- } catch {
83
- return void 0;
84
- }
56
+ if (args.length === 0) return;
57
+ const seen = /* @__PURE__ */ new WeakSet();
58
+ const replacer = (_key, value) => {
59
+ if (typeof value === "bigint") return value.toString();
60
+ if (typeof value === "function") return "[Function]";
61
+ if (typeof value === "object" && value !== null) {
62
+ if (seen.has(value)) return "[Circular]";
63
+ seen.add(value);
64
+ }
65
+ return value;
66
+ };
67
+ const normalized = args.map((arg) => {
68
+ if (arg !== null && typeof arg === "object" && typeof arg.toObject === "function") try {
69
+ return arg.toObject();
70
+ } catch {
71
+ return arg;
72
+ }
73
+ return arg;
74
+ });
75
+ try {
76
+ const json = JSON.stringify(normalized, replacer);
77
+ return json === void 0 ? void 0 : json;
78
+ } catch {
79
+ return;
80
+ }
85
81
  }
82
+ /**
83
+ * Composes a parameter serializer + redactor + length cap into a single
84
+ * capture function for custom-method arguments. The redactor defaults to the
85
+ * instrumentation's `statementRedactor` so parameters get the same PII
86
+ * protection as `db.query.text`.
87
+ */
86
88
  function createParameterCapture(config) {
87
- const { parameterConfig } = config;
88
- const maxLength = parameterConfig?.maxLength ?? DEFAULT_PARAMETER_MAX_LENGTH;
89
- const serialize = parameterConfig?.serializer ?? defaultParameterSerializer;
90
- const redactorSetting = parameterConfig?.redactor === void 0 ? config.statementRedactor : parameterConfig.redactor;
91
- let redact;
92
- if (redactorSetting !== false && redactorSetting !== void 0) {
93
- redact = autotel.createStringRedactor(redactorSetting);
94
- }
95
- return (args, context) => {
96
- const raw = serialize(args, context);
97
- if (raw === void 0) {
98
- return void 0;
99
- }
100
- const redacted = redact ? redact(raw) : raw;
101
- return redacted.length > maxLength ? `${redacted.slice(0, maxLength)}\u2026[truncated]` : redacted;
102
- };
89
+ const { parameterConfig } = config;
90
+ const maxLength = parameterConfig?.maxLength ?? DEFAULT_PARAMETER_MAX_LENGTH;
91
+ const serialize = parameterConfig?.serializer ?? defaultParameterSerializer;
92
+ const redactorSetting = parameterConfig?.redactor === void 0 ? config.statementRedactor : parameterConfig.redactor;
93
+ let redact;
94
+ if (redactorSetting !== false && redactorSetting !== void 0) redact = (0, autotel.createStringRedactor)(redactorSetting);
95
+ return (args, context) => {
96
+ const raw = serialize(args, context);
97
+ if (raw === void 0) return;
98
+ const redacted = redact ? redact(raw) : raw;
99
+ return redacted.length > maxLength ? `${redacted.slice(0, maxLength)}…[truncated]` : redacted;
100
+ };
103
101
  }
104
102
 
105
- // src/instrumentation.ts
106
- var INSTRUMENTED_FLAG = "__autotelMongooseInstrumented";
107
- var WRAPPED_HOOK_FLAG = "__autotelWrappedHook";
108
- var WRAPPED_METHOD_FLAG = "__autotelWrappedMethod";
109
- var MODEL_PATCHED_FLAG = "__autotelModelPatched";
110
- var INSTANCE_REGISTRY = /* @__PURE__ */ new WeakMap();
103
+ //#endregion
104
+ //#region src/instrumentation.ts
105
+ const INSTRUMENTED_FLAG = "__autotelMongooseInstrumented";
106
+ const WRAPPED_HOOK_FLAG = "__autotelWrappedHook";
107
+ const WRAPPED_METHOD_FLAG = "__autotelWrappedMethod";
108
+ const MODEL_PATCHED_FLAG = "__autotelModelPatched";
109
+ /**
110
+ * Per-Mongoose-instance registry of the resolved tracer + config.
111
+ *
112
+ * Custom-function wrappers are installed once on the (potentially shared)
113
+ * schema object, so they must NOT close over a single instance's
114
+ * tracer/config — a schema reused across instances/connections would otherwise
115
+ * be permanently bound to whichever instrumented it first. Instead each wrapper
116
+ * resolves the owning Mongoose instance from its runtime `this` and looks up
117
+ * the config here at call time. An instance that was never instrumented (or has
118
+ * custom methods disabled) is absent, so its calls pass straight through.
119
+ */
120
+ const INSTANCE_REGISTRY = /* @__PURE__ */ new WeakMap();
121
+ /** Resolves the owning Mongoose instance from a custom function's `this`. */
111
122
  function resolveMongooseInstance(self, methodType) {
112
- try {
113
- switch (methodType) {
114
- case "static": {
115
- return self?.base ?? self?.db?.base;
116
- }
117
- case "instance": {
118
- return self?.constructor?.base ?? self?.db?.base;
119
- }
120
- case "query": {
121
- return self?.model?.base;
122
- }
123
- }
124
- } catch {
125
- }
126
- return void 0;
123
+ try {
124
+ switch (methodType) {
125
+ case "static": return self?.base ?? self?.db?.base;
126
+ case "instance": return self?.constructor?.base ?? self?.db?.base;
127
+ case "query": return self?.model?.base;
128
+ }
129
+ } catch {}
127
130
  }
131
+ /** Picks the selector for a given method category from a resolved config. */
128
132
  function selectorFor(cm, methodType) {
129
- switch (methodType) {
130
- case "static": {
131
- return cm.statics;
132
- }
133
- case "instance": {
134
- return cm.methods;
135
- }
136
- case "query": {
137
- return cm.query;
138
- }
139
- }
133
+ switch (methodType) {
134
+ case "static": return cm.statics;
135
+ case "instance": return cm.methods;
136
+ case "query": return cm.query;
137
+ }
140
138
  }
141
- var _STORED_PARENT_SPAN = /* @__PURE__ */ Symbol("stored-parent-span");
139
+ /**
140
+ * Symbol used to store the parent span on Query/Aggregate objects.
141
+ * This preserves context across chainable query methods.
142
+ */
143
+ const _STORED_PARENT_SPAN = Symbol("stored-parent-span");
144
+ /**
145
+ * Creates a span for a Mongoose operation.
146
+ * Note: db.query.text is NOT set here — callers set it after payload extraction.
147
+ */
142
148
  function createSpan(tracer, operation, modelName, collectionName, config) {
143
- const spanName = collectionName ? `${operation} ${collectionName}` : modelName ? `${operation} ${modelName}` : `mongoose.${operation}`;
144
- const attributes = {
145
- [ATTR_DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB,
146
- [ATTR_DB_OPERATION_NAME]: operation
147
- };
148
- if (collectionName && config.captureCollectionName) {
149
- attributes[ATTR_DB_COLLECTION_NAME] = collectionName;
150
- }
151
- if (config.dbName) {
152
- attributes[ATTR_DB_NAMESPACE] = config.dbName;
153
- }
154
- if (config.peerName) {
155
- attributes[ATTR_SERVER_ADDRESS] = config.peerName;
156
- }
157
- if (config.peerPort) {
158
- attributes[ATTR_SERVER_PORT] = config.peerPort;
159
- }
160
- return tracer.startSpan(spanName, { kind: autotel.SpanKind.CLIENT, attributes });
149
+ const spanName = collectionName ? `${operation} ${collectionName}` : modelName ? `${operation} ${modelName}` : `mongoose.${operation}`;
150
+ const attributes = {
151
+ [ATTR_DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB,
152
+ [ATTR_DB_OPERATION_NAME]: operation
153
+ };
154
+ if (collectionName && config.captureCollectionName) attributes[ATTR_DB_COLLECTION_NAME] = collectionName;
155
+ if (config.dbName) attributes[ATTR_DB_NAMESPACE] = config.dbName;
156
+ if (config.peerName) attributes[ATTR_SERVER_ADDRESS] = config.peerName;
157
+ if (config.peerPort) attributes[ATTR_SERVER_PORT] = config.peerPort;
158
+ return tracer.startSpan(spanName, {
159
+ kind: autotel.SpanKind.CLIENT,
160
+ attributes
161
+ });
161
162
  }
163
+ /**
164
+ * Wraps Model methods that return Query objects (find, findOne, findById,
165
+ * findOneAndUpdate, findOneAndDelete, findOneAndReplace, deleteOne, deleteMany,
166
+ * updateOne, updateMany, countDocuments, estimatedDocumentCount).
167
+ *
168
+ * Creates span FIRST, calls original, extracts payload from the returned Query,
169
+ * sets db.query.text AFTER extraction, then wraps exec() to finalize span.
170
+ */
162
171
  function wrapQueryReturningMethod(target, methodName, operation, getCollectionName, getModelName, tracer, config, captureStatement) {
163
- const original = target[methodName];
164
- if (typeof original !== "function") {
165
- return;
166
- }
167
- target[methodName] = function instrumented(...args) {
168
- const collectionName = getCollectionName(this);
169
- const modelName = getModelName(this);
170
- const span = createSpan(
171
- tracer,
172
- operation,
173
- modelName,
174
- collectionName,
175
- config
176
- );
177
- return traceHelpers.runWithSpan(span, () => {
178
- try {
179
- const result = original.apply(this, args);
180
- if (result && typeof result.exec === "function") {
181
- try {
182
- const payload = {};
183
- if (typeof result.getFilter === "function") {
184
- payload.condition = result.getFilter();
185
- }
186
- if (result._update !== void 0) {
187
- payload.updates = result._update;
188
- }
189
- if (typeof result.getOptions === "function") {
190
- payload.options = result.getOptions();
191
- }
192
- if (result._fields !== void 0) {
193
- payload.fields = result._fields;
194
- }
195
- const statementText = captureStatement(operation, payload);
196
- if (statementText) {
197
- span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
198
- }
199
- } catch {
200
- }
201
- const originalExec = result.exec.bind(result);
202
- result.exec = function wrappedExec() {
203
- try {
204
- const execPromise = originalExec();
205
- return Promise.resolve(execPromise).then((value) => {
206
- traceHelpers.finalizeSpan(span);
207
- return value;
208
- }).catch((error) => {
209
- traceHelpers.finalizeSpan(
210
- span,
211
- error instanceof Error ? error : new Error(String(error))
212
- );
213
- throw error;
214
- });
215
- } catch (error) {
216
- traceHelpers.finalizeSpan(
217
- span,
218
- error instanceof Error ? error : new Error(String(error))
219
- );
220
- throw error;
221
- }
222
- };
223
- return result;
224
- }
225
- traceHelpers.finalizeSpan(span);
226
- return result;
227
- } catch (error) {
228
- traceHelpers.finalizeSpan(
229
- span,
230
- error instanceof Error ? error : new Error(String(error))
231
- );
232
- throw error;
233
- }
234
- });
235
- };
172
+ const original = target[methodName];
173
+ if (typeof original !== "function") return;
174
+ target[methodName] = function instrumented(...args) {
175
+ const collectionName = getCollectionName(this);
176
+ const span = createSpan(tracer, operation, getModelName(this), collectionName, config);
177
+ return (0, autotel_trace_helpers.runWithSpan)(span, () => {
178
+ try {
179
+ const result = original.apply(this, args);
180
+ if (result && typeof result.exec === "function") {
181
+ try {
182
+ const payload = {};
183
+ if (typeof result.getFilter === "function") payload.condition = result.getFilter();
184
+ if (result._update !== void 0) payload.updates = result._update;
185
+ if (typeof result.getOptions === "function") payload.options = result.getOptions();
186
+ if (result._fields !== void 0) payload.fields = result._fields;
187
+ const statementText = captureStatement(operation, payload);
188
+ if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
189
+ } catch {}
190
+ const originalExec = result.exec.bind(result);
191
+ result.exec = function wrappedExec() {
192
+ try {
193
+ const execPromise = originalExec();
194
+ return Promise.resolve(execPromise).then((value) => {
195
+ (0, autotel_trace_helpers.finalizeSpan)(span);
196
+ return value;
197
+ }).catch((error) => {
198
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
199
+ throw error;
200
+ });
201
+ } catch (error) {
202
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
203
+ throw error;
204
+ }
205
+ };
206
+ return result;
207
+ }
208
+ (0, autotel_trace_helpers.finalizeSpan)(span);
209
+ return result;
210
+ } catch (error) {
211
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
212
+ throw error;
213
+ }
214
+ });
215
+ };
236
216
  }
217
+ /**
218
+ * Wraps Model static methods (create, insertMany, aggregate, bulkWrite).
219
+ *
220
+ * Builds payload from args BEFORE calling original (args are available
221
+ * immediately), creates span, sets db.query.text, calls original, then wraps
222
+ * exec() or promise for span finalization.
223
+ */
237
224
  function wrapStaticMethod(target, methodName, operation, getCollectionName, getModelName, tracer, config, captureStatement) {
238
- const original = target[methodName];
239
- if (typeof original !== "function") {
240
- return;
241
- }
242
- target[methodName] = function instrumented(...args) {
243
- const collectionName = getCollectionName(this);
244
- const modelName = getModelName(this);
245
- const payload = {};
246
- try {
247
- switch (operation) {
248
- case "create": {
249
- payload.document = args[0];
250
- break;
251
- }
252
- case "insertMany": {
253
- payload.documents = args[0];
254
- break;
255
- }
256
- case "aggregate": {
257
- payload.aggregatePipeline = args[0];
258
- break;
259
- }
260
- case "bulkWrite": {
261
- payload.operations = args[0];
262
- break;
263
- }
264
- default: {
265
- break;
266
- }
267
- }
268
- } catch {
269
- }
270
- const span = createSpan(
271
- tracer,
272
- operation,
273
- modelName,
274
- collectionName,
275
- config
276
- );
277
- try {
278
- const statementText = captureStatement(operation, payload);
279
- if (statementText) {
280
- span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
281
- }
282
- } catch {
283
- }
284
- return traceHelpers.runWithSpan(span, () => {
285
- try {
286
- const result = original.apply(this, args);
287
- if (result && typeof result.exec === "function") {
288
- const originalExec = result.exec.bind(result);
289
- result.exec = function wrappedExec() {
290
- try {
291
- const execPromise = originalExec();
292
- return Promise.resolve(execPromise).then((value) => {
293
- traceHelpers.finalizeSpan(span);
294
- return value;
295
- }).catch((error) => {
296
- traceHelpers.finalizeSpan(
297
- span,
298
- error instanceof Error ? error : new Error(String(error))
299
- );
300
- throw error;
301
- });
302
- } catch (error) {
303
- traceHelpers.finalizeSpan(
304
- span,
305
- error instanceof Error ? error : new Error(String(error))
306
- );
307
- throw error;
308
- }
309
- };
310
- return result;
311
- }
312
- if (result && typeof result.then === "function") {
313
- return Promise.resolve(result).then((value) => {
314
- traceHelpers.finalizeSpan(span);
315
- return value;
316
- }).catch((error) => {
317
- traceHelpers.finalizeSpan(
318
- span,
319
- error instanceof Error ? error : new Error(String(error))
320
- );
321
- throw error;
322
- });
323
- }
324
- traceHelpers.finalizeSpan(span);
325
- return result;
326
- } catch (error) {
327
- traceHelpers.finalizeSpan(
328
- span,
329
- error instanceof Error ? error : new Error(String(error))
330
- );
331
- throw error;
332
- }
333
- });
334
- };
225
+ const original = target[methodName];
226
+ if (typeof original !== "function") return;
227
+ target[methodName] = function instrumented(...args) {
228
+ const collectionName = getCollectionName(this);
229
+ const modelName = getModelName(this);
230
+ const payload = {};
231
+ try {
232
+ switch (operation) {
233
+ case "create":
234
+ payload.document = args[0];
235
+ break;
236
+ case "insertMany":
237
+ payload.documents = args[0];
238
+ break;
239
+ case "aggregate":
240
+ payload.aggregatePipeline = args[0];
241
+ break;
242
+ case "bulkWrite":
243
+ payload.operations = args[0];
244
+ break;
245
+ default: break;
246
+ }
247
+ } catch {}
248
+ const span = createSpan(tracer, operation, modelName, collectionName, config);
249
+ try {
250
+ const statementText = captureStatement(operation, payload);
251
+ if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
252
+ } catch {}
253
+ return (0, autotel_trace_helpers.runWithSpan)(span, () => {
254
+ try {
255
+ const result = original.apply(this, args);
256
+ if (result && typeof result.exec === "function") {
257
+ const originalExec = result.exec.bind(result);
258
+ result.exec = function wrappedExec() {
259
+ try {
260
+ const execPromise = originalExec();
261
+ return Promise.resolve(execPromise).then((value) => {
262
+ (0, autotel_trace_helpers.finalizeSpan)(span);
263
+ return value;
264
+ }).catch((error) => {
265
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
266
+ throw error;
267
+ });
268
+ } catch (error) {
269
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
270
+ throw error;
271
+ }
272
+ };
273
+ return result;
274
+ }
275
+ if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
276
+ (0, autotel_trace_helpers.finalizeSpan)(span);
277
+ return value;
278
+ }).catch((error) => {
279
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
280
+ throw error;
281
+ });
282
+ (0, autotel_trace_helpers.finalizeSpan)(span);
283
+ return result;
284
+ } catch (error) {
285
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
286
+ throw error;
287
+ }
288
+ });
289
+ };
335
290
  }
291
+ /**
292
+ * Wraps Model instance methods (save, deleteOne on prototype).
293
+ *
294
+ * Extracts document via `this.toObject()` BEFORE calling original,
295
+ * creates span, sets db.query.text, calls original, wraps promise
296
+ * for span finalization.
297
+ */
336
298
  function wrapInstanceMethod(target, methodName, operation, getCollectionName, getModelName, tracer, config, captureStatement) {
337
- const original = target[methodName];
338
- if (typeof original !== "function") {
339
- return;
340
- }
341
- target[methodName] = function instrumented(...args) {
342
- const collectionName = getCollectionName(this);
343
- const modelName = getModelName(this);
344
- const payload = {};
345
- try {
346
- if (typeof this.toObject === "function") {
347
- payload.document = this.toObject();
348
- }
349
- } catch {
350
- }
351
- const span = createSpan(
352
- tracer,
353
- operation,
354
- modelName,
355
- collectionName,
356
- config
357
- );
358
- try {
359
- const statementText = captureStatement(operation, payload);
360
- if (statementText) {
361
- span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
362
- }
363
- } catch {
364
- }
365
- return traceHelpers.runWithSpan(span, () => {
366
- try {
367
- const result = original.apply(this, args);
368
- if (result && typeof result.then === "function") {
369
- return Promise.resolve(result).then((value) => {
370
- traceHelpers.finalizeSpan(span);
371
- return value;
372
- }).catch((error) => {
373
- traceHelpers.finalizeSpan(
374
- span,
375
- error instanceof Error ? error : new Error(String(error))
376
- );
377
- throw error;
378
- });
379
- }
380
- traceHelpers.finalizeSpan(span);
381
- return result;
382
- } catch (error) {
383
- traceHelpers.finalizeSpan(
384
- span,
385
- error instanceof Error ? error : new Error(String(error))
386
- );
387
- throw error;
388
- }
389
- });
390
- };
299
+ const original = target[methodName];
300
+ if (typeof original !== "function") return;
301
+ target[methodName] = function instrumented(...args) {
302
+ const collectionName = getCollectionName(this);
303
+ const modelName = getModelName(this);
304
+ const payload = {};
305
+ try {
306
+ if (typeof this.toObject === "function") payload.document = this.toObject();
307
+ } catch {}
308
+ const span = createSpan(tracer, operation, modelName, collectionName, config);
309
+ try {
310
+ const statementText = captureStatement(operation, payload);
311
+ if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
312
+ } catch {}
313
+ return (0, autotel_trace_helpers.runWithSpan)(span, () => {
314
+ try {
315
+ const result = original.apply(this, args);
316
+ if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
317
+ (0, autotel_trace_helpers.finalizeSpan)(span);
318
+ return value;
319
+ }).catch((error) => {
320
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
321
+ throw error;
322
+ });
323
+ (0, autotel_trace_helpers.finalizeSpan)(span);
324
+ return result;
325
+ } catch (error) {
326
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
327
+ throw error;
328
+ }
329
+ });
330
+ };
391
331
  }
332
+ /**
333
+ * Wraps chainable Query methods (populate, select, lean, etc.) to capture span context.
334
+ */
392
335
  function wrapChainableMethod(target, methodName) {
393
- const original = target[methodName];
394
- if (typeof original !== "function") {
395
- return;
396
- }
397
- target[methodName] = function captureContext(...args) {
398
- const currentSpan = traceHelpers.getActiveSpan();
399
- const result = original.apply(this, args);
400
- if (result && typeof result.exec === "function") {
401
- result[_STORED_PARENT_SPAN] = currentSpan;
402
- }
403
- return result;
404
- };
336
+ const original = target[methodName];
337
+ if (typeof original !== "function") return;
338
+ target[methodName] = function captureContext(...args) {
339
+ const currentSpan = (0, autotel_trace_helpers.getActiveSpan)();
340
+ const result = original.apply(this, args);
341
+ if (result && typeof result.exec === "function") result[_STORED_PARENT_SPAN] = currentSpan;
342
+ return result;
343
+ };
405
344
  }
345
+ /**
346
+ * Patches Mongoose Schema hooks (pre/post) to automatically trace them.
347
+ * Only wraps user-defined hooks, skipping Mongoose's internal hooks.
348
+ */
406
349
  function patchSchemaHooks(Schema, tracer, config) {
407
- if (!Schema?.prototype) {
408
- return;
409
- }
410
- const HOOK_FLAG = "__autotelHookInstrumented";
411
- if (Schema.prototype[HOOK_FLAG]) {
412
- return;
413
- }
414
- const originalPre = Schema.prototype.pre;
415
- if (typeof originalPre === "function") {
416
- Schema.prototype.pre = function(hookName, ...args) {
417
- const handler = typeof args[0] === "function" ? args[0] : typeof args[1] === "function" ? args[1] : null;
418
- if (handler && !isMongooseInternalHook(handler)) {
419
- const wrapped = wrapHookHandler(
420
- handler,
421
- hookName,
422
- "pre",
423
- tracer,
424
- config
425
- );
426
- if (typeof args[0] === "function") {
427
- args[0] = wrapped;
428
- } else if (typeof args[1] === "function") {
429
- args[1] = wrapped;
430
- }
431
- }
432
- return Reflect.apply(originalPre, this, [hookName, ...args]);
433
- };
434
- }
435
- const originalPost = Schema.prototype.post;
436
- if (typeof originalPost === "function") {
437
- Schema.prototype.post = function(hookName, ...args) {
438
- const handler = typeof args[0] === "function" ? args[0] : typeof args[1] === "function" ? args[1] : null;
439
- if (handler && !isMongooseInternalHook(handler)) {
440
- const wrapped = wrapHookHandler(
441
- handler,
442
- hookName,
443
- "post",
444
- tracer,
445
- config
446
- );
447
- if (typeof args[0] === "function") {
448
- args[0] = wrapped;
449
- } else if (typeof args[1] === "function") {
450
- args[1] = wrapped;
451
- }
452
- }
453
- return Reflect.apply(originalPost, this, [hookName, ...args]);
454
- };
455
- }
456
- Schema.prototype[HOOK_FLAG] = true;
350
+ if (!Schema?.prototype) return;
351
+ const HOOK_FLAG = "__autotelHookInstrumented";
352
+ if (Schema.prototype[HOOK_FLAG]) return;
353
+ const originalPre = Schema.prototype.pre;
354
+ if (typeof originalPre === "function") Schema.prototype.pre = function(hookName, ...args) {
355
+ const handler = typeof args[0] === "function" ? args[0] : typeof args[1] === "function" ? args[1] : null;
356
+ if (handler && !isMongooseInternalHook(handler)) {
357
+ const wrapped = wrapHookHandler(handler, hookName, "pre", tracer, config);
358
+ if (typeof args[0] === "function") args[0] = wrapped;
359
+ else if (typeof args[1] === "function") args[1] = wrapped;
360
+ }
361
+ return Reflect.apply(originalPre, this, [hookName, ...args]);
362
+ };
363
+ const originalPost = Schema.prototype.post;
364
+ if (typeof originalPost === "function") Schema.prototype.post = function(hookName, ...args) {
365
+ const handler = typeof args[0] === "function" ? args[0] : typeof args[1] === "function" ? args[1] : null;
366
+ if (handler && !isMongooseInternalHook(handler)) {
367
+ const wrapped = wrapHookHandler(handler, hookName, "post", tracer, config);
368
+ if (typeof args[0] === "function") args[0] = wrapped;
369
+ else if (typeof args[1] === "function") args[1] = wrapped;
370
+ }
371
+ return Reflect.apply(originalPost, this, [hookName, ...args]);
372
+ };
373
+ Schema.prototype[HOOK_FLAG] = true;
457
374
  }
375
+ /**
376
+ * Detects if a hook handler is from Mongoose's internal code.
377
+ * Skips private methods, known internal patterns, and functions with
378
+ * Mongoose-internal source code signatures.
379
+ *
380
+ * Note: We intentionally allow anonymous functions because user-defined
381
+ * hooks are often anonymous (e.g., `schema.pre('save', async function() {...})`).
382
+ */
458
383
  function isMongooseInternalHook(handler) {
459
- if (typeof handler !== "function") {
460
- return false;
461
- }
462
- const funcName = handler.name || "";
463
- if (funcName.startsWith("_") || funcName.startsWith("$")) {
464
- return true;
465
- }
466
- const mongooseInternalNamePatterns = [
467
- "shardingPlugin",
468
- "mongooseInternalHook",
469
- "noop",
470
- "wrapped",
471
- "bound ",
472
- "timestampsPreSave",
473
- "timestampsPreUpdate",
474
- "handleTimestampOption"
475
- ];
476
- if (mongooseInternalNamePatterns.some((pattern) => funcName.includes(pattern))) {
477
- return true;
478
- }
479
- try {
480
- const source = handler.toString();
481
- const mongooseInternalSourcePatterns = [
482
- "this.$__",
483
- // Mongoose internal document methods
484
- "this.$isValid",
485
- // Mongoose validation
486
- "this.$locals",
487
- // Mongoose local properties
488
- "_this.$__",
489
- // Mongoose internal with closure
490
- "schema.s.hooks",
491
- // Mongoose hooks system
492
- "kareem"
493
- // Mongoose's hooks library
494
- ];
495
- if (mongooseInternalSourcePatterns.some((pattern) => source.includes(pattern))) {
496
- return true;
497
- }
498
- } catch {
499
- }
500
- return false;
384
+ if (typeof handler !== "function") return false;
385
+ const funcName = handler.name || "";
386
+ if (funcName.startsWith("_") || funcName.startsWith("$")) return true;
387
+ if ([
388
+ "shardingPlugin",
389
+ "mongooseInternalHook",
390
+ "noop",
391
+ "wrapped",
392
+ "bound ",
393
+ "timestampsPreSave",
394
+ "timestampsPreUpdate",
395
+ "handleTimestampOption"
396
+ ].some((pattern) => funcName.includes(pattern))) return true;
397
+ try {
398
+ const source = handler.toString();
399
+ if ([
400
+ "this.$__",
401
+ "this.$isValid",
402
+ "this.$locals",
403
+ "_this.$__",
404
+ "schema.s.hooks",
405
+ "kareem"
406
+ ].some((pattern) => source.includes(pattern))) return true;
407
+ } catch {}
408
+ return false;
501
409
  }
410
+ /**
411
+ * Wraps a hook handler to trace its execution.
412
+ * Handles both callback-style (with next) and promise-style hooks.
413
+ */
502
414
  function wrapHookHandler(handler, hookName, hookType, tracer, config) {
503
- if (typeof handler !== "function") {
504
- return handler;
505
- }
506
- if (handler[WRAPPED_HOOK_FLAG]) {
507
- return handler;
508
- }
509
- const expectsCallback = handler.length > 0;
510
- const startHookSpan = (self) => {
511
- let modelName;
512
- let collectionName;
513
- try {
514
- if (self.constructor?.modelName) {
515
- modelName = self.constructor.modelName;
516
- collectionName = self.constructor.collection?.collectionName || modelName;
517
- } else if (self.model?.modelName) {
518
- modelName = self.model.modelName;
519
- collectionName = self.model.collection?.collectionName || modelName;
520
- }
521
- } catch {
522
- }
523
- const spanName = collectionName ? `mongoose.${collectionName}.${hookType}.${hookName}` : `mongoose.hook.${hookType}.${hookName}`;
524
- const span = tracer.startSpan(spanName, { kind: autotel.SpanKind.INTERNAL });
525
- span.setAttribute("hook.type", hookType);
526
- span.setAttribute("hook.operation", hookName);
527
- if (modelName) {
528
- span.setAttribute("hook.model", modelName);
529
- }
530
- if (collectionName && config.captureCollectionName) {
531
- span.setAttribute(ATTR_DB_COLLECTION_NAME, collectionName);
532
- }
533
- span.setAttribute(ATTR_DB_SYSTEM_NAME, DB_SYSTEM_NAME_VALUE_MONGODB);
534
- if (config.dbName) {
535
- span.setAttribute(ATTR_DB_NAMESPACE, config.dbName);
536
- }
537
- return span;
538
- };
539
- const invokeHook = (self, span, args, callbackStyle) => traceHelpers.runWithSpan(span, () => {
540
- try {
541
- const result = handler.apply(self, args);
542
- if (result && typeof result.then === "function") {
543
- return Promise.resolve(result).then((value) => {
544
- traceHelpers.finalizeSpan(span);
545
- return value;
546
- }).catch((error) => {
547
- traceHelpers.finalizeSpan(
548
- span,
549
- error instanceof Error ? error : new Error(String(error))
550
- );
551
- throw error;
552
- });
553
- }
554
- if (!callbackStyle) {
555
- traceHelpers.finalizeSpan(span);
556
- }
557
- return result;
558
- } catch (error) {
559
- traceHelpers.finalizeSpan(
560
- span,
561
- error instanceof Error ? error : new Error(String(error))
562
- );
563
- throw error;
564
- }
565
- });
566
- const wrappedHook = expectsCallback ? function wrappedHookCallback(arg0, ...args) {
567
- const span = startHookSpan(this);
568
- const runtimeArgs = [arg0, ...args];
569
- const callbackIndex = runtimeArgs.findIndex(
570
- (arg) => typeof arg === "function"
571
- );
572
- const originalNext = callbackIndex === -1 ? void 0 : runtimeArgs[callbackIndex];
573
- const callArgs = callbackIndex === -1 ? runtimeArgs : runtimeArgs.filter((_, index) => index !== callbackIndex);
574
- const wrappedNext = function wrappedNext2(...nextArgs) {
575
- const err = nextArgs[0];
576
- if (err) {
577
- traceHelpers.finalizeSpan(
578
- span,
579
- err instanceof Error ? err : new Error(String(err))
580
- );
581
- } else {
582
- traceHelpers.finalizeSpan(span);
583
- }
584
- if (typeof originalNext === "function") {
585
- return originalNext.apply(this, nextArgs);
586
- }
587
- return;
588
- };
589
- return invokeHook(this, span, [wrappedNext, ...callArgs], true);
590
- } : function wrappedHookPromise(...args) {
591
- const span = startHookSpan(this);
592
- return invokeHook(this, span, args, false);
593
- };
594
- wrappedHook[WRAPPED_HOOK_FLAG] = true;
595
- return wrappedHook;
415
+ if (typeof handler !== "function") return handler;
416
+ if (handler[WRAPPED_HOOK_FLAG]) return handler;
417
+ const expectsCallback = handler.length > 0;
418
+ const startHookSpan = (self) => {
419
+ let modelName;
420
+ let collectionName;
421
+ try {
422
+ if (self.constructor?.modelName) {
423
+ modelName = self.constructor.modelName;
424
+ collectionName = self.constructor.collection?.collectionName || modelName;
425
+ } else if (self.model?.modelName) {
426
+ modelName = self.model.modelName;
427
+ collectionName = self.model.collection?.collectionName || modelName;
428
+ }
429
+ } catch {}
430
+ const spanName = collectionName ? `mongoose.${collectionName}.${hookType}.${hookName}` : `mongoose.hook.${hookType}.${hookName}`;
431
+ const span = tracer.startSpan(spanName, { kind: autotel.SpanKind.INTERNAL });
432
+ span.setAttribute("hook.type", hookType);
433
+ span.setAttribute("hook.operation", hookName);
434
+ if (modelName) span.setAttribute("hook.model", modelName);
435
+ if (collectionName && config.captureCollectionName) span.setAttribute(ATTR_DB_COLLECTION_NAME, collectionName);
436
+ span.setAttribute(ATTR_DB_SYSTEM_NAME, DB_SYSTEM_NAME_VALUE_MONGODB);
437
+ if (config.dbName) span.setAttribute(ATTR_DB_NAMESPACE, config.dbName);
438
+ return span;
439
+ };
440
+ const invokeHook = (self, span, args, callbackStyle) => (0, autotel_trace_helpers.runWithSpan)(span, () => {
441
+ try {
442
+ const result = handler.apply(self, args);
443
+ if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
444
+ (0, autotel_trace_helpers.finalizeSpan)(span);
445
+ return value;
446
+ }).catch((error) => {
447
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
448
+ throw error;
449
+ });
450
+ if (!callbackStyle) (0, autotel_trace_helpers.finalizeSpan)(span);
451
+ return result;
452
+ } catch (error) {
453
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
454
+ throw error;
455
+ }
456
+ });
457
+ const wrappedHook = expectsCallback ? function wrappedHookCallback(arg0, ...args) {
458
+ const span = startHookSpan(this);
459
+ const runtimeArgs = [arg0, ...args];
460
+ const callbackIndex = runtimeArgs.findIndex((arg) => typeof arg === "function");
461
+ const originalNext = callbackIndex === -1 ? void 0 : runtimeArgs[callbackIndex];
462
+ const callArgs = callbackIndex === -1 ? runtimeArgs : runtimeArgs.filter((_, index) => index !== callbackIndex);
463
+ return invokeHook(this, span, [function wrappedNext(...nextArgs) {
464
+ const err = nextArgs[0];
465
+ if (err) (0, autotel_trace_helpers.finalizeSpan)(span, err instanceof Error ? err : new Error(String(err)));
466
+ else (0, autotel_trace_helpers.finalizeSpan)(span);
467
+ if (typeof originalNext === "function") return originalNext.apply(this, nextArgs);
468
+ }, ...callArgs], true);
469
+ } : function wrappedHookPromise(...args) {
470
+ const span = startHookSpan(this);
471
+ return invokeHook(this, span, args, false);
472
+ };
473
+ wrappedHook[WRAPPED_HOOK_FLAG] = true;
474
+ return wrappedHook;
596
475
  }
476
+ /**
477
+ * Resolves the `customMethods` config into a concrete, defaults-applied shape.
478
+ * Omitted/true → wrap everything and capture (redacted) parameters.
479
+ */
597
480
  function resolveCustomMethods(config) {
598
- const setting = config?.customMethods;
599
- if (setting === false) {
600
- return {
601
- enabled: false,
602
- statics: false,
603
- methods: false,
604
- query: false,
605
- captureParameters: false
606
- };
607
- }
608
- const obj = setting === void 0 || setting === true ? {} : setting;
609
- const cp = obj.captureParameters;
610
- let captureParameters = false;
611
- if (cp !== false) {
612
- captureParameters = createParameterCapture({
613
- parameterConfig: cp === void 0 || cp === true ? void 0 : cp,
614
- // Parameters inherit the same PII redaction as db.query.text by default.
615
- statementRedactor: config?.statementRedactor ?? "default"
616
- });
617
- }
618
- return {
619
- enabled: true,
620
- statics: obj.statics ?? true,
621
- methods: obj.methods ?? true,
622
- query: obj.query ?? true,
623
- captureParameters
624
- };
481
+ const setting = config?.customMethods;
482
+ if (setting === false) return {
483
+ enabled: false,
484
+ statics: false,
485
+ methods: false,
486
+ query: false,
487
+ captureParameters: false
488
+ };
489
+ const obj = setting === void 0 || setting === true ? {} : setting;
490
+ const cp = obj.captureParameters;
491
+ let captureParameters = false;
492
+ if (cp !== false) captureParameters = createParameterCapture({
493
+ parameterConfig: cp === void 0 || cp === true ? void 0 : cp,
494
+ statementRedactor: config?.statementRedactor ?? "default"
495
+ });
496
+ return {
497
+ enabled: true,
498
+ statics: obj.statics ?? true,
499
+ methods: obj.methods ?? true,
500
+ query: obj.query ?? true,
501
+ captureParameters
502
+ };
625
503
  }
504
+ /**
505
+ * Evaluates whether a named function in a category should be instrumented.
506
+ * Supports boolean, include-list, and `{ include, exclude }` selectors.
507
+ */
626
508
  function selectorAllows(selector, name) {
627
- if (selector === false) {
628
- return false;
629
- }
630
- if (selector === true) {
631
- return true;
632
- }
633
- if (Array.isArray(selector)) {
634
- return selector.includes(name);
635
- }
636
- if (selector.include && !selector.include.includes(name)) {
637
- return false;
638
- }
639
- if (selector.exclude && selector.exclude.includes(name)) {
640
- return false;
641
- }
642
- return true;
509
+ if (selector === false) return false;
510
+ if (selector === true) return true;
511
+ if (Array.isArray(selector)) return selector.includes(name);
512
+ if (selector.include && !selector.include.includes(name)) return false;
513
+ if (selector.exclude && selector.exclude.includes(name)) return false;
514
+ return true;
643
515
  }
516
+ /**
517
+ * Derives model + collection names from the runtime `this` of a custom
518
+ * function, which differs by category (Model / Document / Query).
519
+ */
644
520
  function resolveModelContext(self, methodType) {
645
- try {
646
- switch (methodType) {
647
- case "static": {
648
- return {
649
- modelName: self?.modelName,
650
- collectionName: self?.collection?.collectionName || self?.modelName
651
- };
652
- }
653
- case "instance": {
654
- const ctor = self?.constructor;
655
- return {
656
- modelName: ctor?.modelName,
657
- collectionName: ctor?.collection?.collectionName || ctor?.modelName
658
- };
659
- }
660
- case "query": {
661
- const model = self?.model;
662
- return {
663
- modelName: model?.modelName,
664
- collectionName: model?.collection?.collectionName || model?.modelName
665
- };
666
- }
667
- }
668
- } catch {
669
- }
670
- return {};
521
+ try {
522
+ switch (methodType) {
523
+ case "static": return {
524
+ modelName: self?.modelName,
525
+ collectionName: self?.collection?.collectionName || self?.modelName
526
+ };
527
+ case "instance": {
528
+ const ctor = self?.constructor;
529
+ return {
530
+ modelName: ctor?.modelName,
531
+ collectionName: ctor?.collection?.collectionName || ctor?.modelName
532
+ };
533
+ }
534
+ case "query": {
535
+ const model = self?.model;
536
+ return {
537
+ modelName: model?.modelName,
538
+ collectionName: model?.collection?.collectionName || model?.modelName
539
+ };
540
+ }
541
+ }
542
+ } catch {}
543
+ return {};
671
544
  }
545
+ /**
546
+ * Wraps a single user-defined function so its invocation is traced. Purely
547
+ * observational: preserves `this`, the return value, and error propagation.
548
+ *
549
+ * The tracer, config, and selection are resolved per Mongoose instance at call
550
+ * time (see {@link INSTANCE_REGISTRY}), so a schema shared across instances or
551
+ * connections is never bound to whichever config instrumented it first. Calls
552
+ * from a non-instrumented, disabled, or de-selected instance pass straight
553
+ * through with no span.
554
+ */
672
555
  function wrapCustomFunction(original, methodName, methodType) {
673
- if (original[WRAPPED_METHOD_FLAG]) {
674
- return original;
675
- }
676
- const wrapped = function instrumentedCustomFn(...args) {
677
- const instance = resolveMongooseInstance(this, methodType);
678
- const entry = instance ? INSTANCE_REGISTRY.get(instance) : void 0;
679
- if (!entry || !entry.config.customMethods.enabled || !selectorAllows(
680
- selectorFor(entry.config.customMethods, methodType),
681
- methodName
682
- )) {
683
- return original.apply(this, args);
684
- }
685
- const { tracer, config } = entry;
686
- const captureParameters = config.customMethods.captureParameters;
687
- const { modelName, collectionName } = resolveModelContext(this, methodType);
688
- const spanName = modelName ? `mongoose.${modelName}.${methodName}` : `mongoose.${methodType}.${methodName}`;
689
- const span = tracer.startSpan(spanName, { kind: autotel.SpanKind.INTERNAL });
690
- span.setAttribute(ATTR_DB_SYSTEM_NAME, DB_SYSTEM_NAME_VALUE_MONGODB);
691
- span.setAttribute(ATTR_CODE_FUNCTION_NAME, methodName);
692
- span.setAttribute(ATTR_MONGOOSE_METHOD_NAME, methodName);
693
- span.setAttribute(ATTR_MONGOOSE_METHOD_TYPE, methodType);
694
- if (modelName) {
695
- span.setAttribute(ATTR_MONGOOSE_METHOD_MODEL, modelName);
696
- }
697
- if (collectionName && config.captureCollectionName) {
698
- span.setAttribute(ATTR_DB_COLLECTION_NAME, collectionName);
699
- }
700
- if (config.dbName) {
701
- span.setAttribute(ATTR_DB_NAMESPACE, config.dbName);
702
- }
703
- if (captureParameters) {
704
- span.setAttribute(ATTR_MONGOOSE_METHOD_PARAMETER_COUNT, args.length);
705
- try {
706
- const params = captureParameters(args, { methodName, methodType });
707
- if (params !== void 0) {
708
- span.setAttribute(ATTR_MONGOOSE_METHOD_PARAMETERS, params);
709
- }
710
- } catch {
711
- }
712
- }
713
- return traceHelpers.runWithSpan(span, () => {
714
- try {
715
- const result = original.apply(this, args);
716
- if (methodType === "query") {
717
- traceHelpers.finalizeSpan(span);
718
- return result;
719
- }
720
- if (result && typeof result.exec === "function") {
721
- const originalExec = result.exec.bind(result);
722
- result.exec = function wrappedExec() {
723
- try {
724
- return Promise.resolve(originalExec()).then((value) => {
725
- traceHelpers.finalizeSpan(span);
726
- return value;
727
- }).catch((error) => {
728
- traceHelpers.finalizeSpan(
729
- span,
730
- error instanceof Error ? error : new Error(String(error))
731
- );
732
- throw error;
733
- });
734
- } catch (error) {
735
- traceHelpers.finalizeSpan(
736
- span,
737
- error instanceof Error ? error : new Error(String(error))
738
- );
739
- throw error;
740
- }
741
- };
742
- return result;
743
- }
744
- if (result && typeof result.then === "function") {
745
- return Promise.resolve(result).then((value) => {
746
- traceHelpers.finalizeSpan(span);
747
- return value;
748
- }).catch((error) => {
749
- traceHelpers.finalizeSpan(
750
- span,
751
- error instanceof Error ? error : new Error(String(error))
752
- );
753
- throw error;
754
- });
755
- }
756
- traceHelpers.finalizeSpan(span);
757
- return result;
758
- } catch (error) {
759
- traceHelpers.finalizeSpan(
760
- span,
761
- error instanceof Error ? error : new Error(String(error))
762
- );
763
- throw error;
764
- }
765
- });
766
- };
767
- try {
768
- Object.defineProperty(wrapped, "name", {
769
- value: original.name || methodName,
770
- configurable: true
771
- });
772
- } catch {
773
- }
774
- wrapped[WRAPPED_METHOD_FLAG] = true;
775
- return wrapped;
556
+ if (original[WRAPPED_METHOD_FLAG]) return original;
557
+ const wrapped = function instrumentedCustomFn(...args) {
558
+ const instance = resolveMongooseInstance(this, methodType);
559
+ const entry = instance ? INSTANCE_REGISTRY.get(instance) : void 0;
560
+ if (!entry || !entry.config.customMethods.enabled || !selectorAllows(selectorFor(entry.config.customMethods, methodType), methodName)) return original.apply(this, args);
561
+ const { tracer, config } = entry;
562
+ const captureParameters = config.customMethods.captureParameters;
563
+ const { modelName, collectionName } = resolveModelContext(this, methodType);
564
+ const spanName = modelName ? `mongoose.${modelName}.${methodName}` : `mongoose.${methodType}.${methodName}`;
565
+ const span = tracer.startSpan(spanName, { kind: autotel.SpanKind.INTERNAL });
566
+ span.setAttribute(ATTR_DB_SYSTEM_NAME, DB_SYSTEM_NAME_VALUE_MONGODB);
567
+ span.setAttribute(ATTR_CODE_FUNCTION_NAME, methodName);
568
+ span.setAttribute(ATTR_MONGOOSE_METHOD_NAME, methodName);
569
+ span.setAttribute(ATTR_MONGOOSE_METHOD_TYPE, methodType);
570
+ if (modelName) span.setAttribute(ATTR_MONGOOSE_METHOD_MODEL, modelName);
571
+ if (collectionName && config.captureCollectionName) span.setAttribute(ATTR_DB_COLLECTION_NAME, collectionName);
572
+ if (config.dbName) span.setAttribute(ATTR_DB_NAMESPACE, config.dbName);
573
+ if (captureParameters) {
574
+ span.setAttribute(ATTR_MONGOOSE_METHOD_PARAMETER_COUNT, args.length);
575
+ try {
576
+ const params = captureParameters(args, {
577
+ methodName,
578
+ methodType
579
+ });
580
+ if (params !== void 0) span.setAttribute(ATTR_MONGOOSE_METHOD_PARAMETERS, params);
581
+ } catch {}
582
+ }
583
+ return (0, autotel_trace_helpers.runWithSpan)(span, () => {
584
+ try {
585
+ const result = original.apply(this, args);
586
+ if (methodType === "query") {
587
+ (0, autotel_trace_helpers.finalizeSpan)(span);
588
+ return result;
589
+ }
590
+ if (result && typeof result.exec === "function") {
591
+ const originalExec = result.exec.bind(result);
592
+ result.exec = function wrappedExec() {
593
+ try {
594
+ return Promise.resolve(originalExec()).then((value) => {
595
+ (0, autotel_trace_helpers.finalizeSpan)(span);
596
+ return value;
597
+ }).catch((error) => {
598
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
599
+ throw error;
600
+ });
601
+ } catch (error) {
602
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
603
+ throw error;
604
+ }
605
+ };
606
+ return result;
607
+ }
608
+ if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
609
+ (0, autotel_trace_helpers.finalizeSpan)(span);
610
+ return value;
611
+ }).catch((error) => {
612
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
613
+ throw error;
614
+ });
615
+ (0, autotel_trace_helpers.finalizeSpan)(span);
616
+ return result;
617
+ } catch (error) {
618
+ (0, autotel_trace_helpers.finalizeSpan)(span, error instanceof Error ? error : new Error(String(error)));
619
+ throw error;
620
+ }
621
+ });
622
+ };
623
+ try {
624
+ Object.defineProperty(wrapped, "name", {
625
+ value: original.name || methodName,
626
+ configurable: true
627
+ });
628
+ } catch {}
629
+ wrapped[WRAPPED_METHOD_FLAG] = true;
630
+ return wrapped;
776
631
  }
777
- var MONGOOSE_INTERNAL_FUNCTION_NAMES = /* @__PURE__ */ new Set([
778
- "initializeTimestamps"
779
- ]);
632
+ /**
633
+ * Wraps every user-defined function on a schema in place (statics / methods /
634
+ * query) at model-compile time. Mutating the schema's collections before
635
+ * compilation means Mongoose copies the wrapped versions onto the Model, its
636
+ * prototype, and its query class.
637
+ *
638
+ * Wrapping is unconditional and idempotent: each wrapper decides per Mongoose
639
+ * instance at call time whether to actually trace (honoring that instance's
640
+ * `enabled` flag and include/exclude selectors). This keeps a schema shared
641
+ * across instances correct — a function excluded by one instance can still be
642
+ * traced by another, and vice versa — while a function is only ever wrapped
643
+ * once.
644
+ */
645
+ /**
646
+ * Functions Mongoose itself injects into `schema.statics`/`methods`/`query`
647
+ * (e.g. the `timestamps: true` option adds an `initializeTimestamps` instance
648
+ * method). These are framework internals, not user code, so we skip them to
649
+ * avoid noisy spans. Names starting with `$` (Mongoose's internal prefix) are
650
+ * also skipped.
651
+ */
652
+ const MONGOOSE_INTERNAL_FUNCTION_NAMES = new Set(["initializeTimestamps"]);
780
653
  function isMongooseInternalFunctionName(name) {
781
- return name.startsWith("$") || MONGOOSE_INTERNAL_FUNCTION_NAMES.has(name);
654
+ return name.startsWith("$") || MONGOOSE_INTERNAL_FUNCTION_NAMES.has(name);
782
655
  }
783
656
  function instrumentSchemaCustomFunctions(schema) {
784
- if (!schema) {
785
- return;
786
- }
787
- const wrapCollection = (collection, methodType) => {
788
- if (!collection) {
789
- return;
790
- }
791
- for (const name of Object.keys(collection)) {
792
- if (isMongooseInternalFunctionName(name)) {
793
- continue;
794
- }
795
- const fn = collection[name];
796
- if (typeof fn !== "function" || fn[WRAPPED_METHOD_FLAG]) {
797
- continue;
798
- }
799
- collection[name] = wrapCustomFunction(fn, name, methodType);
800
- }
801
- };
802
- wrapCollection(schema.statics, "static");
803
- wrapCollection(schema.methods, "instance");
804
- wrapCollection(schema.query, "query");
657
+ if (!schema) return;
658
+ const wrapCollection = (collection, methodType) => {
659
+ if (!collection) return;
660
+ for (const name of Object.keys(collection)) {
661
+ if (isMongooseInternalFunctionName(name)) continue;
662
+ const fn = collection[name];
663
+ if (typeof fn !== "function" || fn[WRAPPED_METHOD_FLAG]) continue;
664
+ collection[name] = wrapCustomFunction(fn, name, methodType);
665
+ }
666
+ };
667
+ wrapCollection(schema.statics, "static");
668
+ wrapCollection(schema.methods, "instance");
669
+ wrapCollection(schema.query, "query");
805
670
  }
671
+ /**
672
+ * Patches `mongoose.model()` (and `Connection.prototype.model()`) so custom
673
+ * functions are wrapped automatically as each model compiles. Idempotent per
674
+ * host and per function. Whether a wrapped function actually traces is decided
675
+ * per instance at call time, so it is safe for the patch to be global.
676
+ */
806
677
  function patchModelFactory(m, config) {
807
- if (!config.customMethods.enabled) {
808
- return;
809
- }
810
- const patchHost = (host) => {
811
- if (!host || typeof host.model !== "function" || host[MODEL_PATCHED_FLAG]) {
812
- return;
813
- }
814
- const originalModel = host.model;
815
- host.model = function patchedModel(nameOrSchema, schema, ...rest) {
816
- if (schema && typeof schema === "object") {
817
- try {
818
- instrumentSchemaCustomFunctions(schema);
819
- } catch {
820
- }
821
- }
822
- return Reflect.apply(originalModel, this, [
823
- nameOrSchema,
824
- schema,
825
- ...rest
826
- ]);
827
- };
828
- host[MODEL_PATCHED_FLAG] = true;
829
- };
830
- patchHost(m);
831
- if (m.Connection?.prototype) {
832
- patchHost(m.Connection.prototype);
833
- }
678
+ if (!config.customMethods.enabled) return;
679
+ const patchHost = (host) => {
680
+ if (!host || typeof host.model !== "function" || host[MODEL_PATCHED_FLAG]) return;
681
+ const originalModel = host.model;
682
+ host.model = function patchedModel(nameOrSchema, schema, ...rest) {
683
+ if (schema && typeof schema === "object") try {
684
+ instrumentSchemaCustomFunctions(schema);
685
+ } catch {}
686
+ return Reflect.apply(originalModel, this, [
687
+ nameOrSchema,
688
+ schema,
689
+ ...rest
690
+ ]);
691
+ };
692
+ host[MODEL_PATCHED_FLAG] = true;
693
+ };
694
+ patchHost(m);
695
+ if (m.Connection?.prototype) patchHost(m.Connection.prototype);
834
696
  }
697
+ /**
698
+ * Instruments Mongoose with OpenTelemetry tracing.
699
+ *
700
+ * Supports Mongoose 8+ with promise-based API only.
701
+ * Patches Model methods, Query methods, and user-defined Schema hooks to create spans.
702
+ *
703
+ * **IMPORTANT:** Call `instrumentMongoose()` BEFORE defining schemas/models
704
+ * to ensure hooks are automatically instrumented.
705
+ *
706
+ * @example
707
+ * ```typescript
708
+ * import mongoose from 'mongoose';
709
+ * import { init } from 'autotel';
710
+ * import { instrumentMongoose } from 'autotel-mongoose';
711
+ *
712
+ * init({ service: 'my-app' });
713
+ *
714
+ * // Call BEFORE defining schemas
715
+ * instrumentMongoose(mongoose, { dbName: 'myapp' });
716
+ *
717
+ * const userSchema = new mongoose.Schema({ name: String });
718
+ * const User = mongoose.model('User', userSchema);
719
+ *
720
+ * // All operations are automatically traced
721
+ * await User.findOne({}).populate('posts').exec();
722
+ * ```
723
+ */
835
724
  function instrumentMongoose(mongoose, config) {
836
- if (!mongoose?.Model) {
837
- return mongoose;
838
- }
839
- const m = mongoose;
840
- if (m[INSTRUMENTED_FLAG]) {
841
- return mongoose;
842
- }
843
- const resolvedSerializer = config?.dbStatementSerializer;
844
- const resolvedRedactor = config?.statementRedactor ?? "default";
845
- const finalConfig = {
846
- dbName: config?.dbName || "",
847
- peerName: config?.peerName || "",
848
- peerPort: config?.peerPort || 27017,
849
- tracerName: config?.tracerName || DEFAULT_TRACER_NAME,
850
- captureCollectionName: config?.captureCollectionName ?? true,
851
- instrumentHooks: config?.instrumentHooks ?? false,
852
- dbStatementSerializer: resolvedSerializer === false ? false : resolvedSerializer ?? defaultSerializer,
853
- statementRedactor: resolvedRedactor,
854
- customMethods: resolveCustomMethods(config)
855
- };
856
- const tracer = autotel.otelTrace.getTracer(finalConfig.tracerName);
857
- INSTANCE_REGISTRY.set(mongoose, { tracer, config: finalConfig });
858
- patchModelFactory(m, finalConfig);
859
- const captureStatement = createStatementCapture({
860
- dbStatementSerializer: resolvedSerializer,
861
- statementRedactor: resolvedRedactor
862
- });
863
- if (m.Schema && finalConfig.instrumentHooks) {
864
- patchSchemaHooks(m.Schema, tracer, finalConfig);
865
- }
866
- const getModelCollectionName = (model) => {
867
- try {
868
- return model.collection?.collectionName || model.modelName;
869
- } catch {
870
- return;
871
- }
872
- };
873
- const queryMethods = [
874
- { method: "find", operation: "find" },
875
- { method: "findOne", operation: "findOne" },
876
- { method: "findById", operation: "findById" },
877
- { method: "findOneAndUpdate", operation: "findOneAndUpdate" },
878
- { method: "findOneAndDelete", operation: "findOneAndDelete" },
879
- { method: "findOneAndReplace", operation: "findOneAndReplace" },
880
- { method: "deleteOne", operation: "deleteOne" },
881
- { method: "deleteMany", operation: "deleteMany" },
882
- { method: "updateOne", operation: "updateOne" },
883
- { method: "updateMany", operation: "updateMany" },
884
- { method: "countDocuments", operation: "countDocuments" },
885
- { method: "estimatedDocumentCount", operation: "estimatedDocumentCount" }
886
- ];
887
- for (const { method, operation } of queryMethods) {
888
- wrapQueryReturningMethod(
889
- m.Model,
890
- method,
891
- operation,
892
- getModelCollectionName,
893
- (model) => model.modelName,
894
- tracer,
895
- finalConfig,
896
- captureStatement
897
- );
898
- if (m.Query?.prototype?.[method]) {
899
- wrapChainableMethod(m.Query.prototype, method);
900
- }
901
- }
902
- const instanceMethods = ["save", "deleteOne"];
903
- for (const method of instanceMethods) {
904
- if (m.Model.prototype[method]) {
905
- wrapInstanceMethod(
906
- m.Model.prototype,
907
- method,
908
- method,
909
- (doc) => {
910
- try {
911
- return doc.constructor?.collection?.collectionName || doc.constructor?.modelName;
912
- } catch {
913
- return;
914
- }
915
- },
916
- (doc) => {
917
- try {
918
- return doc.constructor?.modelName;
919
- } catch {
920
- return;
921
- }
922
- },
923
- tracer,
924
- finalConfig,
925
- captureStatement
926
- );
927
- }
928
- }
929
- const staticMethods = ["create", "insertMany", "aggregate", "bulkWrite"];
930
- for (const method of staticMethods) {
931
- if (m.Model[method]) {
932
- wrapStaticMethod(
933
- m.Model,
934
- method,
935
- method,
936
- (model) => {
937
- try {
938
- return model.collection?.collectionName;
939
- } catch {
940
- return;
941
- }
942
- },
943
- (model) => model.modelName,
944
- tracer,
945
- finalConfig,
946
- captureStatement
947
- );
948
- }
949
- }
950
- const chainableMethods = [
951
- "populate",
952
- "select",
953
- "lean",
954
- "where",
955
- "sort",
956
- "limit",
957
- "skip"
958
- ];
959
- for (const method of chainableMethods) {
960
- if (m.Query?.prototype?.[method]) {
961
- wrapChainableMethod(m.Query.prototype, method);
962
- }
963
- }
964
- m[INSTRUMENTED_FLAG] = true;
965
- return mongoose;
725
+ if (!mongoose?.Model) return mongoose;
726
+ const m = mongoose;
727
+ if (m[INSTRUMENTED_FLAG]) return mongoose;
728
+ const resolvedSerializer = config?.dbStatementSerializer;
729
+ const resolvedRedactor = config?.statementRedactor ?? "default";
730
+ const finalConfig = {
731
+ dbName: config?.dbName || "",
732
+ peerName: config?.peerName || "",
733
+ peerPort: config?.peerPort || 27017,
734
+ tracerName: config?.tracerName || "autotel-mongoose",
735
+ captureCollectionName: config?.captureCollectionName ?? true,
736
+ instrumentHooks: config?.instrumentHooks ?? false,
737
+ dbStatementSerializer: resolvedSerializer === false ? false : resolvedSerializer ?? defaultSerializer,
738
+ statementRedactor: resolvedRedactor,
739
+ customMethods: resolveCustomMethods(config)
740
+ };
741
+ const tracer = autotel.otelTrace.getTracer(finalConfig.tracerName);
742
+ INSTANCE_REGISTRY.set(mongoose, {
743
+ tracer,
744
+ config: finalConfig
745
+ });
746
+ patchModelFactory(m, finalConfig);
747
+ const captureStatement = createStatementCapture({
748
+ dbStatementSerializer: resolvedSerializer,
749
+ statementRedactor: resolvedRedactor
750
+ });
751
+ if (m.Schema && finalConfig.instrumentHooks) patchSchemaHooks(m.Schema, tracer, finalConfig);
752
+ const getModelCollectionName = (model) => {
753
+ try {
754
+ return model.collection?.collectionName || model.modelName;
755
+ } catch {
756
+ return;
757
+ }
758
+ };
759
+ for (const { method, operation } of [
760
+ {
761
+ method: "find",
762
+ operation: "find"
763
+ },
764
+ {
765
+ method: "findOne",
766
+ operation: "findOne"
767
+ },
768
+ {
769
+ method: "findById",
770
+ operation: "findById"
771
+ },
772
+ {
773
+ method: "findOneAndUpdate",
774
+ operation: "findOneAndUpdate"
775
+ },
776
+ {
777
+ method: "findOneAndDelete",
778
+ operation: "findOneAndDelete"
779
+ },
780
+ {
781
+ method: "findOneAndReplace",
782
+ operation: "findOneAndReplace"
783
+ },
784
+ {
785
+ method: "deleteOne",
786
+ operation: "deleteOne"
787
+ },
788
+ {
789
+ method: "deleteMany",
790
+ operation: "deleteMany"
791
+ },
792
+ {
793
+ method: "updateOne",
794
+ operation: "updateOne"
795
+ },
796
+ {
797
+ method: "updateMany",
798
+ operation: "updateMany"
799
+ },
800
+ {
801
+ method: "countDocuments",
802
+ operation: "countDocuments"
803
+ },
804
+ {
805
+ method: "estimatedDocumentCount",
806
+ operation: "estimatedDocumentCount"
807
+ }
808
+ ]) {
809
+ wrapQueryReturningMethod(m.Model, method, operation, getModelCollectionName, (model) => model.modelName, tracer, finalConfig, captureStatement);
810
+ if (m.Query?.prototype?.[method]) wrapChainableMethod(m.Query.prototype, method);
811
+ }
812
+ for (const method of ["save", "deleteOne"]) if (m.Model.prototype[method]) wrapInstanceMethod(m.Model.prototype, method, method, (doc) => {
813
+ try {
814
+ return doc.constructor?.collection?.collectionName || doc.constructor?.modelName;
815
+ } catch {
816
+ return;
817
+ }
818
+ }, (doc) => {
819
+ try {
820
+ return doc.constructor?.modelName;
821
+ } catch {
822
+ return;
823
+ }
824
+ }, tracer, finalConfig, captureStatement);
825
+ for (const method of [
826
+ "create",
827
+ "insertMany",
828
+ "aggregate",
829
+ "bulkWrite"
830
+ ]) if (m.Model[method]) wrapStaticMethod(m.Model, method, method, (model) => {
831
+ try {
832
+ return model.collection?.collectionName;
833
+ } catch {
834
+ return;
835
+ }
836
+ }, (model) => model.modelName, tracer, finalConfig, captureStatement);
837
+ for (const method of [
838
+ "populate",
839
+ "select",
840
+ "lean",
841
+ "where",
842
+ "sort",
843
+ "limit",
844
+ "skip"
845
+ ]) if (m.Query?.prototype?.[method]) wrapChainableMethod(m.Query.prototype, method);
846
+ m[INSTRUMENTED_FLAG] = true;
847
+ return mongoose;
966
848
  }
967
849
 
850
+ //#endregion
968
851
  exports.instrumentMongoose = instrumentMongoose;
969
- //# sourceMappingURL=index.cjs.map
970
852
  //# sourceMappingURL=index.cjs.map