Skip to content

Commit

Permalink
feat(instrumentation-ioredis): add request hook (#415)
Browse files Browse the repository at this point in the history
Co-authored-by: Bartlomiej Obecny <[email protected]>
  • Loading branch information
Amir Blum and obecny authored Apr 27, 2021
1 parent 041508f commit 550e32c
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 26 deletions.
47 changes: 37 additions & 10 deletions plugins/node/opentelemetry-instrumentation-ioredis/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ npm install --save @opentelemetry/instrumentation-ioredis
To load a specific instrumentation (**ioredis** in this case), specify it in the registerInstrumentations's configuration

```javascript
const { NodeTracerProvider } = require('@opentelemetry/node');
const { IORedisInstrumentation } = require('@opentelemetry/instrumentation-ioredis');
const { registerInstrumentations } = require('@opentelemetry/instrumentation');
const { NodeTracerProvider } = require("@opentelemetry/node");
const {
IORedisInstrumentation,
} = require("@opentelemetry/instrumentation-ioredis");
const { registerInstrumentations } = require("@opentelemetry/instrumentation");

const provider = new NodeTracerProvider();
provider.register();
Expand All @@ -36,20 +38,21 @@ registerInstrumentations({
instrumentations: [
new IORedisInstrumentation({
// see under for available configuration
})
}),
],
})
});
```

### IORedis Instrumentation Options

IORedis instrumentation has few options available to choose from. You can set the following:

| Options | Type | Description |
| ------- | ---- | ----------- |
| `dbStatementSerializer` | `DbStatementSerializer` | IORedis instrumentation will serialize db.statement using the specified function. |
| `responseHook` | `RedisResponseCustomAttributeFunction` | Function for adding custom attributes on db response |
| `requireParentSpan` | `boolean` | Require parent to create ioredis span, default when unset is true |
| Options | Type | Description |
| ----------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `dbStatementSerializer` | `DbStatementSerializer` | IORedis instrumentation will serialize db.statement using the specified function. |
| `requestHook` | `RedisRequestCustomAttributeFunction` (function) | Function for adding custom attributes on db request. Receives params: `span, { moduleVersion, cmdName, cmdArgs }` |
| `responseHook` | `RedisResponseCustomAttributeFunction` (function) | Function for adding custom attributes on db response |
| `requireParentSpan` | `boolean` | Require parent to create ioredis span, default when unset is true |

#### Custom db.statement Serializer

Expand All @@ -64,6 +67,30 @@ const { IORedisInstrumentation } = require('@opentelemetry/instrumentation-iored
const ioredisInstrumentation = new IORedisInstrumentation({
dbStatementSerializer: function (cmdName, cmdArgs) {
return cmdName;
},
});
```

#### Using `requestHook`

Instrumentation user can configure a custom "hook" function which will be called on every request with the relevant span and request information. User can then set custom attributes on the span or run any instrumentation-extension logic per request.

Here is a simple example that adds a span attribute of `ioredis` instrumented version on each request:

```javascript
const { IORedisInstrumentation } = require('@opentelemetry/instrumentation-ioredis');

const ioredisInstrumentation = new IORedisInstrumentation({
requestHook: function (
span: Span,
requestInfo: IORedisRequestHookInformation
) {
if (requestInfo.moduleVersion) {
span.setAttribute(
'instrumented_library.version',
requestInfo.moduleVersion
);
}
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ export class IORedisInstrumentation extends InstrumentationBase<
new InstrumentationNodeModuleDefinition<typeof ioredisTypes>(
'ioredis',
['>1 <5'],
moduleExports => {
(moduleExports, moduleVersion?: string) => {
diag.debug('Applying patch for ioredis');
if (isWrapped(moduleExports.prototype.sendCommand)) {
this._unwrap(moduleExports.prototype, 'sendCommand');
}
this._wrap(
moduleExports.prototype,
'sendCommand',
this._patchSendCommand()
this._patchSendCommand(moduleVersion)
);
if (isWrapped(moduleExports.prototype.connect)) {
this._unwrap(moduleExports.prototype, 'connect');
Expand All @@ -80,9 +80,14 @@ export class IORedisInstrumentation extends InstrumentationBase<
/**
* Patch send command internal to trace requests
*/
private _patchSendCommand() {
private _patchSendCommand(moduleVersion?: string) {
return (original: Function) => {
return traceSendCommand(this.tracer, original, this._config);
return traceSendCommand(
this.tracer,
original,
this._config,
moduleVersion
);
};
}

Expand Down
13 changes: 13 additions & 0 deletions plugins/node/opentelemetry-instrumentation-ioredis/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ export type DbStatementSerializer = (
cmdArgs: IORedisCommand['args']
) => string;

export interface IORedisRequestHookInformation {
moduleVersion?: string;
cmdName: IORedisCommand['name'];
cmdArgs: IORedisCommand['args'];
}

export interface RedisRequestCustomAttributeFunction {
(span: Span, requestInfo: IORedisRequestHookInformation): void;
}

/**
* Function that can be used to add custom attributes to span on response from redis server
* @param span - The span created for the redis command, on which attributes can be set
Expand All @@ -64,6 +74,9 @@ export interface IORedisInstrumentationConfig extends InstrumentationConfig {
/** Custom serializer function for the db.statement tag */
dbStatementSerializer?: DbStatementSerializer;

/** Function for adding custom attributes on db request */
requestHook?: RedisRequestCustomAttributeFunction;

/** Function for adding custom attributes on db response */
responseHook?: RedisResponseCustomAttributeFunction;

Expand Down
22 changes: 20 additions & 2 deletions plugins/node/opentelemetry-instrumentation-ioredis/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ const defaultDbStatementSerializer: DbStatementSerializer = (
export const traceSendCommand = (
tracer: Tracer,
original: Function,
config?: IORedisInstrumentationConfig
config?: IORedisInstrumentationConfig,
moduleVersion?: string
) => {
const dbStatementSerializer =
config?.dbStatementSerializer || defaultDbStatementSerializer;
Expand All @@ -107,6 +108,23 @@ export const traceSendCommand = (
},
});

if (config?.requestHook) {
safeExecuteInTheMiddle(
() =>
config?.requestHook!(span, {
moduleVersion,
cmdName: cmd.name,
cmdArgs: cmd.args,
}),
e => {
if (e) {
diag.error('ioredis instrumentation: request hook failed', e);
}
},
true
);
}

const { host, port } = this.options;

span.setAttributes({
Expand All @@ -125,7 +143,7 @@ export const traceSendCommand = (
() => config?.responseHook?.(span, cmd.name, cmd.args, result),
e => {
if (e) {
diag.error('ioredis response hook failed', e);
diag.error('ioredis instrumentation: response hook failed', e);
}
},
true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { IORedisInstrumentation } from '../src';
import {
IORedisInstrumentationConfig,
DbStatementSerializer,
IORedisRequestHookInformation,
} from '../src/types';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';

Expand Down Expand Up @@ -226,6 +227,13 @@ describe('ioredis', () => {
});

describe('Instrumenting query operations', () => {
before(() => {
instrumentation.disable();
instrumentation = new IORedisInstrumentation();
instrumentation.setTracerProvider(provider);
require('ioredis');
});

IOREDIS_CALLBACK_OPERATIONS.forEach(command => {
it(`should create a child span for cb style ${command.description}`, done => {
const attributes = {
Expand Down Expand Up @@ -762,7 +770,74 @@ describe('ioredis', () => {
});
});

describe('Instrumenting with a custom responseHook', () => {
describe('Instrumenting with a custom hooks', () => {
it('should call requestHook when set in config', async () => {
instrumentation.disable();
const config: IORedisInstrumentationConfig = {
requestHook: (
span: Span,
requestInfo: IORedisRequestHookInformation
) => {
assert.ok(
/\d{1,4}\.\d{1,4}\.\d{1,5}.*/.test(
requestInfo.moduleVersion as string
)
);
assert.strictEqual(requestInfo.cmdName, 'incr');
assert.deepStrictEqual(requestInfo.cmdArgs, ['request-hook-test']);

span.setAttribute(
'attribute key from request hook',
'custom value from request hook'
);
},
};
instrumentation = new IORedisInstrumentation(config);
instrumentation.setTracerProvider(provider);
require('ioredis');

const span = provider.getTracer('ioredis-test').startSpan('test span');
await context.with(setSpan(context.active(), span), async () => {
await client.incr('request-hook-test');
const endedSpans = memoryExporter.getFinishedSpans();
assert.strictEqual(endedSpans.length, 1);
assert.strictEqual(
endedSpans[0].attributes['attribute key from request hook'],
'custom value from request hook'
);
});
});

it('should ignore requestHook which throws exception', async () => {
instrumentation.disable();
const config: IORedisInstrumentationConfig = {
requestHook: (
span: Span,
_requestInfo: IORedisRequestHookInformation
) => {
span.setAttribute(
'attribute key BEFORE exception',
'this attribute is added to span BEFORE exception is thrown thus we can expect it'
);
throw Error('error thrown in requestHook');
},
};
instrumentation = new IORedisInstrumentation(config);
instrumentation.setTracerProvider(provider);
require('ioredis');

const span = provider.getTracer('ioredis-test').startSpan('test span');
await context.with(setSpan(context.active(), span), async () => {
await client.incr('request-hook-throw-test');
const endedSpans = memoryExporter.getFinishedSpans();
assert.strictEqual(endedSpans.length, 1);
assert.strictEqual(
endedSpans[0].attributes['attribute key BEFORE exception'],
'this attribute is added to span BEFORE exception is thrown thus we can expect it'
);
});
});

it('should call responseHook when set in config', async () => {
instrumentation.disable();
const config: IORedisInstrumentationConfig = {
Expand All @@ -772,13 +847,17 @@ describe('ioredis', () => {
_cmdArgs: Array<string | Buffer | number>,
response: unknown
) => {
assert.strictEqual(cmdName, 'incr');
// the command is 'incr' on a key which does not exist, thus it increase 0 by 1 and respond 1
assert.strictEqual(response, 1);
span.setAttribute(
'attribute key from hook',
'custom value from hook'
);
try {
assert.strictEqual(cmdName, 'incr');
// the command is 'incr' on a key which does not exist, thus it increase 0 by 1 and respond 1
assert.strictEqual(response, 1);
span.setAttribute(
'attribute key from hook',
'custom value from hook'
);
} catch (err) {
console.log(err);
}
},
};
instrumentation = new IORedisInstrumentation(config);
Expand All @@ -787,7 +866,7 @@ describe('ioredis', () => {

const span = provider.getTracer('ioredis-test').startSpan('test span');
await context.with(setSpan(context.active(), span), async () => {
await client.incr('new-key');
await client.incr('response-hook-test');
const endedSpans = memoryExporter.getFinishedSpans();
assert.strictEqual(endedSpans.length, 1);
assert.strictEqual(
Expand Down Expand Up @@ -815,7 +894,7 @@ describe('ioredis', () => {

const span = provider.getTracer('ioredis-test').startSpan('test span');
await context.with(setSpan(context.active(), span), async () => {
await client.incr('some-key');
await client.incr('response-hook-throw-test');
const endedSpans = memoryExporter.getFinishedSpans();

// hook throw exception, but span should not be affected
Expand Down

0 comments on commit 550e32c

Please sign in to comment.