Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mongodb): add db statement serializer config #626

Merged
merged 12 commits into from Aug 31, 2021
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
dyladan marked this conversation as resolved.
Show resolved Hide resolved
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;
nozik marked this conversation as resolved.
Show resolved Hide resolved
}

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', () => {
blumamir marked this conversation as resolved.
Show resolved Hide resolved
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