Skip to content

Commit 2b1360d

Browse files
Fox32pichlermarcJamieDanielson
authored
feat(instrumentation-mongodb): support aggregation commands and support nested statements (#1728)
* feat(opentelemetry-instrumentation-mongodb): support aggregation commands and support nested statements Signed-off-by: Oliver Sand <oliver@plancraft.de> * docs: fix typos in guidelines Signed-off-by: Oliver Sand <oliver@plancraft.de> * Apply suggestions from code review update semantic conventions imports --------- Signed-off-by: Oliver Sand <oliver@plancraft.de> Co-authored-by: Marc Pichler <marc.pichler@dynatrace.com> Co-authored-by: Jamie Danielson <jamieedanielson@gmail.com>
1 parent 6721bdd commit 2b1360d

File tree

6 files changed

+228
-5
lines changed

6 files changed

+228
-5
lines changed

GUIDELINES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ To support this use case, you can choose one of the following options:
115115
...
116116
```
117117

118-
If possible, this is the prefered option, as it uses types from a maintained package.
118+
If possible, this is the preferred option, as it uses types from a maintained package.
119119

120120
Notice that types may introduce breaking changes in major semver releases, and instrumentation should choose a `@types/` package that is compatible with the version range it supports.
121121

plugins/node/opentelemetry-instrumentation-mongodb/src/instrumentation.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -775,6 +775,8 @@ export class MongoDBInstrumentation extends InstrumentationBase {
775775
return MongodbCommandType.IS_MASTER;
776776
} else if (command.count !== undefined) {
777777
return MongodbCommandType.COUNT;
778+
} else if (command.aggregate !== undefined) {
779+
return MongodbCommandType.AGGREGATE;
778780
} else {
779781
return MongodbCommandType.UNKNOWN;
780782
}
@@ -924,13 +926,28 @@ export class MongoDBInstrumentation extends InstrumentationBase {
924926
const enhancedDbReporting = !!this._config?.enhancedDatabaseReporting;
925927
const resultObj = enhancedDbReporting
926928
? commandObj
927-
: Object.keys(commandObj).reduce((obj, key) => {
928-
obj[key] = '?';
929-
return obj;
930-
}, {} as { [key: string]: unknown });
929+
: this._scrubStatement(commandObj);
931930
return JSON.stringify(resultObj);
932931
}
933932

933+
private _scrubStatement(value: unknown): unknown {
934+
if (Array.isArray(value)) {
935+
return value.map(element => this._scrubStatement(element));
936+
}
937+
938+
if (typeof value === 'object' && value !== null) {
939+
return Object.fromEntries(
940+
Object.entries(value).map(([key, element]) => [
941+
key,
942+
this._scrubStatement(element),
943+
])
944+
);
945+
}
946+
947+
// A value like string or number, possible contains PII, scrub it
948+
return '?';
949+
}
950+
934951
/**
935952
* Triggers the response hook in case it is defined.
936953
* @param span The span to add the results to.

plugins/node/opentelemetry-instrumentation-mongodb/src/internal-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export type MongoInternalCommand = {
5656
findandmodify: boolean;
5757
createIndexes: boolean;
5858
count: boolean;
59+
aggregate: boolean;
5960
ismaster: boolean;
6061
indexes?: unknown[];
6162
query?: Record<string, unknown>;
@@ -166,6 +167,7 @@ export enum MongodbCommandType {
166167
FIND_AND_MODIFY = 'findAndModify',
167168
IS_MASTER = 'isMaster',
168169
COUNT = 'count',
170+
AGGREGATE = 'aggregate',
169171
UNKNOWN = 'unknown',
170172
}
171173

plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v3.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,32 @@ describe('MongoDBInstrumentation-Tracing-v3', () => {
266266
});
267267
});
268268
});
269+
270+
it('should create a child span for aggregation', done => {
271+
const span = trace.getTracer('default').startSpan('indexRootSpan');
272+
context.with(trace.setSpan(context.active(), span), () => {
273+
collection
274+
.aggregate([
275+
{ $match: { key: 'value' } },
276+
{ $group: { _id: '$a', count: { $sum: 1 } } },
277+
])
278+
.toArray()
279+
.then(() => {
280+
span.end();
281+
assertSpans(
282+
getTestSpans(),
283+
'mongodb.aggregate',
284+
SpanKind.CLIENT,
285+
'aggregate',
286+
undefined
287+
);
288+
done();
289+
})
290+
.catch(err => {
291+
done(err);
292+
});
293+
});
294+
});
269295
});
270296

271297
describe('when using enhanced database reporting without db statementSerializer', () => {
@@ -309,6 +335,48 @@ describe('MongoDBInstrumentation-Tracing-v3', () => {
309335
});
310336
});
311337
});
338+
339+
it('should properly collect nested db statement (hide attribute values)', done => {
340+
const span = trace.getTracer('default').startSpan('insertRootSpan');
341+
context.with(trace.setSpan(context.active(), span), () => {
342+
collection
343+
.aggregate([
344+
{ $match: { key: 'value' } },
345+
{ $group: { _id: '$a', count: { $sum: 1 } } },
346+
])
347+
.toArray()
348+
.then(() => {
349+
span.end();
350+
const spans = getTestSpans();
351+
const operationName = 'mongodb.aggregate';
352+
assertSpans(
353+
spans,
354+
operationName,
355+
SpanKind.CLIENT,
356+
'aggregate',
357+
undefined,
358+
false,
359+
false
360+
);
361+
const mongoSpan = spans.find(s => s.name === operationName);
362+
const dbStatement = JSON.parse(
363+
mongoSpan!.attributes[SEMATTRS_DB_STATEMENT] as string
364+
);
365+
assert.deepEqual(dbStatement, {
366+
aggregate: '?',
367+
pipeline: [
368+
{ $match: { key: '?' } },
369+
{ $group: { _id: '?', count: { $sum: '?' } } },
370+
],
371+
cursor: {},
372+
});
373+
done();
374+
})
375+
.catch(err => {
376+
done(err);
377+
});
378+
});
379+
});
312380
});
313381

314382
describe('when specifying a dbStatementSerializer configuration', () => {

plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v4.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,32 @@ describe('MongoDBInstrumentation-Tracing-v4', () => {
301301
});
302302
});
303303
});
304+
305+
it('should create a child span for aggregation', done => {
306+
const span = trace.getTracer('default').startSpan('indexRootSpan');
307+
context.with(trace.setSpan(context.active(), span), () => {
308+
collection
309+
.aggregate([
310+
{ $match: { key: 'value' } },
311+
{ $group: { _id: '$a', count: { $sum: 1 } } },
312+
])
313+
.toArray()
314+
.then(() => {
315+
span.end();
316+
assertSpans(
317+
getTestSpans(),
318+
'mongodb.aggregate',
319+
SpanKind.CLIENT,
320+
'aggregate',
321+
undefined
322+
);
323+
done();
324+
})
325+
.catch(err => {
326+
done(err);
327+
});
328+
});
329+
});
304330
});
305331

306332
describe('when using enhanced database reporting without db statementSerializer', () => {
@@ -344,6 +370,48 @@ describe('MongoDBInstrumentation-Tracing-v4', () => {
344370
});
345371
});
346372
});
373+
374+
it('should properly collect nested db statement (hide attribute values)', done => {
375+
const span = trace.getTracer('default').startSpan('insertRootSpan');
376+
context.with(trace.setSpan(context.active(), span), () => {
377+
collection
378+
.aggregate([
379+
{ $match: { key: 'value' } },
380+
{ $group: { _id: '$a', count: { $sum: 1 } } },
381+
])
382+
.toArray()
383+
.then(() => {
384+
span.end();
385+
const spans = getTestSpans();
386+
const operationName = 'mongodb.aggregate';
387+
assertSpans(
388+
spans,
389+
operationName,
390+
SpanKind.CLIENT,
391+
'aggregate',
392+
undefined,
393+
false,
394+
false
395+
);
396+
const mongoSpan = spans.find(s => s.name === operationName);
397+
const dbStatement = JSON.parse(
398+
mongoSpan!.attributes[SEMATTRS_DB_STATEMENT] as string
399+
);
400+
assert.deepEqual(dbStatement, {
401+
aggregate: '?',
402+
pipeline: [
403+
{ $match: { key: '?' } },
404+
{ $group: { _id: '?', count: { $sum: '?' } } },
405+
],
406+
cursor: {},
407+
});
408+
done();
409+
})
410+
.catch(err => {
411+
done(err);
412+
});
413+
});
414+
});
347415
});
348416

349417
describe('when specifying a dbStatementSerializer configuration', () => {

plugins/node/opentelemetry-instrumentation-mongodb/test/mongodb-v5-v6.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,32 @@ describe('MongoDBInstrumentation-Tracing-v5', () => {
304304
});
305305
});
306306
});
307+
308+
it('should create a child span for aggregation', done => {
309+
const span = trace.getTracer('default').startSpan('indexRootSpan');
310+
context.with(trace.setSpan(context.active(), span), () => {
311+
collection
312+
.aggregate([
313+
{ $match: { key: 'value' } },
314+
{ $group: { _id: '$a', count: { $sum: 1 } } },
315+
])
316+
.toArray()
317+
.then(() => {
318+
span.end();
319+
assertSpans(
320+
getTestSpans(),
321+
'mongodb.aggregate',
322+
SpanKind.CLIENT,
323+
'aggregate',
324+
undefined
325+
);
326+
done();
327+
})
328+
.catch(err => {
329+
done(err);
330+
});
331+
});
332+
});
307333
});
308334

309335
describe('when using enhanced database reporting without db statementSerializer', () => {
@@ -347,6 +373,48 @@ describe('MongoDBInstrumentation-Tracing-v5', () => {
347373
});
348374
});
349375
});
376+
377+
it('should properly collect nested db statement (hide attribute values)', done => {
378+
const span = trace.getTracer('default').startSpan('insertRootSpan');
379+
context.with(trace.setSpan(context.active(), span), () => {
380+
collection
381+
.aggregate([
382+
{ $match: { key: 'value' } },
383+
{ $group: { _id: '$a', count: { $sum: 1 } } },
384+
])
385+
.toArray()
386+
.then(() => {
387+
span.end();
388+
const spans = getTestSpans();
389+
const operationName = 'mongodb.aggregate';
390+
assertSpans(
391+
spans,
392+
operationName,
393+
SpanKind.CLIENT,
394+
'aggregate',
395+
undefined,
396+
false,
397+
false
398+
);
399+
const mongoSpan = spans.find(s => s.name === operationName);
400+
const dbStatement = JSON.parse(
401+
mongoSpan!.attributes[SEMATTRS_DB_STATEMENT] as string
402+
);
403+
assert.deepEqual(dbStatement, {
404+
aggregate: '?',
405+
pipeline: [
406+
{ $match: { key: '?' } },
407+
{ $group: { _id: '?', count: { $sum: '?' } } },
408+
],
409+
cursor: {},
410+
});
411+
done();
412+
})
413+
.catch(err => {
414+
done(err);
415+
});
416+
});
417+
});
350418
});
351419

352420
describe('when specifying a dbStatementSerializer configuration', () => {

0 commit comments

Comments
 (0)