autotel-mongoose 7.0.0 → 8.1.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/README.md CHANGED
@@ -77,6 +77,117 @@ userSchema.pre('save', async function () {
77
77
  });
78
78
  ```
79
79
 
80
+ ## Custom Statics, Methods & Query Helpers
81
+
82
+ The functions you add via `schema.statics`, `schema.methods`, and `schema.query`
83
+ are invisible to the built-in Model/Query instrumentation. This package traces
84
+ them automatically — **no manual `trace()` calls** and no behavioral side
85
+ effects (same `this`, same return value, same error propagation). Each call
86
+ gets an `INTERNAL` span named `mongoose.<Model>.<fn>`.
87
+
88
+ ```typescript
89
+ userSchema.statics.findByEmail = function (email: string) {
90
+ return this.findOne({ email }); // span: mongoose.User.findByEmail
91
+ };
92
+
93
+ userSchema.methods.describe = function () {
94
+ return `${this.name} <${this.email}>`; // span: mongoose.User.describe
95
+ };
96
+
97
+ userSchema.query.byEmailDomain = function (domain: string) {
98
+ return this.where({ email: new RegExp(`@${domain}$`) }); // span: mongoose.User.byEmailDomain
99
+ };
100
+ ```
101
+
102
+ Example spans (from `apps/example-mongoose`, debug output). Note that a static
103
+ returning a Query becomes the **parent** of the underlying operation span, and
104
+ parameters are redacted by default:
105
+
106
+ ```text
107
+ ✓ findOne users 1ms [autotel-mongoose]
108
+ db.system.name=mongodb, db.operation.name=findOne, db.collection.name=users, db.query.text={"condition":{"email":"A***@***.com"},...
109
+ ✓ mongoose.User.findByEmail 2ms [autotel-mongoose]
110
+ db.system.name=mongodb, code.function.name=findByEmail, mongoose.method.name=findByEmail, mongoose.method.type=static, mongoose.method.model=User, db.collection.name=users, mongoose.method.parameter_count=1, mongoose.method.parameters=["A***@***.com"]
111
+
112
+ ✓ mongoose.User.describe 27µs [autotel-mongoose]
113
+ db.system.name=mongodb, code.function.name=describe, mongoose.method.name=describe, mongoose.method.type=instance, mongoose.method.model=User, db.collection.name=users, mongoose.method.parameter_count=0
114
+
115
+ ✓ mongoose.User.countByDomain 2ms [autotel-mongoose]
116
+ db.system.name=mongodb, code.function.name=countByDomain, mongoose.method.name=countByDomain, mongoose.method.type=static, mongoose.method.model=User, db.collection.name=users, mongoose.method.parameter_count=1, mongoose.method.parameters=["hotmail.com"]
117
+
118
+ ✓ mongoose.User.byEmailDomain 82µs [autotel-mongoose]
119
+ db.system.name=mongodb, code.function.name=byEmailDomain, mongoose.method.name=byEmailDomain, mongoose.method.type=query, mongoose.method.model=User, db.collection.name=users, mongoose.method.parameter_count=1, mongoose.method.parameters=["hotmail.com"]
120
+ ```
121
+
122
+ As JSON:
123
+
124
+ ```json
125
+ {
126
+ "name": "mongoose.User.findByEmail",
127
+ "kind": "INTERNAL",
128
+ "instrumentationScope": { "name": "autotel-mongoose" },
129
+ "attributes": {
130
+ "db.system.name": "mongodb",
131
+ "code.function.name": "findByEmail",
132
+ "mongoose.method.name": "findByEmail",
133
+ "mongoose.method.type": "static",
134
+ "mongoose.method.model": "User",
135
+ "db.collection.name": "users",
136
+ "mongoose.method.parameter_count": 1,
137
+ "mongoose.method.parameters": "[\"A***@***.com\"]"
138
+ }
139
+ }
140
+ ```
141
+
142
+ Span attributes: `mongoose.method.name`, `mongoose.method.type`
143
+ (`static` | `instance` | `query`), `mongoose.method.model`, `code.function.name`,
144
+ and — when parameter capture is on — `mongoose.method.parameters` (+
145
+ `mongoose.method.parameter_count`).
146
+
147
+ > **Behavior note (default on):** With no `customMethods` option, `instrumentMongoose(mongoose)`
148
+ > wraps **all** custom functions and captures their arguments by default
149
+ > (maximum observability). Arguments pass through the same redactor as
150
+ > `db.query.text`, but custom-function args are often business payloads rather
151
+ > than DB filters — redaction won't catch arbitrary fields. Use the options
152
+ > below to scope this down for privacy/compliance.
153
+
154
+ ### Opting out / scoping (privacy & compliance)
155
+
156
+ ```typescript
157
+ // Disable entirely
158
+ instrumentMongoose(mongoose, { customMethods: false });
159
+
160
+ // Per-category control. Anything not explicitly disabled stays on.
161
+ instrumentMongoose(mongoose, {
162
+ customMethods: {
163
+ statics: { exclude: ['chargeCard'] }, // opt-out specific statics
164
+ methods: ['describe'], // opt-in: only these instance methods
165
+ query: false, // no query helpers
166
+ captureParameters: false, // trace calls, don't serialize args
167
+ },
168
+ });
169
+
170
+ // Keep tracing, but never serialize arguments anywhere
171
+ instrumentMongoose(mongoose, { customMethods: { captureParameters: false } });
172
+
173
+ // Custom parameter serializer / longer cap / dedicated redactor
174
+ instrumentMongoose(mongoose, {
175
+ customMethods: {
176
+ captureParameters: {
177
+ maxLength: 4096,
178
+ redactor: 'default',
179
+ serializer: (args, { methodName }) =>
180
+ methodName === 'chargeCard' ? undefined : JSON.stringify(args),
181
+ },
182
+ },
183
+ });
184
+ ```
185
+
186
+ A selector accepts `true` (all), `false` (none), `string[]` (opt-in to those
187
+ names), or `{ include?, exclude? }`. Config is resolved **per Mongoose
188
+ instance** at call time, so a schema object reused across multiple
189
+ instances/connections honors each instance's own configuration.
190
+
80
191
  ## Configuration
81
192
 
82
193
  ```typescript
@@ -91,6 +202,7 @@ const config: InstrumentMongooseConfig = {
91
202
  instrumentHooks: false,
92
203
  dbStatementSerializer: false,
93
204
  statementRedactor: 'default',
205
+ customMethods: true, // wrap all custom statics/methods/query helpers (default)
94
206
  };
95
207
  ```
96
208
 
@@ -137,13 +249,15 @@ instrumentMongoose(mongoose, {
137
249
  - `instrumentMongoose(mongoose, config?)`
138
250
  - `InstrumentMongooseConfig`
139
251
  - `SerializerPayload`
252
+ - `CustomMethodsConfig`, `CustomMethodType`, `MethodSelector`, `ParameterCaptureConfig`
140
253
 
141
254
  ## Notes
142
255
 
143
256
  - Query and aggregate operations are traced automatically
144
257
  - Instance methods like `save()` and `deleteOne()` are traced
145
258
  - Static methods like `create()`, `insertMany()`, `aggregate()`, and `bulkWrite()` are traced
146
- - Hook spans use `SpanKind.INTERNAL`
259
+ - User-defined statics, instance methods, and query helpers are traced automatically (see above)
260
+ - Hook and custom-function spans use `SpanKind.INTERNAL`
147
261
 
148
262
  ## License
149
263
 
package/dist/index.cjs CHANGED
@@ -13,6 +13,12 @@ var ATTR_DB_COLLECTION_NAME = "db.collection.name";
13
13
  var ATTR_DB_NAMESPACE = "db.namespace";
14
14
  var ATTR_SERVER_ADDRESS = "server.address";
15
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";
16
22
  var DB_SYSTEM_NAME_VALUE_MONGODB = "mongodb";
17
23
 
18
24
  // src/types.ts
@@ -39,10 +45,99 @@ function createStatementCapture(config) {
39
45
  return redact ? redact(raw) : raw;
40
46
  };
41
47
  }
48
+ var DEFAULT_PARAMETER_MAX_LENGTH = 2048;
49
+ 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
+ }
85
+ }
86
+ 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
+ };
103
+ }
42
104
 
43
105
  // src/instrumentation.ts
44
106
  var INSTRUMENTED_FLAG = "__autotelMongooseInstrumented";
45
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();
111
+ 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;
127
+ }
128
+ 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
+ }
140
+ }
46
141
  var _STORED_PARENT_SPAN = /* @__PURE__ */ Symbol("stored-parent-span");
47
142
  function createSpan(tracer, operation, modelName, collectionName, config) {
48
143
  const spanName = collectionName ? `${operation} ${collectionName}` : modelName ? `${operation} ${modelName}` : `mongoose.${operation}`;
@@ -499,6 +594,244 @@ function wrapHookHandler(handler, hookName, hookType, tracer, config) {
499
594
  wrappedHook[WRAPPED_HOOK_FLAG] = true;
500
595
  return wrappedHook;
501
596
  }
597
+ 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
+ };
625
+ }
626
+ 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;
643
+ }
644
+ 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 {};
671
+ }
672
+ 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;
776
+ }
777
+ var MONGOOSE_INTERNAL_FUNCTION_NAMES = /* @__PURE__ */ new Set([
778
+ "initializeTimestamps"
779
+ ]);
780
+ function isMongooseInternalFunctionName(name) {
781
+ return name.startsWith("$") || MONGOOSE_INTERNAL_FUNCTION_NAMES.has(name);
782
+ }
783
+ 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");
805
+ }
806
+ 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
+ }
834
+ }
502
835
  function instrumentMongoose(mongoose, config) {
503
836
  if (!mongoose?.Model) {
504
837
  return mongoose;
@@ -515,8 +848,14 @@ function instrumentMongoose(mongoose, config) {
515
848
  peerPort: config?.peerPort || 27017,
516
849
  tracerName: config?.tracerName || DEFAULT_TRACER_NAME,
517
850
  captureCollectionName: config?.captureCollectionName ?? true,
518
- instrumentHooks: config?.instrumentHooks ?? false};
851
+ instrumentHooks: config?.instrumentHooks ?? false,
852
+ dbStatementSerializer: resolvedSerializer === false ? false : resolvedSerializer ?? defaultSerializer,
853
+ statementRedactor: resolvedRedactor,
854
+ customMethods: resolveCustomMethods(config)
855
+ };
519
856
  const tracer = autotel.otelTrace.getTracer(finalConfig.tracerName);
857
+ INSTANCE_REGISTRY.set(mongoose, { tracer, config: finalConfig });
858
+ patchModelFactory(m, finalConfig);
520
859
  const captureStatement = createStatementCapture({
521
860
  dbStatementSerializer: resolvedSerializer,
522
861
  statementRedactor: resolvedRedactor