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(mongo instrumentation): added response hook option #533

Merged
merged 1 commit into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Mongodb instrumentation has few options available to choose from. You can set th
| Options | Type | Description |
| ------- | ---- | ----------- |
| [`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 |

## Useful links

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
InstrumentationNodeModuleDefinition,
InstrumentationNodeModuleFile,
isWrapped,
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import type * as mongodb from 'mongodb';
Expand All @@ -37,6 +38,7 @@ import {
MongoInternalCommand,
MongoInternalTopology,
WireProtocolInternal,
CommandResult,
} from './types';
import { VERSION } from './version';

Expand Down Expand Up @@ -417,6 +419,29 @@ export class MongoDBInstrumentation extends InstrumentationBase<
span.setAttribute(SemanticAttributes.DB_STATEMENT, JSON.stringify(query));
}

/**
* Triggers the response hook in case it is defined.
* @param span The span to add the results to.
* @param config The MongoDB instrumentation config object
* @param result The command result
*/
private _handleExecutionResult(span: Span, result: CommandResult) {
const config: MongoDBInstrumentationConfig = this.getConfig();
if (typeof config.responseHook === 'function') {
safeExecuteInTheMiddle(
() => {
config.responseHook!(span, { data: result });
},
err => {
if (err) {
this._diag.error('Error running response hook', err);
}
},
true
);
}
}

/**
* Ends a created span.
* @param span The created span to end.
Expand All @@ -426,13 +451,17 @@ export class MongoDBInstrumentation extends InstrumentationBase<
// mongodb is using "tick" when calling a callback, this way the context
// in final callback (resultHandler) is lost
const activeContext = context.active();
const instrumentation = this;
return function patchedEnd(this: {}, ...args: unknown[]) {
const error = args[0];
if (error instanceof Error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
} else {
const result = args[1] as CommandResult;
instrumentation._handleExecutionResult(span, result);
}
span.end();

Expand Down
24 changes: 24 additions & 0 deletions plugins/node/opentelemetry-instrumentation-mongodb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
*/

import { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { Span } from '@opentelemetry/api';

export interface MongoDBInstrumentationExecutionResponseHook {
(span: Span, responseInfo: MongoResponseHookInformation): void;
}

export interface MongoDBInstrumentationConfig extends InstrumentationConfig {
/**
Expand All @@ -23,6 +28,14 @@ export interface MongoDBInstrumentationConfig extends InstrumentationConfig {
* database operations.
*/
enhancedDatabaseReporting?: boolean;

/**
* Hook that allows adding custom span attributes based on the data
* returned from MongoDB actions.
*
* @default undefined
*/
responseHook?: MongoDBInstrumentationExecutionResponseHook;
}

export type Func<T> = (...args: unknown[]) => T;
Expand All @@ -43,6 +56,17 @@ export type CursorState = { cmd: MongoInternalCommand } & Record<
unknown
>;

export interface MongoResponseHookInformation {
data: CommandResult;
}

// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/connection/command_result.js
export type CommandResult = {
result?: unknown;
connection?: unknown;
message?: unknown;
};

// https://github.com/mongodb/node-mongodb-native/blob/3.6/lib/core/wireprotocol/index.js
export type WireProtocolInternal = {
insert: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@

// for testing locally "npm run docker:start"

import { context, trace, SpanKind } from '@opentelemetry/api';
import { context, trace, SpanKind, Span } from '@opentelemetry/api';
import { BasicTracerProvider } from '@opentelemetry/tracing';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import {
InMemorySpanExporter,
SimpleSpanProcessor,
} from '@opentelemetry/tracing';
import * as assert from 'assert';
import { MongoDBInstrumentation } from '../src';
import { MongoDBInstrumentation, MongoDBInstrumentationConfig } from '../src';
import { MongoResponseHookInformation } from '../src/types';

const instrumentation = new MongoDBInstrumentation();
instrumentation.enable();
Expand All @@ -34,6 +35,10 @@ import * as mongodb from 'mongodb';
import { assertSpans, accessCollection } from './utils';

describe('MongoDBInstrumentation', () => {
function create(config: MongoDBInstrumentationConfig = {}) {
instrumentation.setConfig(config);
instrumentation.enable();
}
// For these tests, mongo must be running. Add RUN_MONGODB_TESTS to run
// these tests.
const RUN_MONGODB_TESTS = process.env.RUN_MONGODB_TESTS as string;
Expand Down Expand Up @@ -244,6 +249,93 @@ describe('MongoDBInstrumentation', () => {
});
});

describe('when specifying a responseHook configuration', () => {
const dataAttributeName = 'mongodb_data';
beforeEach(() => {
memoryExporter.reset();
});

describe('with a valid function', () => {
beforeEach(() => {
create({
responseHook: (span: Span, result: MongoResponseHookInformation) => {
span.setAttribute(
dataAttributeName,
JSON.stringify(result.data.result)
);
},
});
});

it('should attach response hook data to the resulting span for insert function', done => {
const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }];
const span = provider.getTracer('default').startSpan('insertRootSpan');
context.with(trace.setSpan(context.active(), span), () => {
collection.insertMany(insertData, (err, result) => {
span.end();
assert.ifError(err);
const spans = memoryExporter.getFinishedSpans();
const insertSpan = spans[0];

assert.deepStrictEqual(
JSON.parse(insertSpan.attributes[dataAttributeName] as string),
result.result
);

done();
});
});
});

it('should attach response hook data to the resulting span for find function', done => {
const span = provider.getTracer('default').startSpan('findRootSpan');
context.with(trace.setSpan(context.active(), span), () => {
collection.find({ a: 1 }).toArray((err, results) => {
span.end();
assert.ifError(err);
const spans = memoryExporter.getFinishedSpans();
const findSpan = spans[0];
const hookAttributeValue = JSON.parse(
findSpan.attributes[dataAttributeName] as string
);

assert.strictEqual(
hookAttributeValue?.cursor?.firstBatch[0]._id,
results[0]._id.toString()
);

done();
});
});
});
});

describe('with an invalid function', () => {
beforeEach(() => {
create({
responseHook: (span: Span, result: MongoResponseHookInformation) => {
throw 'some error';
},
});
});

it('should not do any harm when throwing an exception', done => {
const span = provider.getTracer('default').startSpan('findRootSpan');
context.with(trace.setSpan(context.active(), span), () => {
collection.find({ a: 1 }).toArray((err, results) => {
span.end();
const spans = memoryExporter.getFinishedSpans();

assert.ifError(err);
assertSpans(spans, 'mongodb.find', SpanKind.CLIENT);

done();
});
});
});
});
});

describe('Mixed operations with callback', () => {
beforeEach(() => {
memoryExporter.reset();
Expand Down