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(instrumentation-ioredis): add response hook for custom attributes #343

Merged
merged 9 commits into from
Feb 25, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ IORedis instrumentation has few options available to choose from. You can set th
| 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 |

#### Custom db.statement Serializer
The instrumentation serializes the whole command into a Span attribute called `db.statement`. The standard serialization format is `{cmdName} {cmdArgs.join(',')}`.
Expand Down
22 changes: 22 additions & 0 deletions plugins/node/opentelemetry-instrumentation-ioredis/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import type * as ioredisTypes from 'ioredis';
import { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { Span } from '@opentelemetry/api';

// eslint-disable-next-line @typescript-eslint/interface-name-prefix
export interface IORedisCommand {
Expand All @@ -39,11 +40,32 @@ export type DbStatementSerializer = (
cmdArgs: IORedisCommand['args']
) => string;

/**
* 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
* @param cmdName - The name of the command (eg. set, get, mset)
* @param cmdArgs - Array of arguments passed to the command
* @param response - The response object which is returned to the user who called this command.
* Can be used to set custom attributes on the span
*/
export interface RedisResponseCustomAttributeFunction {
(
span: Span,
cmdName: IORedisCommand['name'],
cmdArgs: IORedisCommand['args'],
/* eslint-disable @typescript-eslint/no-explicit-any */
response: any
Flarna marked this conversation as resolved.
Show resolved Hide resolved
): void;
}

/**
* Options available for the IORedis Instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-ioredis#ioredis-instrumentation-options))
*/
// eslint-disable-next-line @typescript-eslint/interface-name-prefix
export interface IORedisInstrumentationConfig extends InstrumentationConfig {
/** Custom serializer function for the db.statement tag */
dbStatementSerializer?: DbStatementSerializer;

/** Function for adding custom attributes on db response */
responseHook?: RedisResponseCustomAttributeFunction;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
DatabaseAttribute,
GeneralAttribute,
} from '@opentelemetry/semantic-conventions';
import { safeExecuteInTheMiddle } from '@opentelemetry/instrumentation';

const endSpan = (span: Span, err: NodeJS.ErrnoException | null | undefined) => {
if (err) {
Expand Down Expand Up @@ -121,6 +122,12 @@ export const traceSendCommand = (
const origResolve = cmd.resolve;
/* eslint-disable @typescript-eslint/no-explicit-any */
cmd.resolve = function (result: any) {
safeExecuteInTheMiddle(
() => config?.responseHook?.(span, cmd.name, cmd.args, result),
() => {},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry i didn't catch this before but i think it could be better if we could log an error so the user know that its hook is failing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense. added logging on error :)

true
);

endSpan(span, null);
origResolve(result);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Status,
getSpan,
setSpan,
Span,
} from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/node';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
Expand Down Expand Up @@ -711,5 +712,67 @@ describe('ioredis', () => {
});
});
});

describe('Instrumenting with a custom responseHook', () => {
it('should call responseHook when set in config', async () => {
instrumentation.disable();
const config: IORedisInstrumentationConfig = {
responseHook: (
span: Span,
cmdName: string,
_cmdArgs: Array<string | Buffer | number>,
response: any
) => {
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'
);
},
};
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('new-key');
const endedSpans = memoryExporter.getFinishedSpans();
assert.strictEqual(endedSpans.length, 1);
assert.strictEqual(
endedSpans[0].attributes['attribute key from hook'],
'custom value from hook'
);
});
});

it('should ignore responseHook which throws exception', async () => {
instrumentation.disable();
const config: IORedisInstrumentationConfig = {
responseHook: (
_span: Span,
_cmdName: string,
_cmdArgs: Array<string | Buffer | number>,
_response: any
) => {
throw Error('error thrown in responseHook');
},
};
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('some-key');
const endedSpans = memoryExporter.getFinishedSpans();

// hook throw exception, but span should not be affected
assert.strictEqual(endedSpans.length, 1);
});
});
});
});
});