8000 feat: MongoDB Tracing Support (#3072) · ineba/sentry-javascript@f7fc733 · GitHub
[go: up one dir, main page]

Skip to content

Commit f7fc733

Browse files
kamilogoreklobsterkatieHazAT
authored
feat: MongoDB Tracing Support (getsentry#3072)
* feat: MongoDB Tracing Support * s/concrete/literal * add comment explaining operation signature map * add missing operation * copy pasta * streamline comment * clarify comment * ref: Use getSpan vs. getTransaction * fix: Express name and span op Co-authored-by: Katie Byers <katie.byers@sentry.io> Co-authored-by: Daniel Griesser <daniel.griesser.86@gmail.com>
1 parent 22ecbcd commit f7fc733

File tree

4 files changed

+225
-6
lines changed

4 files changed

+225
-6
lines changed

packages/node/src/handlers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,12 @@ function extractRouteInfo(req: ExpressRequest, options: { path?: boolean; method
108108
let path;
109109
if (req.baseUrl && req.route) {
110110
path = `${req.baseUrl}${req.route.path}`;
111+
} else if (req.route) {
112+
path = `${req.route.path}`;
111113
} else if (req.originalUrl || req.url) {
112114
path = stripUrlQueryAndFragment(req.originalUrl || req.url || '');
113115
} else {
114-
path = req.route?.path || '';
116+
path = '';
115117
}
116118

117119
let info = '';

packages/node/src/integrations/http.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getCurrentHub } from '@sentry/core';
2-
import { Integration, Span, Transaction } from '@sentry/types';
2+
import { Integration, Span } from '@sentry/types';
33
import { fill, logger, parseSemver } from '@sentry/utils';
44
import * as http from 'http';
55
import * as https from 'https';
@@ -104,13 +104,13 @@ function _createWrappedRequestMethodFactory(
104104
}
105105

106106
let span: Span | undefined;
107-
let transaction: Transaction | undefined;
107+
let parentSpan: Span | undefined;
108108

109109
const scope = getCurrentHub().getScope();
110110
if (scope && tracingEnabled) {
111-
transaction = scope.getTransaction();
112-
if (transaction) {
113-
span = transaction.startChild({
111+
parentSpan = scope.getSpan();
112+
if (parentSpan) {
113+
span = parentSpan.startChild({
114114
description: `${requestOptions.method || 'GET'} ${requestUrl}`,
115115
op: 'request',
116116
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { Express } from './express';
2+
export { Mongo } from './mongo';
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { Hub } from '@sentry/hub';
2+
import { EventProcessor, Integration, SpanContext } from '@sentry/types';
3+
import { dynamicRequire, fill, logger } from '@sentry/utils';
4+
5+
// This allows us to use the same array for both defaults options and the type itself.
6+
// (note `as const` at the end to make it a union of string literal types (i.e. "a" | "b" | ... )
7+
// and not just a string[])
8+
type Operation = typeof OPERATIONS[number];
9+
const OPERATIONS = [
10+
'aggregate', // aggregate(pipeline, options, callback)
11+
'bulkWrite', // bulkWrite(operations, options, callback)
12+
'countDocuments', // countDocuments(query, options, callback)
13+
'createIndex', // createIndex(fieldOrSpec, options, callback)
14+
'createIndexes', // createIndexes(indexSpecs, options, callback)
15+
'deleteMany', // deleteMany(filter, options, callback)
16+
'deleteOne', // deleteOne(filter, options, callback)
17+
'distinct', // distinct(key, query, options, callback)
18+
'drop', // drop(options, callback)
19+
'dropIndex', // dropIndex(indexName, options, callback)
20+
'dropIndexes', // dropIndexes(options, callback)
21+
'estimatedDocumentCount', // estimatedDocumentCount(options, callback)
22+
'findOne', // findOne(query, options, callback)
23+
'findOneAndDelete', // findOneAndDelete(filter, options, callback)
24+
'findOneAndReplace', // findOneAndReplace(filter, replacement, options, callback)
25+
'findOneAndUpdate', // findOneAndUpdate(filter, update, options, callback)
26+
'indexes', // indexes(options, callback)
27+
'indexExists', // indexExists(indexes, options, callback)
28+
'indexInformation', // indexInformation(options, callback)
29+
'initializeOrderedBulkOp', // initializeOrderedBulkOp(options, callback)
30+
'insertMany', // insertMany(docs, options, callback)
31+
'insertOne', // insertOne(doc, options, callback)
32+
'isCapped', // isCapped(options, callback)
33+
'mapReduce', // mapReduce(map, reduce, options, callback)
34+
'options', // options(options, callback)
35+
'parallelCollectionScan', // parallelCollectionScan(options, callback)
36+
'rename', // rename(newName, options, callback)
37+
'replaceOne', // replaceOne(filter, doc, options, callback)
38+
'stats', // stats(options, callback)
39+
'updateMany', // updateMany(filter, update, options, callback)
40+
'updateOne', // updateOne(filter, update, options, callback)
41+
] as const;
42+
43+
// All of the operations above take `options` and `callback` as their final parameters, but some of them
44+
// take additional parameters as well. For those operations, this is a map of
45+
// { <operation name>: [<names of additional parameters>] }, as a way to know what to call the operation's
46+
// positional arguments when we add them to the span's `data` object later
47+
const OPERATION_SIGNATURES: {
48+
[op in Operation]?: string[];
49+
} = {
50+
aggregate: ['pipeline'],
51+
bulkWrite: ['operations'],
52+
countDocuments: ['query'],
53+
createIndex: ['fieldOrSpec'],
54+
createIndexes: ['indexSpecs'],
55+
deleteMany: ['filter'],
56+
deleteOne: ['filter'],
57+
distinct: ['key', 'query'],
58+
dropIndex: ['indexName'],
59+
findOne: ['query'],
60+
findOneAndDelete: ['filter'],
61+
findOneAndReplace: ['filter', 'replacement'],
62+
findOneAndUpdate: ['filter', 'update'],
63+
indexExists: ['indexes'],
64+
insertMany: ['docs'],
65+
insertOne: ['doc'],
66+
mapReduce: ['map', 'reduce'],
67+
rename: ['newName'],
68+
replaceOne: ['filter', 'doc'],
69+
updateMany: ['filter', 'update'],
70+
updateOne: ['filter', 'update'],
71+
};
72+
73+
interface MongoCollection {
74+
collectionName: string;
75+
dbName: string;
76+
namespace: string;
77+
prototype: {
78+
[operation in Operation]: (...args: unknown[]) => unknown;
79+
};
80+
}
81+
82+
interface MongoOptions {
83+
operations?: Operation[];
84+
describeOperations?: boolean | Operation[];
85+
}
86+
87+
/** Tracing integration for mongo package */
88+
export class Mongo implements Integration {
89+
/**
90+
* @inheritDoc
91+
*/
92+
public static id: string = 'Mongo';
93+
94+
/**
95+
* @inheritDoc
96+
*/
97+
public name: string = Mongo.id;
98+
99+
private _operations: Operation[];
100+
private _describeOperations?: boolean | Operation[];
101+
102+
/**
103+
* @inheritDoc
104+
*/
105+
public constructor(options: MongoOptions = {}) {
106+
this._operations = Array.isArray(options.operations)
107+
? options.operations
108+
: ((OPERATIONS as unknown) as Operation[]);
109+
this._describeOperations = 'describeOperations' in options ? options.describeOperations : true;
110+
}
111+
112+
/**
113+
* @inheritDoc
114+
*/
115+
public setupOnce(_: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
116+
let collection: MongoCollection;
117+
118+
try {
119+
const mongodbModule = dynamicRequire(module, 'mongodb') as { Collection: MongoCollection };
120+
collection = mongodbModule.Collection;
121+
} catch (e) {
122+
logger.error('Mongo Integration was unable to require `mongodb` package.');
123+
return;
124+
}
125+
126+
this._instrumentOperations(collection, this._operations, getCurrentHub);
127+
}
128+
129+
/**
130+
* Patches original collection methods
131+
*/
132+
private _instrumentOperations(collection: MongoCollection, operations: Operation[], getCurrentHub: () => Hub): void {
133+
operations.forEach((operation: Operation) => this._patchOperation(collection, operation, getCurrentHub));
134+
}
135+
136+
/**
137+
* Patches original collection to utilize our tracing functionality
138+
*/
139+
private _patchOperation(collection: MongoCollection, operation: Operation, getCurrentHub: () => Hub): void {
140+
if (!(operation in collection.prototype)) return;
141+
142+
const getSpanContext = this._getSpanContextFromOperationArguments.bind(this);
143+
144+
fill(collection.prototype, operation, function(orig: () => void | Promise<unknown>) {
145+
return function(this: unknown, ...args: unknown[]) {
146+
const lastArg = args[args.length - 1];
147+
const scope = getCurrentHub().getScope();
148+
const parentSpan = scope?.getSpan();
149+
150+
// Check if the operation was passed a callback. (mapReduce requires a different check, as
151+
// its (non-callback) arguments can also be functions.)
152+
if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) {
153+
const span = parentSpan?.startChild(getSpanContext(this, operation, args));
154+
return (orig.call(this, ...args) as Promise<unknown>).then((res: unknown) => {
155+
span?.finish();
156+
return res;
157+
});
158+
}
159+
160+
const span = parentSpan?.startChild(getSpanContext(this, operation, args.slice(0, -1)));
161+
return orig.call(this, ...args.slice(0, -1), function(err: Error, result: unknown) {
162+
span?.finish();
163+
lastArg(err, result);
164+
});
165+
};
166+
});
167+
}
168+
169+
/**
170+
* Form a SpanContext based on the user input to a given operation.
171+
*/
172+
private _getSpanContextFromOperationArguments(
173+
collection: MongoCollection,
174+
operation: Operation,
175+
args: unknown[],
176+
): SpanContext {
177+
const data: { [key: string]: string } = {
178+
collectionName: collection.collectionName,
179+
dbName: collection.dbName,
180+
namespace: collection.namespace,
181+
};
182+
const spanContext: SpanContext = {
183+
op: `db`,
184+
description: operation,
185+
data,
186+
};
187+
188+
// If the operation takes no arguments besides `options` and `callback`, or if argument
189+
// collection is disabled for this operation, just return early.
190+
const signature = OPERATION_SIGNATURES[operation];
191+
const shouldDescribe = Array.isArray(this._describeOperations)
192+
? this._describeOperations.includes(operation)
193+
: this._describeOperations;
194+
195+
if (!signature || !shouldDescribe) {
196+
return spanContext;
197+
}
198+
199+
try {
200+
// Special case for `mapReduce`, as the only one accepting functions as arguments.
201+
if (operation === 'mapReduce') {
202+
const [map, reduce] = args as { name?: string }[];
203+
data[signature[0]] = typeof map === 'string' ? map : map.name || '<anonymous>';
204+
data[signature[1]] = typeof reduce === 'string' ? reduce : reduce.name || '<anonymous>';
205+
} else {
206+
for (let i = 0; i < signature.length; i++) {
207+
data[signature[i]] = JSON.stringify(args[i]);
208+
}
209+
}
210+
} catch (_oO) {
211+
// no-empty
212+
}
213+
214+
return spanContext;
215+
}
216+
}

0 commit comments

Comments
 (0)
0