autotel-mongoose 10.0.0 → 10.1.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.
- package/dist/index.cjs +163 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +163 -111
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/skills/autotel-mongoose/SKILL.md +256 -0
- package/src/custom-methods.integration.test.ts +163 -0
- package/src/instrumentation.ts +239 -180
package/dist/index.cjs
CHANGED
|
@@ -106,6 +106,7 @@ const INSTRUMENTED_FLAG = "__autotelMongooseInstrumented";
|
|
|
106
106
|
const WRAPPED_HOOK_FLAG = "__autotelWrappedHook";
|
|
107
107
|
const WRAPPED_METHOD_FLAG = "__autotelWrappedMethod";
|
|
108
108
|
const MODEL_PATCHED_FLAG = "__autotelModelPatched";
|
|
109
|
+
const PROXIED_COLLECTION_FLAG = "__autotelProxiedCollection";
|
|
109
110
|
/**
|
|
110
111
|
* Per-Mongoose-instance registry of the resolved tracer + config.
|
|
111
112
|
*
|
|
@@ -161,6 +162,98 @@ function createSpan(tracer, operation, modelName, collectionName, config) {
|
|
|
161
162
|
});
|
|
162
163
|
}
|
|
163
164
|
/**
|
|
165
|
+
* Returns an idempotent finalizer for a span. Every wrapper invocation may try
|
|
166
|
+
* to close its span from more than one place — a callback, an `exec()`
|
|
167
|
+
* continuation, a promise settlement, a synchronous return, or a thrown error —
|
|
168
|
+
* and the span must be ended exactly once. The first call wins; later calls are
|
|
169
|
+
* no-ops. A non-Error rejection is normalized to an `Error`.
|
|
170
|
+
*/
|
|
171
|
+
function createSpanFinalizer(span) {
|
|
172
|
+
let done = false;
|
|
173
|
+
return (error) => {
|
|
174
|
+
if (done) return;
|
|
175
|
+
done = true;
|
|
176
|
+
(0, autotel_trace_helpers.finalizeSpan)(span, error === void 0 || error === null ? void 0 : error instanceof Error ? error : new Error(String(error)));
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Closes `finalize` over whatever async shape an operation returns:
|
|
181
|
+
*
|
|
182
|
+
* - a Mongoose Query/Aggregate (`exec()`) → wrap `exec()` so the span spans the
|
|
183
|
+
* real DB round-trip, and return the Query unchanged for further chaining;
|
|
184
|
+
* - a Promise → finalize when it settles, and return the chained promise;
|
|
185
|
+
* - a synchronous value → finalize now, and return it.
|
|
186
|
+
*
|
|
187
|
+
* This is the single settlement ladder shared by every wrapper, so the rule
|
|
188
|
+
* "the span ends when the work ends" lives in exactly one place.
|
|
189
|
+
*/
|
|
190
|
+
function settleSpan(result, finalize) {
|
|
191
|
+
if (result && typeof result.exec === "function") {
|
|
192
|
+
const originalExec = result.exec.bind(result);
|
|
193
|
+
result.exec = function wrappedExec() {
|
|
194
|
+
try {
|
|
195
|
+
return Promise.resolve(originalExec()).then((value) => {
|
|
196
|
+
finalize();
|
|
197
|
+
return value;
|
|
198
|
+
}, (error) => {
|
|
199
|
+
finalize(error);
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
202
|
+
} catch (error) {
|
|
203
|
+
finalize(error);
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
if (result && typeof result.then === "function") return Promise.resolve(result).then((value) => {
|
|
210
|
+
finalize();
|
|
211
|
+
return value;
|
|
212
|
+
}, (error) => {
|
|
213
|
+
finalize(error);
|
|
214
|
+
throw error;
|
|
215
|
+
});
|
|
216
|
+
finalize();
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Node-convention callback support for custom methods: if the last argument is
|
|
221
|
+
* a function, replace it with one that (a) runs the original callback inside the
|
|
222
|
+
* span's context — so any DB calls the callback makes nest under this span — and
|
|
223
|
+
* (b) finalizes the span when the callback fires, treating a truthy first
|
|
224
|
+
* argument as the error. Older Mongoose code returns synchronously and does its
|
|
225
|
+
* real work in such a callback (e.g. `doc.checkValidationErrors(cb)`), so the
|
|
226
|
+
* span must outlive the synchronous return.
|
|
227
|
+
*
|
|
228
|
+
* Returns the args to call with and whether finalization was handed to a
|
|
229
|
+
* callback. If there is no trailing callback, the args pass through unchanged.
|
|
230
|
+
*
|
|
231
|
+
* NOTE: This is the Node trailing-callback convention. Mongoose *hooks* use a
|
|
232
|
+
* different convention — kareem's positional `next` — handled separately in
|
|
233
|
+
* wrapHookHandler. The two are intentionally not merged: a single "find the
|
|
234
|
+
* callback" rule across both would hide two genuinely different calling shapes.
|
|
235
|
+
*/
|
|
236
|
+
function deferFinalizeToCallback(args, span, finalize) {
|
|
237
|
+
const lastIndex = args.length - 1;
|
|
238
|
+
const maybeCallback = lastIndex >= 0 ? args[lastIndex] : void 0;
|
|
239
|
+
if (typeof maybeCallback !== "function") return {
|
|
240
|
+
callArgs: args,
|
|
241
|
+
deferred: false
|
|
242
|
+
};
|
|
243
|
+
const callArgs = [...args];
|
|
244
|
+
callArgs[lastIndex] = function tracedCallback(...callbackArgs) {
|
|
245
|
+
try {
|
|
246
|
+
return (0, autotel_trace_helpers.runWithSpan)(span, () => maybeCallback.apply(this, callbackArgs));
|
|
247
|
+
} finally {
|
|
248
|
+
finalize(callbackArgs[0]);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
return {
|
|
252
|
+
callArgs,
|
|
253
|
+
deferred: true
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
164
257
|
* Wraps Model methods that return Query objects (find, findOne, findById,
|
|
165
258
|
* findOneAndUpdate, findOneAndDelete, findOneAndReplace, deleteOne, deleteMany,
|
|
166
259
|
* updateOne, updateMany, countDocuments, estimatedDocumentCount).
|
|
@@ -174,41 +267,22 @@ function wrapQueryReturningMethod(target, methodName, operation, getCollectionNa
|
|
|
174
267
|
target[methodName] = function instrumented(...args) {
|
|
175
268
|
const collectionName = getCollectionName(this);
|
|
176
269
|
const span = createSpan(tracer, operation, getModelName(this), collectionName, config);
|
|
270
|
+
const finalize = createSpanFinalizer(span);
|
|
177
271
|
return (0, autotel_trace_helpers.runWithSpan)(span, () => {
|
|
178
272
|
try {
|
|
179
273
|
const result = original.apply(this, args);
|
|
180
|
-
if (result && typeof result.exec === "function") {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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;
|
|
274
|
+
if (result && typeof result.exec === "function") try {
|
|
275
|
+
const payload = {};
|
|
276
|
+
if (typeof result.getFilter === "function") payload.condition = result.getFilter();
|
|
277
|
+
if (result._update !== void 0) payload.updates = result._update;
|
|
278
|
+
if (typeof result.getOptions === "function") payload.options = result.getOptions();
|
|
279
|
+
if (result._fields !== void 0) payload.fields = result._fields;
|
|
280
|
+
const statementText = captureStatement(operation, payload);
|
|
281
|
+
if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
|
|
282
|
+
} catch {}
|
|
283
|
+
return settleSpan(result, finalize);
|
|
210
284
|
} catch (error) {
|
|
211
|
-
(
|
|
285
|
+
finalize(error);
|
|
212
286
|
throw error;
|
|
213
287
|
}
|
|
214
288
|
});
|
|
@@ -250,39 +324,12 @@ function wrapStaticMethod(target, methodName, operation, getCollectionName, getM
|
|
|
250
324
|
const statementText = captureStatement(operation, payload);
|
|
251
325
|
if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
|
|
252
326
|
} catch {}
|
|
327
|
+
const finalize = createSpanFinalizer(span);
|
|
253
328
|
return (0, autotel_trace_helpers.runWithSpan)(span, () => {
|
|
254
329
|
try {
|
|
255
|
-
|
|
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;
|
|
330
|
+
return settleSpan(original.apply(this, args), finalize);
|
|
284
331
|
} catch (error) {
|
|
285
|
-
(
|
|
332
|
+
finalize(error);
|
|
286
333
|
throw error;
|
|
287
334
|
}
|
|
288
335
|
});
|
|
@@ -310,20 +357,12 @@ function wrapInstanceMethod(target, methodName, operation, getCollectionName, ge
|
|
|
310
357
|
const statementText = captureStatement(operation, payload);
|
|
311
358
|
if (statementText) span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
|
|
312
359
|
} catch {}
|
|
360
|
+
const finalize = createSpanFinalizer(span);
|
|
313
361
|
return (0, autotel_trace_helpers.runWithSpan)(span, () => {
|
|
314
362
|
try {
|
|
315
|
-
|
|
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;
|
|
363
|
+
return settleSpan(original.apply(this, args), finalize);
|
|
325
364
|
} catch (error) {
|
|
326
|
-
(
|
|
365
|
+
finalize(error);
|
|
327
366
|
throw error;
|
|
328
367
|
}
|
|
329
368
|
});
|
|
@@ -580,42 +619,24 @@ function wrapCustomFunction(original, methodName, methodType) {
|
|
|
580
619
|
if (params !== void 0) span.setAttribute(ATTR_MONGOOSE_METHOD_PARAMETERS, params);
|
|
581
620
|
} catch {}
|
|
582
621
|
}
|
|
622
|
+
const finalize = createSpanFinalizer(span);
|
|
583
623
|
return (0, autotel_trace_helpers.runWithSpan)(span, () => {
|
|
584
|
-
try {
|
|
624
|
+
if (methodType === "query") try {
|
|
585
625
|
const result = original.apply(this, args);
|
|
586
|
-
|
|
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);
|
|
626
|
+
finalize();
|
|
616
627
|
return result;
|
|
617
628
|
} catch (error) {
|
|
618
|
-
(
|
|
629
|
+
finalize(error);
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
const { callArgs, deferred } = deferFinalizeToCallback(args, span, finalize);
|
|
633
|
+
try {
|
|
634
|
+
const result = original.apply(this, callArgs);
|
|
635
|
+
if (result && (typeof result.exec === "function" || typeof result.then === "function")) return settleSpan(result, finalize);
|
|
636
|
+
if (!deferred) finalize();
|
|
637
|
+
return result;
|
|
638
|
+
} catch (error) {
|
|
639
|
+
finalize(error);
|
|
619
640
|
throw error;
|
|
620
641
|
}
|
|
621
642
|
});
|
|
@@ -653,20 +674,51 @@ const MONGOOSE_INTERNAL_FUNCTION_NAMES = new Set(["initializeTimestamps"]);
|
|
|
653
674
|
function isMongooseInternalFunctionName(name) {
|
|
654
675
|
return name.startsWith("$") || MONGOOSE_INTERNAL_FUNCTION_NAMES.has(name);
|
|
655
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Detects a compiled Mongoose Model assigned onto a schema collection — e.g.
|
|
679
|
+
* the `Patches` model attached to `schema.statics` by history/audit plugins
|
|
680
|
+
* (`schema.statics.Patches = mongoose.model(...)`). A Model is a constructor
|
|
681
|
+
* function carrying its own statics (`find`, `create`, …); wrapping it in a
|
|
682
|
+
* plain tracing function would drop those and break callers, so it must be
|
|
683
|
+
* skipped — both at the compile-time scan and on later assignment.
|
|
684
|
+
*/
|
|
685
|
+
function isMongooseModelLike(fn) {
|
|
686
|
+
try {
|
|
687
|
+
return typeof fn === "function" && typeof fn.modelName === "string" && fn.schema != null;
|
|
688
|
+
} catch {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/** Whether a value assigned to a schema collection should be wrapped. */
|
|
693
|
+
function shouldWrapCustomFunction(name, value) {
|
|
694
|
+
return typeof value === "function" && !isMongooseInternalFunctionName(name) && !value[WRAPPED_METHOD_FLAG] && !isMongooseModelLike(value);
|
|
695
|
+
}
|
|
656
696
|
function instrumentSchemaCustomFunctions(schema) {
|
|
657
697
|
if (!schema) return;
|
|
658
698
|
const wrapCollection = (collection, methodType) => {
|
|
659
|
-
if (!collection) return;
|
|
699
|
+
if (!collection || collection[PROXIED_COLLECTION_FLAG]) return collection;
|
|
660
700
|
for (const name of Object.keys(collection)) {
|
|
661
|
-
if (isMongooseInternalFunctionName(name)) continue;
|
|
662
701
|
const fn = collection[name];
|
|
663
|
-
if (
|
|
664
|
-
collection[name] = wrapCustomFunction(fn, name, methodType);
|
|
702
|
+
if (isMongooseModelLike(fn)) continue;
|
|
703
|
+
if (shouldWrapCustomFunction(name, fn)) collection[name] = wrapCustomFunction(fn, name, methodType);
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
Object.defineProperty(collection, PROXIED_COLLECTION_FLAG, {
|
|
707
|
+
value: true,
|
|
708
|
+
enumerable: false,
|
|
709
|
+
configurable: true
|
|
710
|
+
});
|
|
711
|
+
} catch {
|
|
712
|
+
return collection;
|
|
665
713
|
}
|
|
714
|
+
return new Proxy(collection, { set(target, prop, value) {
|
|
715
|
+
target[prop] = typeof prop === "string" && shouldWrapCustomFunction(prop, value) ? wrapCustomFunction(value, prop, methodType) : value;
|
|
716
|
+
return true;
|
|
717
|
+
} });
|
|
666
718
|
};
|
|
667
|
-
wrapCollection(schema.statics, "static");
|
|
668
|
-
wrapCollection(schema.methods, "instance");
|
|
669
|
-
wrapCollection(schema.query, "query");
|
|
719
|
+
schema.statics = wrapCollection(schema.statics, "static");
|
|
720
|
+
schema.methods = wrapCollection(schema.methods, "instance");
|
|
721
|
+
schema.query = wrapCollection(schema.query, "query");
|
|
670
722
|
}
|
|
671
723
|
/**
|
|
672
724
|
* Patches `mongoose.model()` (and `Connection.prototype.model()`) so custom
|