Skip to content

Commit

Permalink
feat(mongodb): add db statement serializer config (#626)
Browse files Browse the repository at this point in the history
  • Loading branch information
nozik committed Aug 31, 2021
1 parent aec1518 commit 8584432
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 12 deletions.
Expand Up @@ -51,6 +51,7 @@ Mongodb instrumentation has few options available to choose from. You can set th
| ------- | ---- | ----------- |
| [`enhancedDatabaseReporting`](https://github.com/open-telemetry/opentelemetry-js/blob/main/packages/opentelemetry-api/src/trace/instrumentation/instrumentation.ts#L91) | `string` | If true, additional information about query parameters and results will be attached (as `attributes`) to spans representing database operations |
| `responseHook` | `MongoDBInstrumentationExecutionResponseHook` (function) | Function for adding custom attributes from db response |
| `dbStatementSerializer` | `DbStatementSerializer` (function) | Custom serializer function for the db.statement tag |

## Useful links

Expand Down
Expand Up @@ -153,12 +153,13 @@ export class MongoDBInstrumentation extends InstrumentationBase<
kind: SpanKind.CLIENT,
}
);

instrumentation._populateAttributes(
span,
ns,
server,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
operationName !== 'insert' ? (ops[0] as any) : undefined
ops[0] as any
);
const patchedCallback = instrumentation._patchEnd(span, resultHandler);
// handle when options is the callback to send the correct number of args
Expand Down Expand Up @@ -408,15 +409,34 @@ export class MongoDBInstrumentation extends InstrumentationBase<

// capture parameters within the query as well if enhancedDatabaseReporting is enabled.
const commandObj = command.query ?? command.q ?? command;
const query =
this._config?.enhancedDatabaseReporting === true
? commandObj
: Object.keys(commandObj).reduce((obj, key) => {
obj[key] = '?';
return obj;
}, {} as { [key: string]: unknown });
const dbStatementSerializer =
typeof this._config.dbStatementSerializer === 'function'
? this._config.dbStatementSerializer
: this._defaultDbStatementSerializer.bind(this);

safeExecuteInTheMiddle(
() => {
const query = dbStatementSerializer(commandObj);
span.setAttribute(SemanticAttributes.DB_STATEMENT, query);
},
err => {
if (err) {
this._diag.error('Error running dbStatementSerializer hook', err);
}
},
true
);
}

span.setAttribute(SemanticAttributes.DB_STATEMENT, JSON.stringify(query));
private _defaultDbStatementSerializer(commandObj: Record<string, unknown>) {
const enhancedDbReporting = !!this._config?.enhancedDatabaseReporting;
const resultObj = enhancedDbReporting
? commandObj
: Object.keys(commandObj).reduce((obj, key) => {
obj[key] = '?';
return obj;
}, {} as { [key: string]: unknown });
return JSON.stringify(resultObj);
}

/**
Expand Down
13 changes: 13 additions & 0 deletions plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts
Expand Up @@ -21,6 +21,14 @@ export interface MongoDBInstrumentationExecutionResponseHook {
(span: Span, responseInfo: MongoResponseHookInformation): void;
}

/**
* Function that can be used to serialize db.statement tag
* @param cmd - MongoDB command object
*
* @returns serialized string that will be used as the db.statement attribute.
*/
export type DbStatementSerializer = (cmd: Record<string, unknown>) => string;

export interface MongoDBInstrumentationConfig extends InstrumentationConfig {
/**
* If true, additional information about query parameters and
Expand All @@ -36,6 +44,11 @@ export interface MongoDBInstrumentationConfig extends InstrumentationConfig {
* @default undefined
*/
responseHook?: MongoDBInstrumentationExecutionResponseHook;

/**
* Custom serializer function for the db.statement tag
*/
dbStatementSerializer?: DbStatementSerializer;
}

export type Func<T> = (...args: unknown[]) => T;
Expand Down
Expand Up @@ -33,6 +33,7 @@ instrumentation.disable();

import * as mongodb from 'mongodb';
import { assertSpans, accessCollection } from './utils';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';

describe('MongoDBInstrumentation', () => {
function create(config: MongoDBInstrumentationConfig = {}) {
Expand Down Expand Up @@ -249,6 +250,96 @@ describe('MongoDBInstrumentation', () => {
});
});

describe('when using enhanced database reporting without db statementSerializer', () => {
const key = 'key';
const value = 'value';
const object = { [key]: value };

beforeEach(() => {
memoryExporter.reset();
create({
enhancedDatabaseReporting: false,
});
});

it('should properly collect db statement (hide attribute values)', done => {
const span = provider.getTracer('default').startSpan('insertRootSpan');
context.with(trace.setSpan(context.active(), span), () => {
collection.insertOne(object).then(() => {
span.end();
const spans = memoryExporter.getFinishedSpans();
const operationName = 'mongodb.insert';
assertSpans(spans, operationName, SpanKind.CLIENT, false, false);
const mongoSpan = spans.find(s => s.name === operationName);
const dbStatement = JSON.parse(
mongoSpan!.attributes[SemanticAttributes.DB_STATEMENT] as string
);
assert.strictEqual(dbStatement[key], '?');
done();
});
});
});
});

describe('when specifying a dbStatementSerializer configuration', () => {
const key = 'key';
const value = 'value';
const object = { [key]: value };

describe('with a valid function', () => {
beforeEach(() => {
memoryExporter.reset();
create({
dbStatementSerializer: (commandObj: Record<string, unknown>) => {
return JSON.stringify(commandObj);
},
});
});

it('should properly collect db statement', done => {
const span = provider.getTracer('default').startSpan('insertRootSpan');
context.with(trace.setSpan(context.active(), span), () => {
collection.insertOne(object).then(() => {
span.end();
const spans = memoryExporter.getFinishedSpans();
const operationName = 'mongodb.insert';
assertSpans(spans, operationName, SpanKind.CLIENT, false, true);
const mongoSpan = spans.find(s => s.name === operationName);
const dbStatement = JSON.parse(
mongoSpan!.attributes[SemanticAttributes.DB_STATEMENT] as string
);
assert.strictEqual(dbStatement[key], value);
done();
});
});
});
});

describe('with an invalid function', () => {
beforeEach(() => {
memoryExporter.reset();
create({
enhancedDatabaseReporting: true,
dbStatementSerializer: (_commandObj: Record<string, unknown>) => {
throw new Error('something went wrong!');
},
});
});

it('should not do any harm when throwing an exception', done => {
const span = provider.getTracer('default').startSpan('insertRootSpan');
context.with(trace.setSpan(context.active(), span), () => {
collection.insertOne(object).then(() => {
span.end();
const spans = memoryExporter.getFinishedSpans();
assertSpans(spans, 'mongodb.insert', SpanKind.CLIENT);
done();
});
});
});
});
});

describe('when specifying a responseHook configuration', () => {
const dataAttributeName = 'mongodb_data';
beforeEach(() => {
Expand Down
Expand Up @@ -86,9 +86,9 @@ export function assertSpans(
assert.strictEqual(mongoSpan.status.code, SpanStatusCode.UNSET);

if (isEnhancedDatabaseReportingEnabled) {
const dbStatement = mongoSpan.attributes[
SemanticAttributes.DB_STATEMENT
] as any;
const dbStatement = JSON.parse(
mongoSpan.attributes[SemanticAttributes.DB_STATEMENT] as string
);
for (const key in dbStatement) {
assert.notStrictEqual(dbStatement[key], '?');
}
Expand Down

0 comments on commit 8584432

Please sign in to comment.