From fa583fde07425f0aca75d3902d13e5a7d9a5dc65 Mon Sep 17 00:00:00 2001 From: Mark Wolff Date: Mon, 18 Nov 2019 23:18:50 -0800 Subject: [PATCH] feat(plugin): implement redis plugin (#503) * feat(plugin): implement redis plugin * fix: circle redis testing * fix: set span error status * fix: run the redis service * fix: linting * feat: add redis error handling statuses * fix: pr comments * fix: redis linting * refactor: move patches to utils for clarity * fix: linting --- .circleci/config.yml | 18 +- packages/opentelemetry-plugin-redis/README.md | 29 ++- .../opentelemetry-plugin-redis/package.json | 14 +- .../opentelemetry-plugin-redis/src/enums.ts | 32 +++ .../opentelemetry-plugin-redis/src/index.ts | 2 + .../opentelemetry-plugin-redis/src/redis.ts | 101 ++++++++ .../opentelemetry-plugin-redis/src/types.ts | 41 ++++ .../opentelemetry-plugin-redis/src/utils.ts | 126 ++++++++++ .../test/assertionUtils.ts | 76 +++++++ .../test/redis.test.ts | 215 ++++++++++++++++++ .../test/testUtils.ts | 54 +++++ 11 files changed, 697 insertions(+), 11 deletions(-) create mode 100644 packages/opentelemetry-plugin-redis/src/enums.ts create mode 100644 packages/opentelemetry-plugin-redis/src/redis.ts create mode 100644 packages/opentelemetry-plugin-redis/src/types.ts create mode 100644 packages/opentelemetry-plugin-redis/src/utils.ts create mode 100644 packages/opentelemetry-plugin-redis/test/assertionUtils.ts create mode 100644 packages/opentelemetry-plugin-redis/test/redis.test.ts create mode 100644 packages/opentelemetry-plugin-redis/test/testUtils.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 95d4c61fa2..ab5b0ddceb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,21 +1,26 @@ version: 2 -test_env: &test_env +node_test_env: &node_test_env RUN_POSTGRES_TESTS: 1 RUN_MONGODB_TESTS: 1 + RUN_REDIS_TESTS: 1 POSTGRES_USER: postgres POSTGRES_DB: circle_database POSTGRES_HOST: localhost POSTGRES_PORT: 5432 + OPENTELEMETRY_REDIS_HOST: 'localhost' + OPENTELEMETRY_REDIS_PORT: 6379 MONGODB_HOST: localhost MONGODB_PORT: 27017 MONGODB_DB: opentelemetry-tests postgres_service: &postgres_service image: circleci/postgres:9.6-alpine - environment: # env to pass to CircleCI, specified values must match test_env + environment: # env to pass to CircleCI, specified values must match node_test_env POSTGRES_USER: postgres POSTGRES_DB: circle_database +redis_service: &redis_service + image: redis mongo_service: &mongo_service image: mongo @@ -150,22 +155,25 @@ jobs: node8: docker: - image: node:8 - environment: *test_env + environment: *node_test_env - *postgres_service + - *redis_service - *mongo_service <<: *node_unit_tests node10: docker: - image: node:10 - environment: *test_env + environment: *node_test_env - *postgres_service + - *redis_service - *mongo_service <<: *node_unit_tests node12: docker: - image: node:12 - environment: *test_env + environment: *node_test_env - *postgres_service + - *redis_service - *mongo_service <<: *node_unit_tests node12-browsers: diff --git a/packages/opentelemetry-plugin-redis/README.md b/packages/opentelemetry-plugin-redis/README.md index 3509b21de3..266435a421 100644 --- a/packages/opentelemetry-plugin-redis/README.md +++ b/packages/opentelemetry-plugin-redis/README.md @@ -4,7 +4,7 @@ [![devDependencies][devDependencies-image]][devDependencies-url] [![Apache License][license-image]][license-image] -This module provides automatic instrumentation for [`redis`](https://github.com/NodeRedis/node_redis). +This module provides automatic instrumentation for [`redis@^2.6.0`](https://github.com/NodeRedis/node_redis). For automatic instrumentation see the [@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node) package. @@ -15,14 +15,37 @@ For automatic instrumentation see the npm install --save @opentelemetry/plugin-redis ``` +### Supported Versions + - `>=2.6.0` + ## Usage +OptenTelemetry Redis Instrumentation allows the user to automatically collect trace data and export them to the backend of choice, to give observability to distributed systems when working with [redis](https://www.npmjs.com/package/redis). + +To load a specific plugin (**redis** in this case), specify it in the Node Tracer's configuration +```js +const { NodeTracer } = require('@opentelemetry/node'); + +const tracer = new NodeTracer({ + plugins: { + redis: { + enabled: true, + // You may use a package name or absolute path to the file. + path: '@opentelemetry/plugin-redis', + } + } +}); ``` -const opentelemetry = require('@opentelemetry/plugin-redis'); -// TODO: DEMONSTRATE API +To load all the [supported plugins](https://github.com/open-telemetry/opentelemetry-js#plugins), use below approach. Each plugin is only loaded when the module that it patches is loaded; in other words, there is no computational overhead for listing plugins for unused modules. +```javascript +const { NodeTracer } = require('@opentelemetry/node'); + +const tracer = new NodeTracer(); ``` + + ## Useful links - For more information on OpenTelemetry, visit: - For more about OpenTelemetry JavaScript: diff --git a/packages/opentelemetry-plugin-redis/package.json b/packages/opentelemetry-plugin-redis/package.json index baa86c0f4f..07ea3b3e48 100644 --- a/packages/opentelemetry-plugin-redis/package.json +++ b/packages/opentelemetry-plugin-redis/package.json @@ -2,17 +2,19 @@ "name": "@opentelemetry/plugin-redis", "version": "0.2.0", "description": "OpenTelemetry redis automatic instrumentation package.", - "private": true, "main": "build/src/index.js", "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js", "scripts": { "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'", + "test:debug": "cross-env RUN_REDIS_TESTS_LOCAL=true ts-mocha --inspect-brk --no-timeouts -p tsconfig.json 'test/**/*.test.ts'", + "test:local": "cross-env RUN_REDIS_TESTS_LOCAL=true yarn test", "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", "check": "gts check", "precompile": "tsc --version", "compile": "tsc -p .", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", "fix": "gts fix", "prepare": "npm run compile" }, @@ -42,10 +44,16 @@ "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", + "@types/redis": "^2.8.14", + "@types/shimmer": "^1.0.1", + "@opentelemetry/node": "^0.2.0", + "@opentelemetry/tracing": "^0.2.0", "codecov": "^3.6.1", + "cross-env": "^6.0.3", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", + "redis": "^2.8.0", "rimraf": "^3.0.0", "ts-mocha": "^6.0.0", "ts-node": "^8.3.0", @@ -55,7 +63,7 @@ }, "dependencies": { "@opentelemetry/core": "^0.2.0", - "@opentelemetry/node": "^0.2.0", - "@opentelemetry/types": "^0.2.0" + "@opentelemetry/types": "^0.2.0", + "shimmer": "^1.2.1" } } diff --git a/packages/opentelemetry-plugin-redis/src/enums.ts b/packages/opentelemetry-plugin-redis/src/enums.ts new file mode 100644 index 0000000000..035287a14b --- /dev/null +++ b/packages/opentelemetry-plugin-redis/src/enums.ts @@ -0,0 +1,32 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum AttributeNames { + // required by https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-semantic-conventions.md#databases-client-calls + COMPONENT = 'component', + DB_TYPE = 'db.type', + DB_INSTANCE = 'db.instance', + DB_STATEMENT = 'db.statement', + PEER_ADDRESS = 'peer.address', + PEER_HOSTNAME = 'peer.host', + + // optional + DB_USER = 'db.user', + PEER_PORT = 'peer.port', + PEER_IPV4 = 'peer.ipv4', + PEER_IPV6 = 'peer.ipv6', + PEER_SERVICE = 'peer.service', +} diff --git a/packages/opentelemetry-plugin-redis/src/index.ts b/packages/opentelemetry-plugin-redis/src/index.ts index ae225f6b52..4595acb43a 100644 --- a/packages/opentelemetry-plugin-redis/src/index.ts +++ b/packages/opentelemetry-plugin-redis/src/index.ts @@ -13,3 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export * from './redis'; diff --git a/packages/opentelemetry-plugin-redis/src/redis.ts b/packages/opentelemetry-plugin-redis/src/redis.ts new file mode 100644 index 0000000000..5d4cb833f8 --- /dev/null +++ b/packages/opentelemetry-plugin-redis/src/redis.ts @@ -0,0 +1,101 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BasePlugin } from '@opentelemetry/core'; +import * as redisTypes from 'redis'; +import * as shimmer from 'shimmer'; +import { + getTracedCreateClient, + getTracedCreateStreamTrace, + getTracedInternalSendCommand, +} from './utils'; + +export class RedisPlugin extends BasePlugin { + static readonly COMPONENT = 'redis'; + readonly supportedVersions = ['^2.6.0']; // equivalent to >= 2.6.0 <3 + + constructor(readonly moduleName: string) { + super(); + } + + protected patch() { + if (this._moduleExports.RedisClient) { + this._logger.debug( + 'Patching redis.RedisClient.prototype.internal_send_command' + ); + shimmer.wrap( + this._moduleExports.RedisClient.prototype, + 'internal_send_command', + this._getPatchInternalSendCommand() + ); + + this._logger.debug('patching redis.create_stream'); + shimmer.wrap( + this._moduleExports.RedisClient.prototype, + 'create_stream', + this._getPatchCreateStream() + ); + + this._logger.debug('patching redis.createClient'); + shimmer.wrap( + this._moduleExports, + 'createClient', + this._getPatchCreateClient() + ); + } + return this._moduleExports; + } + + protected unpatch(): void { + if (this._moduleExports) { + shimmer.unwrap( + this._moduleExports.RedisClient.prototype, + 'internal_send_command' + ); + shimmer.unwrap( + this._moduleExports.RedisClient.prototype, + 'create_stream' + ); + shimmer.unwrap(this._moduleExports, 'createClient'); + } + } + + /** + * Patch internal_send_command(...) to trace requests + */ + private _getPatchInternalSendCommand() { + const tracer = this._tracer; + return function internal_send_command(original: Function) { + return getTracedInternalSendCommand(tracer, original); + }; + } + + private _getPatchCreateClient() { + const tracer = this._tracer; + return function createClient(original: Function) { + return getTracedCreateClient(tracer, original); + }; + } + + private _getPatchCreateStream() { + const tracer = this._tracer; + return function createReadStream(original: Function) { + return getTracedCreateStreamTrace(tracer, original); + }; + } +} + +export const plugin = new RedisPlugin(RedisPlugin.COMPONENT); diff --git a/packages/opentelemetry-plugin-redis/src/types.ts b/packages/opentelemetry-plugin-redis/src/types.ts new file mode 100644 index 0000000000..4014508f49 --- /dev/null +++ b/packages/opentelemetry-plugin-redis/src/types.ts @@ -0,0 +1,41 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as redisTypes from 'redis'; +import { EventEmitter } from 'events'; + +// exported from +// https://github.com/NodeRedis/node_redis/blob/master/lib/command.js +export interface RedisCommand { + command: string; + args: string[]; + buffer_args: boolean; + callback: redisTypes.Callback; + call_on_write: boolean; +} + +export interface RedisPluginClientTypes { + options?: { + host: string; + port: string; + }; + + address?: string; +} + +export interface RedisPluginStreamTypes { + stream?: { get(): EventEmitter; set(val: EventEmitter): void }; +} diff --git a/packages/opentelemetry-plugin-redis/src/utils.ts b/packages/opentelemetry-plugin-redis/src/utils.ts new file mode 100644 index 0000000000..0be83cf644 --- /dev/null +++ b/packages/opentelemetry-plugin-redis/src/utils.ts @@ -0,0 +1,126 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as redisTypes from 'redis'; +import { Tracer, SpanKind, Span, CanonicalCode } from '@opentelemetry/types'; +import { + RedisPluginStreamTypes, + RedisPluginClientTypes, + RedisCommand, +} from './types'; +import { EventEmitter } from 'events'; +import { RedisPlugin } from './redis'; +import { AttributeNames } from './enums'; + +const endSpan = (span: Span, err?: Error | null) => { + if (err) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: err.message, + }); + } else { + span.setStatus({ code: CanonicalCode.OK }); + } + span.end(); +}; + +export const getTracedCreateClient = (tracer: Tracer, original: Function) => { + return function createClientTrace(this: redisTypes.RedisClient) { + const client: redisTypes.RedisClient = original.apply(this, arguments); + return tracer.bind(client); + }; +}; + +export const getTracedCreateStreamTrace = ( + tracer: Tracer, + original: Function +) => { + return function create_stream_trace(this: RedisPluginStreamTypes) { + if (!this.stream) { + Object.defineProperty(this, 'stream', { + get() { + return this._patched_redis_stream; + }, + set(val: EventEmitter) { + tracer.bind(val); + this._patched_redis_stream = val; + }, + }); + } + return original.apply(this, arguments); + }; +}; + +export const getTracedInternalSendCommand = ( + tracer: Tracer, + original: Function +) => { + return function internal_send_command_trace( + this: redisTypes.RedisClient & RedisPluginClientTypes, + cmd?: RedisCommand + ) { + const parentSpan = tracer.getCurrentSpan(); + + // New versions of redis (2.4+) use a single options object + // instead of named arguments + if (arguments.length === 1 && typeof cmd === 'object') { + const span = tracer.startSpan(`${RedisPlugin.COMPONENT}-${cmd.command}`, { + kind: SpanKind.CLIENT, + parent: parentSpan || undefined, + attributes: { + [AttributeNames.COMPONENT]: RedisPlugin.COMPONENT, + [AttributeNames.DB_STATEMENT]: cmd.command, + }, + }); + + // Set attributes for not explicitly typed RedisPluginClientTypes + if (this.options) { + span.setAttributes({ + [AttributeNames.PEER_HOSTNAME]: this.options.host, + [AttributeNames.PEER_PORT]: this.options.port, + }); + } + if (this.address) { + span.setAttribute( + AttributeNames.PEER_ADDRESS, + `redis://${this.address}` + ); + } + + const originalCallback = arguments[0].callback; + if (originalCallback) { + (arguments[0] as RedisCommand).callback = function callback( + this: unknown, + err: Error | null, + _reply: T + ) { + endSpan(span, err); + return originalCallback.apply(this, arguments); + }; + } + try { + // Span will be ended in callback + return original.apply(this, arguments); + } catch (rethrow) { + endSpan(span, rethrow); + throw rethrow; // rethrow after ending span + } + } + + // We don't know how to trace this call, so don't start/stop a span + return original.apply(this, arguments); + }; +}; diff --git a/packages/opentelemetry-plugin-redis/test/assertionUtils.ts b/packages/opentelemetry-plugin-redis/test/assertionUtils.ts new file mode 100644 index 0000000000..fdb517c7a0 --- /dev/null +++ b/packages/opentelemetry-plugin-redis/test/assertionUtils.ts @@ -0,0 +1,76 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + SpanKind, + Attributes, + Event, + Span, + Status, +} from '@opentelemetry/types'; +import * as assert from 'assert'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import { + hrTimeToMilliseconds, + hrTimeToMicroseconds, +} from '@opentelemetry/core'; + +export const assertSpan = ( + span: ReadableSpan, + kind: SpanKind, + attributes: Attributes, + events: Event[], + status: Status +) => { + assert.strictEqual(span.spanContext.traceId.length, 32); + assert.strictEqual(span.spanContext.spanId.length, 16); + assert.strictEqual(span.kind, kind); + + assert.ok(span.endTime); + assert.strictEqual(span.links.length, 0); + + assert.ok( + hrTimeToMicroseconds(span.startTime) < hrTimeToMicroseconds(span.endTime) + ); + assert.ok(hrTimeToMilliseconds(span.endTime) > 0); + + // attributes + assert.deepStrictEqual(span.attributes, attributes); + + // events + assert.deepStrictEqual(span.events, events); + + assert.strictEqual(span.status.code, status.code); + if (status.message) { + assert.strictEqual(span.status.message, status.message); + } +}; + +// Check if childSpan was propagated from parentSpan +export const assertPropagation = ( + childSpan: ReadableSpan, + parentSpan: Span +) => { + const targetSpanContext = childSpan.spanContext; + const sourceSpanContext = parentSpan.context(); + assert.strictEqual(targetSpanContext.traceId, sourceSpanContext.traceId); + assert.strictEqual(childSpan.parentSpanId, sourceSpanContext.spanId); + assert.strictEqual( + targetSpanContext.traceFlags, + sourceSpanContext.traceFlags + ); + assert.notStrictEqual(targetSpanContext.spanId, sourceSpanContext.spanId); +}; diff --git a/packages/opentelemetry-plugin-redis/test/redis.test.ts b/packages/opentelemetry-plugin-redis/test/redis.test.ts new file mode 100644 index 0000000000..72d6730a69 --- /dev/null +++ b/packages/opentelemetry-plugin-redis/test/redis.test.ts @@ -0,0 +1,215 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as assert from 'assert'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { NodeTracer } from '@opentelemetry/node'; +import { plugin, RedisPlugin } from '../src'; +import * as redisTypes from 'redis'; +import { NoopLogger } from '@opentelemetry/core'; +import * as dockerUtils from './testUtils'; +import * as assertionUtils from './assertionUtils'; +import { SpanKind, Status, CanonicalCode } from '@opentelemetry/types'; +import { AttributeNames } from '../src/enums'; + +const memoryExporter = new InMemorySpanExporter(); + +const CONFIG = { + host: process.env.OPENTELEMETRY_REDIS_HOST || 'localhost', + port: process.env.OPENTELEMETRY_REDIS_PORT || '63790', +}; + +const URL = `redis://${CONFIG.host}:${CONFIG.port}`; + +const DEFAULT_ATTRIBUTES = { + [AttributeNames.COMPONENT]: RedisPlugin.COMPONENT, + [AttributeNames.PEER_HOSTNAME]: CONFIG.host, + [AttributeNames.PEER_PORT]: CONFIG.port, + [AttributeNames.PEER_ADDRESS]: URL, +}; + +const okStatus: Status = { + code: CanonicalCode.OK, +}; + +describe('redis@2.x', () => { + const tracer = new NodeTracer(); + let redis: typeof redisTypes; + const shouldTestLocal = process.env.RUN_REDIS_TESTS_LOCAL; + const shouldTest = process.env.RUN_REDIS_TESTS || shouldTestLocal; + + before(function() { + // needs to be "function" to have MochaContext "this" scope + if (!shouldTest) { + // this.skip() workaround + // https://github.com/mochajs/mocha/issues/2683#issuecomment-375629901 + this.test!.parent!.pending = true; + this.skip(); + } + + if (shouldTestLocal) { + dockerUtils.startDocker(); + } + + redis = require('redis'); + tracer.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + plugin.enable(redis, tracer, new NoopLogger()); + }); + + after(() => { + if (shouldTestLocal) { + dockerUtils.cleanUpDocker(); + } + }); + + it('should have correct module name', () => { + assert.strictEqual(plugin.moduleName, RedisPlugin.COMPONENT); + }); + + describe('#createClient()', () => { + it('should propagate the current span to event handlers', done => { + const span = tracer.startSpan('test span'); + let client: redisTypes.RedisClient; + const readyHandler = () => { + assert.strictEqual(tracer.getCurrentSpan(), span); + client.quit(done); + }; + const errorHandler = (err: Error) => { + assert.ifError(err); + client.quit(done); + }; + + tracer.withSpan(span, () => { + client = redis.createClient(URL); + client.on('ready', readyHandler); + client.on('error', errorHandler); + }); + }); + }); + + describe('#send_internal_message()', () => { + let client: redisTypes.RedisClient; + + const REDIS_OPERATIONS: Array<{ + description: string; + command: string; + method: (cb: redisTypes.Callback) => unknown; + }> = [ + { + description: 'insert', + command: 'hset', + method: (cb: redisTypes.Callback) => + client.hset('hash', 'random', 'random', cb), + }, + { + description: 'get', + command: 'get', + method: (cb: redisTypes.Callback) => client.get('test', cb), + }, + { + description: 'delete', + command: 'del', + method: (cb: redisTypes.Callback) => client.del('test', cb), + }, + ]; + + before(done => { + client = redis.createClient(URL); + client.on('error', err => { + done(err); + }); + client.on('ready', done); + }); + + beforeEach(done => { + client.set('test', 'data', () => { + memoryExporter.reset(); + done(); + }); + }); + + after(done => { + client.quit(done); + }); + + afterEach(done => { + client.del('hash', () => { + memoryExporter.reset(); + done(); + }); + }); + + describe('Instrumenting query operations', () => { + REDIS_OPERATIONS.forEach(operation => { + it(`should create a child span for ${operation.description}`, done => { + const attributes = { + ...DEFAULT_ATTRIBUTES, + [AttributeNames.DB_STATEMENT]: operation.command, + }; + const span = tracer.startSpan('test span'); + tracer.withSpan(span, () => { + operation.method((err, _result) => { + assert.ifError(err); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 1); + span.end(); + const endedSpans = memoryExporter.getFinishedSpans(); + assert.strictEqual(endedSpans.length, 2); + assert.strictEqual( + endedSpans[0].name, + `redis-${operation.command}` + ); + assertionUtils.assertSpan( + endedSpans[0], + SpanKind.CLIENT, + attributes, + [], + okStatus + ); + assertionUtils.assertPropagation(endedSpans[0], span); + done(); + }); + }); + }); + }); + }); + + describe('Removing instrumentation', () => { + before(() => { + plugin.disable(); + }); + + REDIS_OPERATIONS.forEach(operation => { + it(`should not create a child span for ${operation.description}`, done => { + const span = tracer.startSpan('test span'); + tracer.withSpan(span, () => { + operation.method((err, _) => { + assert.ifError(err); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); + span.end(); + const endedSpans = memoryExporter.getFinishedSpans(); + assert.strictEqual(endedSpans.length, 1); + assert.strictEqual(endedSpans[0], span); + done(); + }); + }); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-plugin-redis/test/testUtils.ts b/packages/opentelemetry-plugin-redis/test/testUtils.ts new file mode 100644 index 0000000000..a40b0b4aab --- /dev/null +++ b/packages/opentelemetry-plugin-redis/test/testUtils.ts @@ -0,0 +1,54 @@ +/*! + * Copyright 2019, OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as childProcess from 'child_process'; +export function startDocker() { + const tasks = [ + run('docker run -d -p 63790:6379 --name otjsredis redis:alpine'), + ]; + + for (let i = 0; i < tasks.length; i++) { + const task = tasks[i]; + if (task && task.code !== 0) { + console.error('Failed to start container!'); + console.error(task.output); + return false; + } + } + return true; +} + +export function cleanUpDocker() { + run('docker stop otjsredis'); + run('docker rm otjsredis'); +} + +function run(cmd: string) { + try { + const proc = childProcess.spawnSync(cmd, { + shell: true, + }); + return { + code: proc.status, + output: proc.output + .map(v => String.fromCharCode.apply(null, v as any)) + .join(''), + }; + } catch (e) { + console.log(e); + return; + } +}