From 631d9e4f59a6484460f9f429007a6c98040253e8 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Thu, 14 Nov 2019 11:19:48 -0800 Subject: [PATCH 1/9] chore: update prometheus exporter readme with usage and links (#521) * chore: update prometheus exporter readme with example and links * fix: minor change --- .../README.md | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/opentelemetry-exporter-prometheus/README.md b/packages/opentelemetry-exporter-prometheus/README.md index ef4b68d30f3..4ac8bc579bb 100644 --- a/packages/opentelemetry-exporter-prometheus/README.md +++ b/packages/opentelemetry-exporter-prometheus/README.md @@ -4,9 +4,58 @@ [![devDependencies][devDependencies-image]][devDependencies-url] [![Apache License][license-image]][license-image] -OpenTelemetry Exporter Prometheus provides a metrics endpoint for Prometheus. +The OpenTelemetry Prometheus Metrics Exporter allows the user to send collected [OpenTelemetry Metrics](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-metrics) to Prometheus. -**Work in progress** +[Prometheus](https://prometheus.io/) is a monitoring system that collects metrics, by scraping exposed endpoints at regular intervals, evaluating rule expressions. It can also trigger alerts if certain conditions are met. For assistance setting up Prometheus, [Click here](https://opencensus.io/codelabs/prometheus/#0) for a guided codelab. + +## Installation + +```bash +npm install --save @opentelemetry/metrics +npm install --save @opentelemetry/exporter-prometheus +``` + +## Usage + +Create & register the exporter on your application. + +```js +const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); +const { Meter } = require('@opentelemetry/metrics'); + +// Add your port and startServer to the Prometheus options +const options = {port: 9464, startServer: true}; +const exporter = new PrometheusExporter(options); + +// Register the exporter +const meter = new Meter(); +meter.addExporter(exporter); + +// Now, start recording data +const counter = meter.createCounter('metric_name'); +counter.add(10, meter.labels({ [key]: 'value' })); + +// Record data using Handle: It is recommended to keep a reference to the Handle instead of +// always calling `getHandle()` method for every operations. +const handle = counter.getHandle(meter.labels({ [key]: 'value' })); +handle.add(10); + +// .. some other work + +// Create and record Gauge +const gauge = meter.createGauge('metric_name1'); +gauge.set(10, meter.labels({ [key1]: 'value1' })); +``` + +## Viewing your metrics + +With the above you should now be able to navigate to the Prometheus UI at: http://localhost:9464/metrics + +## Useful links +- For more information on OpenTelemetry, visit: +- To learn more about Prometheus, visit: https://prometheus.io/ +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] ## License From 0c1ff8dec9aad4cb69232d85c6322cb78d5ca6dd Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Fri, 15 Nov 2019 07:37:03 -0800 Subject: [PATCH 2/9] chore: update README with plugins work and add DNS plugin in default supported plugins list (#529) --- README.md | 5 ++++- packages/opentelemetry-node/src/config.ts | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 95d5b443796..2a2a87238d1 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Maintainers ([@open-telemetry/js-maintainers](https://github.com/orgs/open-telem | [@opentelemetry/metrics](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-metrics) | This module provides instruments and meters for reporting of time series data. | | [@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node) | This module provides automatic tracing for Node.js applications. It is intended for use on the server only. | | [@opentelemetry/web](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-web) | This module provides automated instrumentation and tracing for Web applications. It is intended for use in the browser only. | +| [@opentelemetry/base](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-base) | This package provides base code for the SDK packages (tracing and metrics). | ### Exporters @@ -99,7 +100,7 @@ OpenTelemetry is vendor-agnostic and can upload data to any backend with various - [@opentelemetry/exporter-zipkin](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-exporter-zipkin) #### Metric Exporters -- [@opentelemetry/exporter-prometheus](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-exporter-prometheus) - WIP +- [@opentelemetry/exporter-prometheus](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-exporter-prometheus) ### Plugins @@ -112,6 +113,8 @@ OpenTelemetry can collect tracing data automatically using plugins. Vendors/User - [@opentelemetry/plugin-dns](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-dns) - [@opentelemetry/plugin-mongodb](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-mongodb) - WIP - [@opentelemetry/plugin-postgres](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-postgres) - WIP +- [@opentelemetry/plugin-redis](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-redis) - WIP +- [@opentelemetry/plugin-mysql](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-mysql) - WIP #### Web Plugins - [@opentelemetry/plugin-document-load](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-document-load) diff --git a/packages/opentelemetry-node/src/config.ts b/packages/opentelemetry-node/src/config.ts index 10fe664d424..7d0eb09175a 100644 --- a/packages/opentelemetry-node/src/config.ts +++ b/packages/opentelemetry-node/src/config.ts @@ -39,4 +39,8 @@ export const DEFAULT_INSTRUMENTATION_PLUGINS: Plugins = { enabled: true, path: '@opentelemetry/plugin-https', }, + dns: { + enabled: true, + path: '@opentelemetry/plugin-dns', + }, }; From d80cf562053b194deafa369b215e21eb47e2d456 Mon Sep 17 00:00:00 2001 From: Valentin Marchaud Date: Fri, 15 Nov 2019 20:37:48 +0100 Subject: [PATCH 3/9] feat: add mongodb plugin (#205) * feat: add mongodb plugin * test(plugin-mongodb): add tests for mongodb plugin * chore(review): address PRs comments / add attributes on each span * chore: rename mongodb plugin to mongodb-core * chore(review): fix CI tests + use enum for attributes names * chore: address PR comments --- .circleci/config.yml | 12 +- .../.npmignore | 0 .../LICENSE | 0 .../README.md | 16 +- .../package.json | 19 +- .../src/index.ts | 2 + .../src/mongodb.ts | 251 ++++++++++++++++ .../src/types.ts | 59 ++++ .../test/mongodb.test.ts | 284 ++++++++++++++++++ .../tsconfig.json | 3 +- .../tslint.json | 0 11 files changed, 628 insertions(+), 18 deletions(-) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/.npmignore (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/LICENSE (100%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/README.md (77%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/package.json (72%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/src/index.ts (95%) create mode 100644 packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts create mode 100644 packages/opentelemetry-plugin-mongodb-core/src/types.ts create mode 100644 packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/tsconfig.json (76%) rename packages/{opentelemetry-plugin-mongodb => opentelemetry-plugin-mongodb-core}/tslint.json (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2e3d019f2e8..95d4c61fa23 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,10 +2,14 @@ version: 2 test_env: &test_env RUN_POSTGRES_TESTS: 1 + RUN_MONGODB_TESTS: 1 POSTGRES_USER: postgres POSTGRES_DB: circle_database POSTGRES_HOST: localhost POSTGRES_PORT: 5432 + MONGODB_HOST: localhost + MONGODB_PORT: 27017 + MONGODB_DB: opentelemetry-tests postgres_service: &postgres_service image: circleci/postgres:9.6-alpine @@ -13,6 +17,9 @@ postgres_service: &postgres_service POSTGRES_USER: postgres POSTGRES_DB: circle_database +mongo_service: &mongo_service + image: mongo + cache_1: &cache_1 key: npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-1 paths: @@ -39,7 +46,7 @@ cache_2: &cache_2 - packages/opentelemetry-plugin-grpc/node_modules - packages/opentelemetry-plugin-http/node_modules - packages/opentelemetry-plugin-http2/node_modules - - packages/opentelemetry-plugin-mongodb/node_modules + - packages/opentelemetry-plugin-mongodb-core/node_modules - packages/opentelemetry-plugin-redis/node_modules - packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/node_modules - packages/opentelemetry-plugin-document-load/node_modules @@ -145,18 +152,21 @@ jobs: - image: node:8 environment: *test_env - *postgres_service + - *mongo_service <<: *node_unit_tests node10: docker: - image: node:10 environment: *test_env - *postgres_service + - *mongo_service <<: *node_unit_tests node12: docker: - image: node:12 environment: *test_env - *postgres_service + - *mongo_service <<: *node_unit_tests node12-browsers: docker: diff --git a/packages/opentelemetry-plugin-mongodb/.npmignore b/packages/opentelemetry-plugin-mongodb-core/.npmignore similarity index 100% rename from packages/opentelemetry-plugin-mongodb/.npmignore rename to packages/opentelemetry-plugin-mongodb-core/.npmignore diff --git a/packages/opentelemetry-plugin-mongodb/LICENSE b/packages/opentelemetry-plugin-mongodb-core/LICENSE similarity index 100% rename from packages/opentelemetry-plugin-mongodb/LICENSE rename to packages/opentelemetry-plugin-mongodb-core/LICENSE diff --git a/packages/opentelemetry-plugin-mongodb/README.md b/packages/opentelemetry-plugin-mongodb-core/README.md similarity index 77% rename from packages/opentelemetry-plugin-mongodb/README.md rename to packages/opentelemetry-plugin-mongodb-core/README.md index 6c05d1b0da1..1c6ed4aac60 100644 --- a/packages/opentelemetry-plugin-mongodb/README.md +++ b/packages/opentelemetry-plugin-mongodb-core/README.md @@ -1,10 +1,10 @@ -# OpenTelemetry mongodb Instrumentation for Node.js +# OpenTelemetry mongodb-core Instrumentation for Node.js [![Gitter chat][gitter-image]][gitter-url] [![dependencies][dependencies-image]][dependencies-url] [![devDependencies][devDependencies-image]][devDependencies-url] [![Apache License][license-image]][license-image] -This module provides automatic instrumentation for [`mongodb`](https://github.com/mongodb/node-mongodb-native). +This module provides automatic instrumentation for [`mongodb-core`](https://github.com/mongodb-js/mongodb-core). For automatic instrumentation see the [@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node) package. @@ -12,13 +12,13 @@ For automatic instrumentation see the ## Installation ```bash -npm install --save @opentelemetry/plugin-mongodb +npm install --save @opentelemetry/plugin-mongodb-core ``` ## Usage ```js -const opentelemetry = require('@opentelemetry/plugin-mongodb'); +const opentelemetry = require('@opentelemetry/plugin-mongodb-core'); // TODO: DEMONSTRATE API ``` @@ -36,7 +36,7 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge [license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE [license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat -[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-plugin-mongodb -[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-mongodb -[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-plugin-mongodb -[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-mongodb&type=dev +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-plugin-mongodb-core +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-mongodb-core +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-plugin-mongodb-core +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-mongodb-core&type=dev diff --git a/packages/opentelemetry-plugin-mongodb/package.json b/packages/opentelemetry-plugin-mongodb-core/package.json similarity index 72% rename from packages/opentelemetry-plugin-mongodb/package.json rename to packages/opentelemetry-plugin-mongodb-core/package.json index 6c8b02219d7..f54d725addb 100644 --- a/packages/opentelemetry-plugin-mongodb/package.json +++ b/packages/opentelemetry-plugin-mongodb-core/package.json @@ -1,7 +1,7 @@ { - "name": "@opentelemetry/plugin-mongodb", + "name": "@opentelemetry/plugin-mongodb-core", "version": "0.2.0", - "description": "OpenTelemetry mongodb automatic instrumentation package.", + "description": "OpenTelemetry mongodb-core automatic instrumentation package.", "private": true, "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -18,7 +18,7 @@ }, "keywords": [ "opentelemetry", - "mongodb", + "mongodb-core", "nodejs", "tracing", "profiling", @@ -40,22 +40,27 @@ "access": "public" }, "devDependencies": { + "@opentelemetry/node": "^0.1.1", + "@opentelemetry/tracing": "^0.1.1", "@types/mocha": "^5.2.7", - "@types/node": "^12.6.9", + "@types/mongodb": "^3.2.3", + "@types/node": "^12.7.2", + "@types/shimmer": "^1.0.1", "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", + "mongodb": "^3.3.0", "nyc": "^14.1.1", "rimraf": "^3.0.0", "ts-mocha": "^6.0.0", "ts-node": "^8.3.0", - "tslint-consistent-codestyle": "^1.15.1", + "tslint-consistent-codestyle": "^1.16.0", "tslint-microsoft-contrib": "^6.2.0", "typescript": "3.7.2" }, "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-mongodb/src/index.ts b/packages/opentelemetry-plugin-mongodb-core/src/index.ts similarity index 95% rename from packages/opentelemetry-plugin-mongodb/src/index.ts rename to packages/opentelemetry-plugin-mongodb-core/src/index.ts index ae225f6b521..21a9d57333a 100644 --- a/packages/opentelemetry-plugin-mongodb/src/index.ts +++ b/packages/opentelemetry-plugin-mongodb-core/src/index.ts @@ -13,3 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export * from './mongodb'; diff --git a/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts b/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts new file mode 100644 index 00000000000..e3e179c7b20 --- /dev/null +++ b/packages/opentelemetry-plugin-mongodb-core/src/mongodb.ts @@ -0,0 +1,251 @@ +/*! + * 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. + */ + +// mongodb.Server type is deprecated so every use trigger a lint error +/* tslint:disable:deprecation */ + +import { BasePlugin } from '@opentelemetry/core'; +import { Span, SpanKind, CanonicalCode } from '@opentelemetry/types'; +import { + Func, + MongoInternalCommand, + MongoInternalTopology, + AttributeNames, + MongodbCommandType, +} from './types'; +import * as mongodb from 'mongodb'; +import * as shimmer from 'shimmer'; + +/** MongoDBCore instrumentation plugin for OpenTelemetry */ +export class MongoDBCorePlugin extends BasePlugin { + private readonly _SERVER_METHODS = ['insert', 'update', 'remove', 'command']; + private readonly _CURSOR_METHODS = ['_next', 'next']; + + private readonly _COMPONENT = 'mongodb-core'; + private readonly _DB_TYPE = 'mongodb'; + + readonly supportedVersions = ['>=2 <3']; + + constructor(readonly moduleName: string) { + super(); + } + + /** + * Patches MongoDB operations. + */ + protected patch() { + this._logger.debug('Patched MongoDB'); + + if (this._moduleExports.Server) { + for (const fn of this._SERVER_METHODS) { + this._logger.debug(`patching mongodb-core.Server.prototype.${fn}`); + shimmer.wrap( + this._moduleExports.Server.prototype, + // Forced to ignore due to incomplete typings + // tslint:disable-next-line:ban-ts-ignore + // @ts-ignore + fn, + this._getPatchCommand(fn) + ); + } + } + + if (this._moduleExports.Cursor) { + this._logger.debug( + 'patching mongodb-core.Cursor.prototype functions:', + this._CURSOR_METHODS + ); + shimmer.massWrap( + [this._moduleExports.Cursor.prototype], + this._CURSOR_METHODS as never[], + // tslint:disable-next-line:no-any + this._getPatchCursor() as any + ); + } + + return this._moduleExports; + } + + /** Unpatches all MongoDB patched functions. */ + unpatch(): void { + shimmer.massUnwrap( + [this._moduleExports.Server.prototype], + this._SERVER_METHODS as never[] + ); + shimmer.massUnwrap( + [this._moduleExports.Cursor.prototype], + this._CURSOR_METHODS as never[] + ); + } + + /** Creates spans for Command operations */ + private _getPatchCommand(operationName: string) { + const plugin = this; + return (original: Func) => { + return function patchedServerCommand( + this: mongodb.Server, + ns: string, + commands: MongoInternalCommand[] | MongoInternalCommand, + options: {} | Function, + callback: Function + ): mongodb.Server { + const currentSpan = plugin._tracer.getCurrentSpan(); + const resultHandler = + typeof options === 'function' ? options : callback; + if ( + currentSpan === null || + typeof resultHandler !== 'function' || + typeof commands !== 'object' + ) { + return original.apply(this, (arguments as unknown) as unknown[]); + } + const command = commands instanceof Array ? commands[0] : commands; + const commandType = plugin._getCommandType(command); + const type = + commandType === MongodbCommandType.UNKNOWN + ? operationName + : commandType; + const span = plugin._tracer.startSpan(`mongodb.${type}`, { + parent: currentSpan, + kind: SpanKind.CLIENT, + }); + plugin._populateAttributes( + span, + ns, + command, + this as MongoInternalTopology + ); + return original.call( + this, + ns, + commands, + plugin._patchEnd(span, resultHandler) + ); + }; + }; + } + + /** + * Get the mongodb command type from the object. + * @param command Internal mongodb command object + * @param defaulType the default type to return if we could not find a + * specific command. + */ + private _getCommandType(command: MongoInternalCommand): MongodbCommandType { + if (command.createIndexes !== undefined) { + return MongodbCommandType.CREATE_INDEXES; + } else if (command.findandmodify !== undefined) { + return MongodbCommandType.FIND_AND_MODIFY; + } else if (command.ismaster !== undefined) { + return MongodbCommandType.IS_MASTER; + } else if (command.count !== undefined) { + return MongodbCommandType.COUNT; + } else { + return MongodbCommandType.UNKNOWN; + } + } + + /** + * Populate span's attributes by fetching related metadata from the context + * @param span span to add attributes to + * @param ns mongodb namespace + * @param command mongodb internal representation of a command + * @param topology mongodb internal representation of the network topology + */ + private _populateAttributes( + span: Span, + ns: string, + command: MongoInternalCommand, + topology: MongoInternalTopology + ) { + // add network attributes to determine the remote server + if (topology && topology.s && topology.s.options) { + span.setAttributes({ + [AttributeNames.PEER_HOSTNAME]: `${topology.s.options.host}`, + [AttributeNames.PEER_PORT]: `${topology.s.options.port}`, + }); + } + // add database related attributes + span.setAttributes({ + [AttributeNames.DB_INSTANCE]: `${ns}`, + [AttributeNames.DB_TYPE]: this._DB_TYPE, + [AttributeNames.COMPONENT]: this._COMPONENT, + }); + + if (command === undefined) return; + const query = Object.keys(command.query || command.q || {}).reduce( + (obj, key) => { + obj[key] = '?'; + return obj; + }, + {} as { [key: string]: string } + ); + span.setAttribute('db.statement', JSON.stringify(query)); + } + + /** Creates spans for Cursor operations */ + private _getPatchCursor() { + const plugin = this; + return (original: Func) => { + return function patchedCursorCommand( + this: { + ns: string; + cmd: MongoInternalCommand; + topology: MongoInternalTopology; + }, + ...args: unknown[] + ): mongodb.Cursor { + const currentSpan = plugin._tracer.getCurrentSpan(); + const resultHandler = args[0]; + if (currentSpan === null || typeof resultHandler !== 'function') { + return original.apply(this, args); + } + const span = plugin._tracer.startSpan(`mongodb.query`, { + parent: currentSpan, + kind: SpanKind.CLIENT, + }); + plugin._populateAttributes(span, this.ns, this.cmd, this.topology); + + return original.call(this, plugin._patchEnd(span, resultHandler)); + }; + }; + } + + /** + * Ends a created span. + * @param span The created span to end. + * @param resultHandler A callback function. + */ + private _patchEnd(span: Span, resultHandler: Function): Function { + return function patchedEnd(this: {}, ...args: unknown[]) { + const error = args[0]; + if (error instanceof Error) { + span.setStatus({ + code: CanonicalCode.UNKNOWN, + message: error.message, + }); + } else { + span.setStatus({ + code: CanonicalCode.OK, + }); + } + span.end(); + return resultHandler.apply(this, args); + }; + } +} + +export const plugin = new MongoDBCorePlugin('mongodb-core'); diff --git a/packages/opentelemetry-plugin-mongodb-core/src/types.ts b/packages/opentelemetry-plugin-mongodb-core/src/types.ts new file mode 100644 index 00000000000..3c8b33b542b --- /dev/null +++ b/packages/opentelemetry-plugin-mongodb-core/src/types.ts @@ -0,0 +1,59 @@ +/*! + * 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 type Func = (...args: unknown[]) => T; +export type MongoInternalCommand = { + findandmodify: boolean; + createIndexes: boolean; + count: boolean; + ismaster: boolean; + query?: { [key: string]: unknown }; + q?: { [key: string]: unknown }; +}; +// +// https://github.com/mongodb-js/mongodb-core/blob/master/lib/topologies/server.js#L117 +export type MongoInternalTopology = { + s?: { + options?: { + host?: string; + port?: number; + servername?: string; + }; + }; +}; + +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', + + PEER_PORT = 'peer.port', + PEER_IPV4 = 'peer.ipv4', + PEER_IPV6 = 'peer.ipv6', + PEER_SERVICE = 'peer.service', +} + +export enum MongodbCommandType { + CREATE_INDEXES = 'createIndexes', + FIND_AND_MODIFY = 'findAndModify', + IS_MASTER = 'isMaster', + COUNT = 'count', + UNKNOWN = 'unknown', +} diff --git a/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts b/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts new file mode 100644 index 00000000000..b0f4ee15fcd --- /dev/null +++ b/packages/opentelemetry-plugin-mongodb-core/test/mongodb.test.ts @@ -0,0 +1,284 @@ +/*! + * 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 { NodeTracer } from '@opentelemetry/node'; +import * as assert from 'assert'; +import * as mongodb from 'mongodb'; +import { plugin } from '../src'; +import { SpanKind, CanonicalCode } from '@opentelemetry/types'; +import { NoopLogger } from '@opentelemetry/core'; +import { AttributeNames } from '../src/types'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, + ReadableSpan, +} from '@opentelemetry/tracing'; + +interface MongoDBAccess { + client: mongodb.MongoClient; + collection: mongodb.Collection; +} + +/** + * Access the mongodb collection. + * @param url The mongodb URL to access. + * @param dbName The mongodb database name. + * @param collectionName The mongodb collection name. + */ +function accessCollection( + url: string, + dbName: string, + collectionName: string +): Promise { + return new Promise((resolve, reject) => { + mongodb.MongoClient.connect(url, function connectedClient(err, client) { + if (err) { + reject(err); + return; + } + const db = client.db(dbName); + const collection = db.collection(collectionName); + resolve({ client, collection }); + }); + }); +} + +/** + * Asserts root spans attributes. + * @param spans Readable spans that we need to asert. + * @param expectedName The expected name of the first root span. + * @param expectedKind The expected kind of the first root span. + */ +function assertSpans( + spans: ReadableSpan[], + expectedName: string, + expectedKind: SpanKind +) { + assert.strictEqual(spans.length, 2); + spans.forEach(span => { + assert(span.endTime instanceof Array); + assert(span.endTime.length === 2); + }); + const [mongoSpan] = spans; + assert.strictEqual(mongoSpan.name, expectedName); + assert.strictEqual(mongoSpan.kind, expectedKind); + assert.strictEqual( + mongoSpan.attributes[AttributeNames.COMPONENT], + 'mongodb-core' + ); + assert.strictEqual( + mongoSpan.attributes[AttributeNames.PEER_HOSTNAME], + process.env.MONGODB_HOST || 'localhost' + ); + assert.strictEqual(mongoSpan.status.code, CanonicalCode.OK); +} + +describe('MongoDBPlugin', () => { + // 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; + let shouldTest = true; + if (!RUN_MONGODB_TESTS) { + console.log('Skipping test-mongodb. Run MongoDB to test'); + shouldTest = false; + } + + const URL = `mongodb://${process.env.MONGODB_HOST || 'localhost'}:${process + .env.MONGODB_PORT || '27017'}`; + const DB_NAME = process.env.MONGODB_DB || 'opentelemetry-tests'; + const COLLECTION_NAME = 'test'; + + let client: mongodb.MongoClient; + let collection: mongodb.Collection; + const logger = new NoopLogger(); + const tracer = new NodeTracer(); + const memoryExporter = new InMemorySpanExporter(); + tracer.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + + before(done => { + plugin.enable(mongodb, tracer, logger); + accessCollection(URL, DB_NAME, COLLECTION_NAME) + .then(result => { + client = result.client; + collection = result.collection; + done(); + }) + .catch((err: Error) => { + console.log( + 'Skipping test-mongodb. Could not connect. Run MongoDB to test' + ); + shouldTest = false; + done(); + }); + }); + + beforeEach(function mongoBeforeEach(done) { + // Skiping all tests in beforeEach() is a workarround. Mocha does not work + // properly when skiping tests in before() on nested describe() calls. + // https://github.com/mochajs/mocha/issues/2819 + if (!shouldTest) { + this.skip(); + } + memoryExporter.reset(); + // Non traced insertion of basic data to perform tests + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + collection.insertMany(insertData, (err, result) => { + done(); + }); + }); + + afterEach(done => { + collection.deleteOne({}, done); + }); + + after(() => { + if (client) { + client.close(); + } + }); + + /** Should intercept query */ + describe('Instrumenting query operations', () => { + it('should create a child span for insert', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + + const span = tracer.startSpan(`insertRootSpan`); + tracer.withSpan(span, () => { + collection.insertMany(insertData, (err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.insert`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + + it('should create a child span for update', done => { + const span = tracer.startSpan('updateRootSpan'); + tracer.withSpan(span, () => { + collection.updateOne({ a: 2 }, { $set: { b: 1 } }, (err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.update`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + + it('should create a child span for remove', done => { + const span = tracer.startSpan('removeRootSpan'); + tracer.withSpan(span, () => { + collection.deleteOne({ a: 3 }, (err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.remove`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + }); + + /** Should intercept cursor */ + describe('Instrumenting cursor operations', () => { + it('should create a child span for find', done => { + const span = tracer.startSpan('findRootSpan'); + tracer.withSpan(span, () => { + collection.find({}).toArray((err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.query`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + }); + + /** Should intercept command */ + describe('Instrumenting command operations', () => { + it('should create a child span for create index', done => { + const span = tracer.startSpan('indexRootSpan'); + tracer.withSpan(span, () => { + collection.createIndex({ a: 1 }, (err, result) => { + span.end(); + assert.ifError(err); + assertSpans( + memoryExporter.getFinishedSpans(), + `mongodb.createIndexes`, + SpanKind.CLIENT + ); + done(); + }); + }); + }); + }); + + /** Should intercept command */ + describe('Removing Instrumentation', () => { + it('should unpatch plugin', () => { + assert.doesNotThrow(() => { + plugin.unpatch(); + }); + }); + + it('should not create a child span for query', done => { + const insertData = [{ a: 1 }, { a: 2 }, { a: 3 }]; + + const span = tracer.startSpan('insertRootSpan'); + collection.insertMany(insertData, (err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 1); + done(); + }); + }); + + it('should not create a child span for cursor', done => { + const span = tracer.startSpan('findRootSpan'); + collection.find({}).toArray((err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 1); + done(); + }); + }); + + it('should not create a child span for command', done => { + const span = tracer.startSpan('indexRootSpan'); + collection.createIndex({ a: 1 }, (err, result) => { + span.end(); + assert.ifError(err); + assert.strictEqual(memoryExporter.getFinishedSpans().length, 1); + done(); + }); + }); + }); +}); diff --git a/packages/opentelemetry-plugin-mongodb/tsconfig.json b/packages/opentelemetry-plugin-mongodb-core/tsconfig.json similarity index 76% rename from packages/opentelemetry-plugin-mongodb/tsconfig.json rename to packages/opentelemetry-plugin-mongodb-core/tsconfig.json index a2042cd68b1..3e83278f6c2 100644 --- a/packages/opentelemetry-plugin-mongodb/tsconfig.json +++ b/packages/opentelemetry-plugin-mongodb-core/tsconfig.json @@ -5,7 +5,6 @@ "outDir": "build" }, "include": [ - "src/**/*.ts", - "test/**/*.ts" + "src/**/*.ts" ] } diff --git a/packages/opentelemetry-plugin-mongodb/tslint.json b/packages/opentelemetry-plugin-mongodb-core/tslint.json similarity index 100% rename from packages/opentelemetry-plugin-mongodb/tslint.json rename to packages/opentelemetry-plugin-mongodb-core/tslint.json From b57951cbe0b657b5b751fc48e640f669a73d18ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20St=C3=B6bich?= <18708370+Flarna@users.noreply.github.com> Date: Tue, 19 Nov 2019 01:17:57 +0100 Subject: [PATCH 4/9] fix(plugin-http): adapt to current @types/node (#548) --- packages/opentelemetry-plugin-http/package.json | 2 +- packages/opentelemetry-plugin-http/test/utils/assertSpan.ts | 2 +- packages/opentelemetry-plugin-https/package.json | 2 +- packages/opentelemetry-plugin-https/test/utils/assertSpan.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/opentelemetry-plugin-http/package.json b/packages/opentelemetry-plugin-http/package.json index 59cefd55bbc..94a5baa1144 100644 --- a/packages/opentelemetry-plugin-http/package.json +++ b/packages/opentelemetry-plugin-http/package.json @@ -46,7 +46,7 @@ "@types/got": "^9.6.7", "@types/mocha": "^5.2.7", "@types/nock": "^11.1.0", - "@types/node": "^12.7.9", + "@types/node": "^12.12.9", "@types/request-promise-native": "^1.0.17", "@types/semver": "^6.0.2", "@types/shimmer": "^1.0.1", diff --git a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts index 9a222f48f49..eccf35bc7a8 100644 --- a/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts +++ b/packages/opentelemetry-plugin-http/test/utils/assertSpan.ts @@ -33,7 +33,7 @@ export const assertSpan = ( hostname: string; pathname: string; reqHeaders?: http.OutgoingHttpHeaders; - path?: string; + path?: string | null; forceStatus?: Status; component: string; } diff --git a/packages/opentelemetry-plugin-https/package.json b/packages/opentelemetry-plugin-https/package.json index 09387444e37..8d3a19264f5 100644 --- a/packages/opentelemetry-plugin-https/package.json +++ b/packages/opentelemetry-plugin-https/package.json @@ -45,7 +45,7 @@ "@types/got": "^9.6.7", "@types/mocha": "^5.2.7", "@types/nock": "^11.1.0", - "@types/node": "^12.7.8", + "@types/node": "^12.12.9", "@types/request-promise-native": "^1.0.17", "@types/semver": "^6.0.2", "@types/shimmer": "^1.0.1", diff --git a/packages/opentelemetry-plugin-https/test/utils/assertSpan.ts b/packages/opentelemetry-plugin-https/test/utils/assertSpan.ts index f89aa5b5b63..639a16d989b 100644 --- a/packages/opentelemetry-plugin-https/test/utils/assertSpan.ts +++ b/packages/opentelemetry-plugin-https/test/utils/assertSpan.ts @@ -35,7 +35,7 @@ export const assertSpan = ( hostname: string; pathname: string; reqHeaders?: http.OutgoingHttpHeaders; - path?: string; + path?: string | null; component: string; } ) => { From 41fd196f5c3176d271e3a1b3b51c04a22d1976fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20St=C3=B6bich?= <18708370+Flarna@users.noreply.github.com> Date: Tue, 19 Nov 2019 03:23:53 +0100 Subject: [PATCH 5/9] fix: zipkin-exporter: don't export after shutdown (#526) * fix: zipkin-exporter: don't export after shutdown According to spec and exporter should return FailedNotRetryable error after shutdown was called. * chore: use setTimeout instead setImmediate for browsers * chore: rename shutdown to isShutdown --- .../opentelemetry-exporter-zipkin/src/zipkin.ts | 10 ++++++++++ .../test/zipkin.test.ts | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/opentelemetry-exporter-zipkin/src/zipkin.ts b/packages/opentelemetry-exporter-zipkin/src/zipkin.ts index fdad1f2ef13..2be9f91fcf0 100644 --- a/packages/opentelemetry-exporter-zipkin/src/zipkin.ts +++ b/packages/opentelemetry-exporter-zipkin/src/zipkin.ts @@ -39,6 +39,7 @@ export class ZipkinExporter implements SpanExporter { private readonly _statusCodeTagName: string; private readonly _statusDescriptionTagName: string; private readonly _reqOpts: http.RequestOptions; + private _isShutdown: boolean; constructor(config: zipkinTypes.ExporterConfig) { const urlStr = config.url || ZipkinExporter.DEFAULT_URL; @@ -60,6 +61,7 @@ export class ZipkinExporter implements SpanExporter { this._statusCodeTagName = config.statusCodeTagName || statusCodeTagName; this._statusDescriptionTagName = config.statusDescriptionTagName || statusDescriptionTagName; + this._isShutdown = false; } /** @@ -70,6 +72,10 @@ export class ZipkinExporter implements SpanExporter { resultCallback: (result: ExportResult) => void ) { this._logger.debug('Zipkin exporter export'); + if (this._isShutdown) { + setTimeout(() => resultCallback(ExportResult.FAILED_NOT_RETRYABLE)); + return; + } return this._sendSpans(spans, resultCallback); } @@ -78,6 +84,10 @@ export class ZipkinExporter implements SpanExporter { */ shutdown() { this._logger.debug('Zipkin exporter shutdown'); + if (this._isShutdown) { + return; + } + this._isShutdown = true; // Make an optimistic flush. if (this._forceFlush) { // @todo get spans from span processor (batch) diff --git a/packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts b/packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts index dfc1cf08e93..2a09e37230c 100644 --- a/packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts +++ b/packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts @@ -311,6 +311,20 @@ describe('ZipkinExporter', () => { assert.strictEqual(result, ExportResult.FAILED_RETRYABLE); }); }); + + it('should return FailedNonRetryable after shutdown', done => { + const exporter = new ZipkinExporter({ + serviceName: 'my-service', + logger: new NoopLogger(), + }); + + exporter.shutdown(); + + exporter.export([getReadableSpan()], (result: ExportResult) => { + assert.strictEqual(result, ExportResult.FAILED_NOT_RETRYABLE); + done(); + }); + }); }); describe('shutdown', () => { From 724f9cc45b2e174fc5d56778ad4b40e5d6793430 Mon Sep 17 00:00:00 2001 From: Yuri Shkuro Date: Mon, 18 Nov 2019 21:20:07 -0800 Subject: [PATCH 6/9] Remove myself as approver (#547) I haven't been following this project closely. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 2a2a87238d1..ebca1e3cea3 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ We have a weekly SIG meeting! See the [community page](https://github.com/open-t Approvers ([@open-telemetry/js-approvers](https://github.com/orgs/open-telemetry/teams/javascript-approvers)): -- [Yuri Shkuro](https://github.com/yurishkuro), Uber - [Roch Devost](https://github.com/rochdev), DataDog - [Brandon Gonzalez](https://github.com/bg451), LightStep - [Olivier Albertini](https://github.com/OlivierAlbertini), VilledeMontreal From fa583fde07425f0aca75d3902d13e5a7d9a5dc65 Mon Sep 17 00:00:00 2001 From: Mark Wolff Date: Mon, 18 Nov 2019 23:18:50 -0800 Subject: [PATCH 7/9] 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 95d4c61fa23..ab5b0ddceb9 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 3509b21de35..266435a421d 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 baa86c0f4f5..07ea3b3e482 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 00000000000..035287a14bb --- /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 ae225f6b521..4595acb43a2 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 00000000000..5d4cb833f89 --- /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 00000000000..4014508f49a --- /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 00000000000..0be83cf6446 --- /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 00000000000..fdb517c7a09 --- /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 00000000000..72d6730a69a --- /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 00000000000..a40b0b4aab0 --- /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; + } +} From f48de819d19d9d6b12cca75aaf2bc1b79ac8db00 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Wed, 13 Nov 2019 17:50:53 -0800 Subject: [PATCH 8/9] docs: add multi exporter example --- examples/basic-tracer-node/multi_exporter.js | 55 ++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 examples/basic-tracer-node/multi_exporter.js diff --git a/examples/basic-tracer-node/multi_exporter.js b/examples/basic-tracer-node/multi_exporter.js new file mode 100644 index 00000000000..dc4c85474b8 --- /dev/null +++ b/examples/basic-tracer-node/multi_exporter.js @@ -0,0 +1,55 @@ +const opentelemetry = require('@opentelemetry/core'); +const { BasicTracer, BatchSpanProcessor, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); +const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); + +const tracer = new BasicTracer(); + +const zipkinExporter = new ZipkinExporter({serviceName: 'basic-service'}); +const jaegerExporter = new JaegerExporter({ + serviceName: 'basic-service', + // The default flush interval is 5 seconds. + flushInterval: 2000 +}) + +// It is recommended to use this BatchSpanProcessor for better performance +// and optimization, especially in production. +tracer.addSpanProcessor(new BatchSpanProcessor(zipkinExporter, { + bufferSize: 10 // This is added for example, default size is 100. +})); + +// It is recommended to use SimpleSpanProcessor in case of Jaeger exporter as +// it's internal client already handles the spans with batching logic. +tracer.addSpanProcessor(new SimpleSpanProcessor(jaegerExporter)); + +// Initialize the OpenTelemetry APIs to use the BasicTracer bindings +opentelemetry.initGlobalTracer(tracer); + +// Create a span. A span must be closed. +const span = opentelemetry.getTracer().startSpan('main'); +for (let i = 0; i < 10; i++) { + doWork(span); +} +// Be sure to end the span. +span.end(); + +// flush and close the connection. +zipkinExporter.shutdown(); +jaegerExporter.shutdown(); + +function doWork(parent) { + // Start another span. In this example, the main method already started a + // span, so that'll be the parent span, and this will be a child span. + const span = opentelemetry.getTracer().startSpan('doWork', { + parent: parent + }); + + // simulate some random work. + for (let i = 0; i <= Math.floor(Math.random() * 40000000); i++) { } + + // Set attributes to the span. + span.setAttribute('key', 'value'); + + // Annotate our span to capture metadata about our operation + span.addEvent('invoking doWork').end(); +} From fa36e9c2e5b53d8380fee47178da5e70d4e96af8 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Wed, 13 Nov 2019 18:03:04 -0800 Subject: [PATCH 9/9] chore: add script and update readme --- examples/basic-tracer-node/README.md | 11 +++++++++++ examples/basic-tracer-node/package.json | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/examples/basic-tracer-node/README.md b/examples/basic-tracer-node/README.md index 8df783472eb..d7e0feb9eac 100644 --- a/examples/basic-tracer-node/README.md +++ b/examples/basic-tracer-node/README.md @@ -57,6 +57,17 @@ Click on the trace to view its details.

+### Export to multiple exporters + + - Run the sample + + ```sh + $ # from this directory + $ npm run multi_exporter + ``` + + This will export the spans data simultaneously on `Zipkin` and `Jaeger` backend. This is handy if transitioning from one vendor/OSS project to another for the tracing backend. You might want to export to both during the transitional phase. + ## Useful links - For more information on OpenTelemetry, visit: - For more information on tracing, visit: diff --git a/examples/basic-tracer-node/package.json b/examples/basic-tracer-node/package.json index a8b0ec329e1..77417ab385e 100644 --- a/examples/basic-tracer-node/package.json +++ b/examples/basic-tracer-node/package.json @@ -6,7 +6,8 @@ "main": "index.js", "scripts": { "zipkin:basic": "cross-env EXPORTER=zipkin node ./index.js", - "jaeger:basic": "cross-env EXPORTER=jaeger node ./index.js" + "jaeger:basic": "cross-env EXPORTER=jaeger node ./index.js", + "multi_exporter": "node ./multi_exporter.js" }, "repository": { "type": "git",