autotel-mongoose 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,594 @@
1
+ import { otelTrace, createStringRedactor, SpanKind } from 'autotel';
2
+ import { runWithSpan, finalizeSpan, getActiveSpan } from 'autotel/trace-helpers';
3
+
4
+ // src/instrumentation.ts
5
+
6
+ // src/constants.ts
7
+ var ATTR_DB_QUERY_TEXT = "db.query.text";
8
+ var ATTR_DB_OPERATION_NAME = "db.operation.name";
9
+ var ATTR_DB_SYSTEM_NAME = "db.system.name";
10
+ var ATTR_DB_COLLECTION_NAME = "db.collection.name";
11
+ var ATTR_DB_NAMESPACE = "db.namespace";
12
+ var ATTR_SERVER_ADDRESS = "server.address";
13
+ var ATTR_SERVER_PORT = "server.port";
14
+ var DB_SYSTEM_NAME_VALUE_MONGODB = "mongodb";
15
+
16
+ // src/types.ts
17
+ var DEFAULT_TRACER_NAME = "autotel-mongoose";
18
+ function defaultSerializer(_operation, payload) {
19
+ return JSON.stringify(payload);
20
+ }
21
+ function createStatementCapture(config) {
22
+ if (config.dbStatementSerializer === false) {
23
+ return () => {
24
+ return;
25
+ };
26
+ }
27
+ const serializer = typeof config.dbStatementSerializer === "function" ? config.dbStatementSerializer : defaultSerializer;
28
+ let redact;
29
+ if (config.statementRedactor !== false && config.statementRedactor !== void 0) {
30
+ redact = createStringRedactor(config.statementRedactor);
31
+ }
32
+ return (operation, payload) => {
33
+ const raw = serializer(operation, payload);
34
+ if (raw === void 0) {
35
+ return void 0;
36
+ }
37
+ return redact ? redact(raw) : raw;
38
+ };
39
+ }
40
+
41
+ // src/instrumentation.ts
42
+ var INSTRUMENTED_FLAG = "__autotelMongooseInstrumented";
43
+ var WRAPPED_HOOK_FLAG = "__autotelWrappedHook";
44
+ var _STORED_PARENT_SPAN = /* @__PURE__ */ Symbol("stored-parent-span");
45
+ function createSpan(tracer, operation, modelName, collectionName, config) {
46
+ const spanName = collectionName ? `${operation} ${collectionName}` : modelName ? `${operation} ${modelName}` : `mongoose.${operation}`;
47
+ const attributes = {
48
+ [ATTR_DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MONGODB,
49
+ [ATTR_DB_OPERATION_NAME]: operation
50
+ };
51
+ if (collectionName && config.captureCollectionName) {
52
+ attributes[ATTR_DB_COLLECTION_NAME] = collectionName;
53
+ }
54
+ if (config.dbName) {
55
+ attributes[ATTR_DB_NAMESPACE] = config.dbName;
56
+ }
57
+ if (config.peerName) {
58
+ attributes[ATTR_SERVER_ADDRESS] = config.peerName;
59
+ }
60
+ if (config.peerPort) {
61
+ attributes[ATTR_SERVER_PORT] = config.peerPort;
62
+ }
63
+ return tracer.startSpan(spanName, { kind: SpanKind.CLIENT, attributes });
64
+ }
65
+ function wrapQueryReturningMethod(target, methodName, operation, getCollectionName, getModelName, tracer, config, captureStatement) {
66
+ const original = target[methodName];
67
+ if (typeof original !== "function") {
68
+ return;
69
+ }
70
+ target[methodName] = function instrumented(...args) {
71
+ const collectionName = getCollectionName(this);
72
+ const modelName = getModelName(this);
73
+ const span = createSpan(
74
+ tracer,
75
+ operation,
76
+ modelName,
77
+ collectionName,
78
+ config
79
+ );
80
+ return runWithSpan(span, () => {
81
+ try {
82
+ const result = original.apply(this, args);
83
+ if (result && typeof result.exec === "function") {
84
+ try {
85
+ const payload = {};
86
+ if (typeof result.getFilter === "function") {
87
+ payload.condition = result.getFilter();
88
+ }
89
+ if (result._update !== void 0) {
90
+ payload.updates = result._update;
91
+ }
92
+ if (typeof result.getOptions === "function") {
93
+ payload.options = result.getOptions();
94
+ }
95
+ if (result._fields !== void 0) {
96
+ payload.fields = result._fields;
97
+ }
98
+ const statementText = captureStatement(operation, payload);
99
+ if (statementText) {
100
+ span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
101
+ }
102
+ } catch {
103
+ }
104
+ const originalExec = result.exec.bind(result);
105
+ result.exec = function wrappedExec() {
106
+ try {
107
+ const execPromise = originalExec();
108
+ return Promise.resolve(execPromise).then((value) => {
109
+ finalizeSpan(span);
110
+ return value;
111
+ }).catch((error) => {
112
+ finalizeSpan(
113
+ span,
114
+ error instanceof Error ? error : new Error(String(error))
115
+ );
116
+ throw error;
117
+ });
118
+ } catch (error) {
119
+ finalizeSpan(
120
+ span,
121
+ error instanceof Error ? error : new Error(String(error))
122
+ );
123
+ throw error;
124
+ }
125
+ };
126
+ return result;
127
+ }
128
+ finalizeSpan(span);
129
+ return result;
130
+ } catch (error) {
131
+ finalizeSpan(
132
+ span,
133
+ error instanceof Error ? error : new Error(String(error))
134
+ );
135
+ throw error;
136
+ }
137
+ });
138
+ };
139
+ }
140
+ function wrapStaticMethod(target, methodName, operation, getCollectionName, getModelName, tracer, config, captureStatement) {
141
+ const original = target[methodName];
142
+ if (typeof original !== "function") {
143
+ return;
144
+ }
145
+ target[methodName] = function instrumented(...args) {
146
+ const collectionName = getCollectionName(this);
147
+ const modelName = getModelName(this);
148
+ const payload = {};
149
+ try {
150
+ switch (operation) {
151
+ case "create": {
152
+ payload.document = args[0];
153
+ break;
154
+ }
155
+ case "insertMany": {
156
+ payload.documents = args[0];
157
+ break;
158
+ }
159
+ case "aggregate": {
160
+ payload.aggregatePipeline = args[0];
161
+ break;
162
+ }
163
+ case "bulkWrite": {
164
+ payload.operations = args[0];
165
+ break;
166
+ }
167
+ default: {
168
+ break;
169
+ }
170
+ }
171
+ } catch {
172
+ }
173
+ const span = createSpan(
174
+ tracer,
175
+ operation,
176
+ modelName,
177
+ collectionName,
178
+ config
179
+ );
180
+ try {
181
+ const statementText = captureStatement(operation, payload);
182
+ if (statementText) {
183
+ span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
184
+ }
185
+ } catch {
186
+ }
187
+ return runWithSpan(span, () => {
188
+ try {
189
+ const result = original.apply(this, args);
190
+ if (result && typeof result.exec === "function") {
191
+ const originalExec = result.exec.bind(result);
192
+ result.exec = function wrappedExec() {
193
+ try {
194
+ const execPromise = originalExec();
195
+ return Promise.resolve(execPromise).then((value) => {
196
+ finalizeSpan(span);
197
+ return value;
198
+ }).catch((error) => {
199
+ finalizeSpan(
200
+ span,
201
+ error instanceof Error ? error : new Error(String(error))
202
+ );
203
+ throw error;
204
+ });
205
+ } catch (error) {
206
+ finalizeSpan(
207
+ span,
208
+ error instanceof Error ? error : new Error(String(error))
209
+ );
210
+ throw error;
211
+ }
212
+ };
213
+ return result;
214
+ }
215
+ if (result && typeof result.then === "function") {
216
+ return Promise.resolve(result).then((value) => {
217
+ finalizeSpan(span);
218
+ return value;
219
+ }).catch((error) => {
220
+ finalizeSpan(
221
+ span,
222
+ error instanceof Error ? error : new Error(String(error))
223
+ );
224
+ throw error;
225
+ });
226
+ }
227
+ finalizeSpan(span);
228
+ return result;
229
+ } catch (error) {
230
+ finalizeSpan(
231
+ span,
232
+ error instanceof Error ? error : new Error(String(error))
233
+ );
234
+ throw error;
235
+ }
236
+ });
237
+ };
238
+ }
239
+ function wrapInstanceMethod(target, methodName, operation, getCollectionName, getModelName, tracer, config, captureStatement) {
240
+ const original = target[methodName];
241
+ if (typeof original !== "function") {
242
+ return;
243
+ }
244
+ target[methodName] = function instrumented(...args) {
245
+ const collectionName = getCollectionName(this);
246
+ const modelName = getModelName(this);
247
+ const payload = {};
248
+ try {
249
+ if (typeof this.toObject === "function") {
250
+ payload.document = this.toObject();
251
+ }
252
+ } catch {
253
+ }
254
+ const span = createSpan(
255
+ tracer,
256
+ operation,
257
+ modelName,
258
+ collectionName,
259
+ config
260
+ );
261
+ try {
262
+ const statementText = captureStatement(operation, payload);
263
+ if (statementText) {
264
+ span.setAttribute(ATTR_DB_QUERY_TEXT, statementText);
265
+ }
266
+ } catch {
267
+ }
268
+ return runWithSpan(span, () => {
269
+ try {
270
+ const result = original.apply(this, args);
271
+ if (result && typeof result.then === "function") {
272
+ return Promise.resolve(result).then((value) => {
273
+ finalizeSpan(span);
274
+ return value;
275
+ }).catch((error) => {
276
+ finalizeSpan(
277
+ span,
278
+ error instanceof Error ? error : new Error(String(error))
279
+ );
280
+ throw error;
281
+ });
282
+ }
283
+ finalizeSpan(span);
284
+ return result;
285
+ } catch (error) {
286
+ finalizeSpan(
287
+ span,
288
+ error instanceof Error ? error : new Error(String(error))
289
+ );
290
+ throw error;
291
+ }
292
+ });
293
+ };
294
+ }
295
+ function wrapChainableMethod(target, methodName) {
296
+ const original = target[methodName];
297
+ if (typeof original !== "function") {
298
+ return;
299
+ }
300
+ target[methodName] = function captureContext(...args) {
301
+ const currentSpan = getActiveSpan();
302
+ const result = original.apply(this, args);
303
+ if (result && typeof result.exec === "function") {
304
+ result[_STORED_PARENT_SPAN] = currentSpan;
305
+ }
306
+ return result;
307
+ };
308
+ }
309
+ function patchSchemaHooks(Schema, tracer, config) {
310
+ if (!Schema?.prototype) {
311
+ return;
312
+ }
313
+ const HOOK_FLAG = "__autotelHookInstrumented";
314
+ if (Schema.prototype[HOOK_FLAG]) {
315
+ return;
316
+ }
317
+ const originalPre = Schema.prototype.pre;
318
+ if (typeof originalPre === "function") {
319
+ Schema.prototype.pre = function(hookName, ...args) {
320
+ const handler = typeof args[0] === "function" ? args[0] : typeof args[1] === "function" ? args[1] : null;
321
+ if (handler && !isMongooseInternalHook(handler)) {
322
+ const wrapped = wrapHookHandler(
323
+ handler,
324
+ hookName,
325
+ "pre",
326
+ tracer,
327
+ config
328
+ );
329
+ if (typeof args[0] === "function") {
330
+ args[0] = wrapped;
331
+ } else if (typeof args[1] === "function") {
332
+ args[1] = wrapped;
333
+ }
334
+ }
335
+ return Reflect.apply(originalPre, this, [hookName, ...args]);
336
+ };
337
+ }
338
+ const originalPost = Schema.prototype.post;
339
+ if (typeof originalPost === "function") {
340
+ Schema.prototype.post = function(hookName, ...args) {
341
+ const handler = typeof args[0] === "function" ? args[0] : typeof args[1] === "function" ? args[1] : null;
342
+ if (handler && !isMongooseInternalHook(handler)) {
343
+ const wrapped = wrapHookHandler(
344
+ handler,
345
+ hookName,
346
+ "post",
347
+ tracer,
348
+ config
349
+ );
350
+ if (typeof args[0] === "function") {
351
+ args[0] = wrapped;
352
+ } else if (typeof args[1] === "function") {
353
+ args[1] = wrapped;
354
+ }
355
+ }
356
+ return Reflect.apply(originalPost, this, [hookName, ...args]);
357
+ };
358
+ }
359
+ Schema.prototype[HOOK_FLAG] = true;
360
+ }
361
+ function isMongooseInternalHook(handler) {
362
+ if (typeof handler !== "function") {
363
+ return false;
364
+ }
365
+ const funcName = handler.name || "";
366
+ if (funcName.startsWith("_") || funcName.startsWith("$")) {
367
+ return true;
368
+ }
369
+ const mongooseInternalNamePatterns = [
370
+ "shardingPlugin",
371
+ "mongooseInternalHook",
372
+ "noop",
373
+ "wrapped",
374
+ "bound "
375
+ ];
376
+ if (mongooseInternalNamePatterns.some((pattern) => funcName.includes(pattern))) {
377
+ return true;
378
+ }
379
+ try {
380
+ const source = handler.toString();
381
+ const mongooseInternalSourcePatterns = [
382
+ "this.$__",
383
+ // Mongoose internal document methods
384
+ "this.$isValid",
385
+ // Mongoose validation
386
+ "this.$locals",
387
+ // Mongoose local properties
388
+ "_this.$__",
389
+ // Mongoose internal with closure
390
+ "schema.s.hooks",
391
+ // Mongoose hooks system
392
+ "kareem"
393
+ // Mongoose's hooks library
394
+ ];
395
+ if (mongooseInternalSourcePatterns.some((pattern) => source.includes(pattern))) {
396
+ return true;
397
+ }
398
+ } catch {
399
+ }
400
+ return false;
401
+ }
402
+ function wrapHookHandler(handler, hookName, hookType, tracer, config) {
403
+ if (typeof handler !== "function") {
404
+ return handler;
405
+ }
406
+ if (handler[WRAPPED_HOOK_FLAG]) {
407
+ return handler;
408
+ }
409
+ const wrappedHook = function wrappedHook2(...args) {
410
+ let modelName;
411
+ let collectionName;
412
+ try {
413
+ if (this.constructor?.modelName) {
414
+ modelName = this.constructor.modelName;
415
+ collectionName = this.constructor.collection?.collectionName || modelName;
416
+ } else if (this.model?.modelName) {
417
+ modelName = this.model.modelName;
418
+ collectionName = this.model.collection?.collectionName || modelName;
419
+ }
420
+ } catch {
421
+ }
422
+ const spanName = collectionName ? `mongoose.${collectionName}.${hookType}.${hookName}` : `mongoose.hook.${hookType}.${hookName}`;
423
+ const span = tracer.startSpan(spanName, { kind: SpanKind.INTERNAL });
424
+ span.setAttribute("hook.type", hookType);
425
+ span.setAttribute("hook.operation", hookName);
426
+ if (modelName) {
427
+ span.setAttribute("hook.model", modelName);
428
+ }
429
+ if (collectionName && config.captureCollectionName) {
430
+ span.setAttribute(ATTR_DB_COLLECTION_NAME, collectionName);
431
+ }
432
+ span.setAttribute(ATTR_DB_SYSTEM_NAME, DB_SYSTEM_NAME_VALUE_MONGODB);
433
+ if (config.dbName) {
434
+ span.setAttribute(ATTR_DB_NAMESPACE, config.dbName);
435
+ }
436
+ return runWithSpan(span, () => {
437
+ try {
438
+ const result = handler.apply(this, args);
439
+ if (result && typeof result.then === "function") {
440
+ return Promise.resolve(result).then((value) => {
441
+ finalizeSpan(span);
442
+ return value;
443
+ }).catch((error) => {
444
+ finalizeSpan(
445
+ span,
446
+ error instanceof Error ? error : new Error(String(error))
447
+ );
448
+ throw error;
449
+ });
450
+ }
451
+ finalizeSpan(span);
452
+ return result;
453
+ } catch (error) {
454
+ finalizeSpan(
455
+ span,
456
+ error instanceof Error ? error : new Error(String(error))
457
+ );
458
+ throw error;
459
+ }
460
+ });
461
+ };
462
+ wrappedHook[WRAPPED_HOOK_FLAG] = true;
463
+ return wrappedHook;
464
+ }
465
+ function instrumentMongoose(mongoose, config) {
466
+ if (!mongoose?.Model) {
467
+ return mongoose;
468
+ }
469
+ const m = mongoose;
470
+ if (m[INSTRUMENTED_FLAG]) {
471
+ return mongoose;
472
+ }
473
+ const resolvedSerializer = config?.dbStatementSerializer;
474
+ const resolvedRedactor = config?.statementRedactor ?? "default";
475
+ const finalConfig = {
476
+ dbName: config?.dbName || "",
477
+ peerName: config?.peerName || "",
478
+ peerPort: config?.peerPort || 27017,
479
+ tracerName: config?.tracerName || DEFAULT_TRACER_NAME,
480
+ captureCollectionName: config?.captureCollectionName ?? true,
481
+ instrumentHooks: config?.instrumentHooks ?? false};
482
+ const tracer = otelTrace.getTracer(finalConfig.tracerName);
483
+ const captureStatement = createStatementCapture({
484
+ dbStatementSerializer: resolvedSerializer,
485
+ statementRedactor: resolvedRedactor
486
+ });
487
+ if (m.Schema && finalConfig.instrumentHooks) {
488
+ patchSchemaHooks(m.Schema, tracer, finalConfig);
489
+ }
490
+ const getModelCollectionName = (model) => {
491
+ try {
492
+ return model.collection?.collectionName || model.modelName;
493
+ } catch {
494
+ return;
495
+ }
496
+ };
497
+ const queryMethods = [
498
+ { method: "find", operation: "find" },
499
+ { method: "findOne", operation: "findOne" },
500
+ { method: "findById", operation: "findById" },
501
+ { method: "findOneAndUpdate", operation: "findOneAndUpdate" },
502
+ { method: "findOneAndDelete", operation: "findOneAndDelete" },
503
+ { method: "findOneAndReplace", operation: "findOneAndReplace" },
504
+ { method: "deleteOne", operation: "deleteOne" },
505
+ { method: "deleteMany", operation: "deleteMany" },
506
+ { method: "updateOne", operation: "updateOne" },
507
+ { method: "updateMany", operation: "updateMany" },
508
+ { method: "countDocuments", operation: "countDocuments" },
509
+ { method: "estimatedDocumentCount", operation: "estimatedDocumentCount" }
510
+ ];
511
+ for (const { method, operation } of queryMethods) {
512
+ wrapQueryReturningMethod(
513
+ m.Model,
514
+ method,
515
+ operation,
516
+ getModelCollectionName,
517
+ (model) => model.modelName,
518
+ tracer,
519
+ finalConfig,
520
+ captureStatement
521
+ );
522
+ if (m.Query?.prototype?.[method]) {
523
+ wrapChainableMethod(m.Query.prototype, method);
524
+ }
525
+ }
526
+ const instanceMethods = ["save", "deleteOne"];
527
+ for (const method of instanceMethods) {
528
+ if (m.Model.prototype[method]) {
529
+ wrapInstanceMethod(
530
+ m.Model.prototype,
531
+ method,
532
+ method,
533
+ (doc) => {
534
+ try {
535
+ return doc.constructor?.collection?.collectionName || doc.constructor?.modelName;
536
+ } catch {
537
+ return;
538
+ }
539
+ },
540
+ (doc) => {
541
+ try {
542
+ return doc.constructor?.modelName;
543
+ } catch {
544
+ return;
545
+ }
546
+ },
547
+ tracer,
548
+ finalConfig,
549
+ captureStatement
550
+ );
551
+ }
552
+ }
553
+ const staticMethods = ["create", "insertMany", "aggregate", "bulkWrite"];
554
+ for (const method of staticMethods) {
555
+ if (m.Model[method]) {
556
+ wrapStaticMethod(
557
+ m.Model,
558
+ method,
559
+ method,
560
+ (model) => {
561
+ try {
562
+ return model.collection?.collectionName;
563
+ } catch {
564
+ return;
565
+ }
566
+ },
567
+ (model) => model.modelName,
568
+ tracer,
569
+ finalConfig,
570
+ captureStatement
571
+ );
572
+ }
573
+ }
574
+ const chainableMethods = [
575
+ "populate",
576
+ "select",
577
+ "lean",
578
+ "where",
579
+ "sort",
580
+ "limit",
581
+ "skip"
582
+ ];
583
+ for (const method of chainableMethods) {
584
+ if (m.Query?.prototype?.[method]) {
585
+ wrapChainableMethod(m.Query.prototype, method);
586
+ }
587
+ }
588
+ m[INSTRUMENTED_FLAG] = true;
589
+ return mongoose;
590
+ }
591
+
592
+ export { instrumentMongoose };
593
+ //# sourceMappingURL=index.js.map
594
+ //# sourceMappingURL=index.js.map