diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59aac5e6d1..cfd36d4f4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,3 +74,26 @@ The `opentelemetry-js-contrib` project is written in TypeScript. When two or more approaches must be compared, please write a benchmark in the benchmark/index.js module so that we can keep track of the most efficient algorithm. - `npm run bench` to run your benchmark. + +## Contributing Vendor Components +This repo is generally meant for hosting components that work with popular open-source frameworks and tools. However, it is also possible to contribute components specific to a 3rd party vendor in this repo. + +### Adding a New Vendor Component +Vendor components that are hosted in this repo will be versioned the same as all other contrib components, and released in lockstep with them under the `@opentelemetry` org in NPM. + +In exchange, vendor component contributors are expected to: + +- Include documentation for the component that covers: + - The installation and getting started process for the component + - Any configuration for the component + - Any APIs exposed by the component + - Design information for the component if relevant +- Add enough unit tests to *at least* meet the current coverage +- Assign at least one full-time engineer to their component in the [CODEOWNERS](.github/CODEOWNERS) file +- Review pull requests that touch their component +- Respond to issues related to their component, as determined by the maintainers +- Fix failing unit tests or any other blockers to the CI/CD workflow +- Update their components' usage of Core APIs upon the introduction of breaking changes upstream + +### Removing Vendor Components +All vendor components are subject to removal from the repo at the sole discretion of the maintainers. Reasons for removal include but are not limited to failing to adhere to any of the expectations defined above in a timely manner. "Timely manner" can vary depending on the urgency of the task, for example if a flaky unit test is blocking a release for the entire repo that would be far more urgent than responding to a question about usage. As a rule of thumb, 2-3 business days is a good goal for non-urgent response times. diff --git a/metapackages/auto-instrumentations-node/package.json b/metapackages/auto-instrumentations-node/package.json index 411eb163d4..30a0e0d8c7 100644 --- a/metapackages/auto-instrumentations-node/package.json +++ b/metapackages/auto-instrumentations-node/package.json @@ -53,6 +53,7 @@ "@opentelemetry/instrumentation-koa": "^0.14.0", "@opentelemetry/instrumentation-ioredis": "^0.14.0", "@opentelemetry/instrumentation-mongodb": "^0.14.0", + "@opentelemetry/instrumentation-mysql": "^0.14.0", "@opentelemetry/instrumentation-pg": "^0.14.0", "@opentelemetry/instrumentation-redis": "^0.14.0" } diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index 2facebf623..65b272b4b1 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -25,20 +25,20 @@ import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis'; import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; -// import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; +import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; import { RedisInstrumentation } from '@opentelemetry/instrumentation-redis'; const InstrumentationMap = { '@opentelemetry/instrumentation-dns': DnsInstrumentation, '@opentelemetry/instrumentation-express': ExpressInstrumentation, - '@opentelemetry/instrumentation-http': HttpInstrumentation, '@opentelemetry/instrumentation-graphql': GraphQLInstrumentation, '@opentelemetry/instrumentation-grpc': GrpcInstrumentation, - '@opentelemetry/instrumentation-koa': KoaInstrumentation, + '@opentelemetry/instrumentation-http': HttpInstrumentation, '@opentelemetry/instrumentation-ioredis': IORedisInstrumentation, + '@opentelemetry/instrumentation-koa': KoaInstrumentation, '@opentelemetry/instrumentation-mongodb': MongoDBInstrumentation, + '@opentelemetry/instrumentation-mysql': MySQLInstrumentation, '@opentelemetry/instrumentation-pg': PgInstrumentation, - // '@opentelemetry/instrumentation-mysql': MySQLInstrumentation, '@opentelemetry/instrumentation-redis': RedisInstrumentation, }; diff --git a/metapackages/auto-instrumentations-node/test/utils.test.ts b/metapackages/auto-instrumentations-node/test/utils.test.ts index ce934382c5..832902d741 100644 --- a/metapackages/auto-instrumentations-node/test/utils.test.ts +++ b/metapackages/auto-instrumentations-node/test/utils.test.ts @@ -27,17 +27,17 @@ describe('utils', () => { const expectedInstrumentations = [ '@opentelemetry/instrumentation-dns', '@opentelemetry/instrumentation-express', - '@opentelemetry/instrumentation-http', '@opentelemetry/instrumentation-graphql', '@opentelemetry/instrumentation-grpc', - '@opentelemetry/instrumentation-koa', + '@opentelemetry/instrumentation-http', '@opentelemetry/instrumentation-ioredis', + '@opentelemetry/instrumentation-koa', '@opentelemetry/instrumentation-mongodb', - // '@opentelemetry/instrumentation-mysql', + '@opentelemetry/instrumentation-mysql', '@opentelemetry/instrumentation-pg', '@opentelemetry/instrumentation-redis', ]; - assert.strictEqual(instrumentations.length, 10); + assert.strictEqual(instrumentations.length, 11); for (let i = 0, j = instrumentations.length; i < j; i++) { assert.strictEqual( instrumentations[i].instrumentationName, diff --git a/plugins/node/opentelemetry-instrumentation-koa/src/koa.ts b/plugins/node/opentelemetry-instrumentation-koa/src/koa.ts index 5b01a063a1..223223aece 100644 --- a/plugins/node/opentelemetry-instrumentation-koa/src/koa.ts +++ b/plugins/node/opentelemetry-instrumentation-koa/src/koa.ts @@ -28,6 +28,8 @@ import { KoaContext, KoaComponentName, kLayerPatched, + KoaLayerType, + AttributeNames, } from './types'; import { VERSION } from './version'; import { getMiddlewareMetadata } from './utils'; @@ -127,7 +129,8 @@ export class KoaInstrumentation extends InstrumentationBase { middlewareLayer[kLayerPatched] = true; api.diag.debug('patching Koa middleware layer'); return async (context: KoaContext, next: koa.Next) => { - if (api.getSpan(api.context.active()) === undefined) { + const parent = api.getSpan(api.context.active()); + if (parent === undefined) { return middlewareLayer(context, next); } const metadata = getMiddlewareMetadata( @@ -140,6 +143,28 @@ export class KoaInstrumentation extends InstrumentationBase { attributes: metadata.attributes, }); + if (!context.request.ctx.parentSpan) { + context.request.ctx.parentSpan = parent; + } + + if ( + metadata.attributes[AttributeNames.KOA_TYPE] === KoaLayerType.ROUTER + ) { + if (context.request.ctx.parentSpan.name) { + const parentRoute = context.request.ctx.parentSpan.name.split(' ')[1]; + if ( + context._matchedRoute && + !context._matchedRoute.toString().includes(parentRoute) + ) { + context.request.ctx.parentSpan.updateName( + `${context.method} ${context._matchedRoute}` + ); + + delete context.request.ctx.parentSpan; + } + } + } + return api.context.with( api.setSpan(api.context.active(), span), async () => { diff --git a/plugins/node/opentelemetry-instrumentation-koa/test/koa.test.ts b/plugins/node/opentelemetry-instrumentation-koa/test/koa.test.ts index ba7db72aff..24633ff0df 100644 --- a/plugins/node/opentelemetry-instrumentation-koa/test/koa.test.ts +++ b/plugins/node/opentelemetry-instrumentation-koa/test/koa.test.ts @@ -158,7 +158,7 @@ describe('Koa Instrumentation', () => { const exportedRootSpan = memoryExporter .getFinishedSpans() - .find(span => span.name === 'rootSpan'); + .find(span => span.name === 'GET /post/:id'); assert.notStrictEqual(exportedRootSpan, undefined); }); }); @@ -200,7 +200,7 @@ describe('Koa Instrumentation', () => { const exportedRootSpan = memoryExporter .getFinishedSpans() - .find(span => span.name === 'rootSpan'); + .find(span => span.name === 'GET /:first/post/:id'); assert.notStrictEqual(exportedRootSpan, undefined); }); }); @@ -240,7 +240,7 @@ describe('Koa Instrumentation', () => { const exportedRootSpan = memoryExporter .getFinishedSpans() - .find(span => span.name === 'rootSpan'); + .find(span => span.name === 'GET /:first/post/:id'); assert.notStrictEqual(exportedRootSpan, undefined); }); }); diff --git a/plugins/node/opentelemetry-instrumentation-mysql/.eslintignore b/plugins/node/opentelemetry-instrumentation-mysql/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/opentelemetry-instrumentation-mysql/.eslintrc.js b/plugins/node/opentelemetry-instrumentation-mysql/.eslintrc.js new file mode 100644 index 0000000000..f756f4488b --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js') +} diff --git a/plugins/node/opentelemetry-instrumentation-mysql/.npmignore b/plugins/node/opentelemetry-instrumentation-mysql/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/opentelemetry-instrumentation-mysql/LICENSE b/plugins/node/opentelemetry-instrumentation-mysql/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/plugins/node/opentelemetry-instrumentation-mysql/README.md b/plugins/node/opentelemetry-instrumentation-mysql/README.md new file mode 100644 index 0000000000..61be5e3cdb --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/README.md @@ -0,0 +1,64 @@ +# OpenTelemetry mysql Instrumentation for Node.js +[![dependencies][dependencies-image]][dependencies-url] +[![devDependencies][devDependencies-image]][devDependencies-url] +[![Apache License][license-image]][license-image] + +This module provides automatic instrumentation for [`mysql`](https://www.npmjs.com/package/mysql). + +For automatic instrumentation see the +[@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-node) package. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-mysql +``` + +## Supported Versions +- `2.x` + +## Usage + +OpenTelemetry MySQL 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 [mysql](https://www.npmjs.com/package/mysql). + +To load a specific plugin (**MySQL** in this case), specify it in the registerInstrumentations's configuration +```js +const { NodeTracerProvider } = require('@opentelemetry/node'); +const { MySQLInstrumentation } = require('@opentelemetry/instrumentation-mysql'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); +provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new MySQLInstrumentation(), + { + // be sure to disable old plugin but only if it was installed + plugins: { + mysql: { enabled: false, path: '@opentelemetry/plugin-mysql' } + }, + } + ], + tracerProvider: provider, +}) +``` + +See [examples/mysql](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/examples/mysql) for a short example. + +## Useful links +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[license-url]: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/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-contrib/status.svg?path=plugins/node/opentelemetry-instrumentation-mysql +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=plugins%2Fnode%2Fopentelemetry-instrumentation-mysql +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib/dev-status.svg?path=plugins/node/opentelemetry-instrumentation-mysql +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=plugins%2Fnode%2Fopentelemetry-instrumentation-mysql&type=dev diff --git a/plugins/node/opentelemetry-instrumentation-mysql/package.json b/plugins/node/opentelemetry-instrumentation-mysql/package.json new file mode 100644 index 0000000000..5b6127294c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/package.json @@ -0,0 +1,67 @@ +{ + "name": "@opentelemetry/instrumentation-mysql", + "version": "0.14.0", + "description": "OpenTelemetry mysql automatic instrumentation package.", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js-contrib", + "scripts": { + "clean": "rimraf build/*", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "compile": "npm run version:update && tsc -p .", + "lint:fix": "eslint . --ext .ts --fix", + "lint": "eslint . --ext .ts", + "precompile": "tsc --version", + "prepare": "npm run compile", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "version:update": "node ../../../scripts/version-update.js" + }, + "keywords": [ + "opentelemetry", + "mysql", + "nodejs", + "tracing", + "profiling", + "instrumentation" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.5.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@opentelemetry/context-async-hooks": "0.18.0", + "@opentelemetry/tracing": "0.18.0", + "@opentelemetry/test-utils": "0.14.0", + "@types/mocha": "7.0.2", + "@types/mysql": "2.15.18", + "@types/node": "14.0.27", + "codecov": "3.7.2", + "gts": "3.1.0", + "mocha": "7.2.0", + "mysql": "2.18.1", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "ts-mocha": "8.0.0", + "ts-node": "9.0.0", + "tslint-consistent-codestyle": "1.16.0", + "tslint-microsoft-contrib": "6.2.0", + "typescript": "4.1.3" + }, + "dependencies": { + "@opentelemetry/api": "^0.18.0", + "@opentelemetry/instrumentation": "^0.18.0", + "@opentelemetry/semantic-conventions": "^0.18.0" + } +} diff --git a/plugins/node/opentelemetry-instrumentation-mysql/src/index.ts b/plugins/node/opentelemetry-instrumentation-mysql/src/index.ts new file mode 100644 index 0000000000..cf2009e3ce --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright The 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 * from './mysql'; +export { MySQLInstrumentationConfig } from './types'; diff --git a/plugins/node/opentelemetry-instrumentation-mysql/src/mysql.ts b/plugins/node/opentelemetry-instrumentation-mysql/src/mysql.ts new file mode 100644 index 0000000000..17d094f165 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/src/mysql.ts @@ -0,0 +1,310 @@ +/* + * Copyright The 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 { diag, Span, SpanKind, SpanStatusCode } from '@opentelemetry/api'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + isWrapped, +} from '@opentelemetry/instrumentation'; +import { DatabaseAttribute } from '@opentelemetry/semantic-conventions'; +import type * as mysqlTypes from 'mysql'; +import { MySQLInstrumentationConfig } from './types'; +import { getConnectionAttributes, getDbStatement, getSpanName } from './utils'; +import { VERSION } from './version'; + +type formatType = typeof mysqlTypes.format; + +export class MySQLInstrumentation extends InstrumentationBase< + typeof mysqlTypes +> { + static readonly COMPONENT = 'mysql'; + static readonly COMMON_ATTRIBUTES = { + [DatabaseAttribute.DB_SYSTEM]: MySQLInstrumentation.COMPONENT, + }; + + constructor(protected _config: MySQLInstrumentationConfig = {}) { + super('@opentelemetry/instrumentation-mysql', VERSION, _config); + } + + protected init() { + return [ + new InstrumentationNodeModuleDefinition( + 'mysql', + ['2.*'], + (moduleExports, moduleVersion) => { + diag.debug(`Patching mysql@${moduleVersion}`); + + diag.debug('Patching mysql.createConnection'); + if (isWrapped(moduleExports.createConnection)) { + this._unwrap(moduleExports, 'createConnection'); + } + this._wrap( + moduleExports, + 'createConnection', + this._patchCreateConnection(moduleExports.format) as any + ); + + diag.debug('Patching mysql.createPool'); + if (isWrapped(moduleExports.createPool)) { + this._unwrap(moduleExports, 'createPool'); + } + this._wrap( + moduleExports, + 'createPool', + this._patchCreatePool(moduleExports.format) as any + ); + + diag.debug('Patching mysql.createPoolCluster'); + if (isWrapped(moduleExports.createPoolCluster)) { + this._unwrap(moduleExports, 'createPoolCluster'); + } + this._wrap( + moduleExports, + 'createPoolCluster', + this._patchCreatePoolCluster(moduleExports.format) as any + ); + + return moduleExports; + }, + moduleExports => { + if (moduleExports === undefined) return; + this._unwrap(moduleExports, 'createConnection'); + this._unwrap(moduleExports, 'createPool'); + this._unwrap(moduleExports, 'createPoolCluster'); + } + ), + ]; + } + + // global export function + private _patchCreateConnection(format: formatType) { + return (originalCreateConnection: Function) => { + const thisPlugin = this; + diag.debug('MySQLInstrumentation#patch: patched mysql createConnection'); + + return function createConnection( + _connectionUri: string | mysqlTypes.ConnectionConfig + ) { + const originalResult = originalCreateConnection(...arguments); + + // This is unwrapped on next call after unpatch + thisPlugin._wrap( + originalResult, + 'query', + thisPlugin._patchQuery(originalResult, format) as any + ); + + return originalResult; + }; + }; + } + + // global export function + private _patchCreatePool(format: formatType) { + return (originalCreatePool: Function) => { + const thisPlugin = this; + diag.debug('MySQLInstrumentation#patch: patched mysql createPool'); + return function createPool(_config: string | mysqlTypes.PoolConfig) { + const pool = originalCreatePool(...arguments); + + thisPlugin._wrap(pool, 'query', thisPlugin._patchQuery(pool, format)); + thisPlugin._wrap( + pool, + 'getConnection', + thisPlugin._patchGetConnection(pool, format) + ); + + return pool; + }; + }; + } + + // global export function + private _patchCreatePoolCluster(format: formatType) { + return (originalCreatePoolCluster: Function) => { + const thisPlugin = this; + diag.debug('MySQLInstrumentation#patch: patched mysql createPoolCluster'); + return function createPool(_config: string | mysqlTypes.PoolConfig) { + const cluster = originalCreatePoolCluster(...arguments); + + // This is unwrapped on next call after unpatch + thisPlugin._wrap( + cluster, + 'getConnection', + thisPlugin._patchGetConnection(cluster, format) + ); + + return cluster; + }; + }; + } + + // method on cluster or pool + private _patchGetConnection( + pool: mysqlTypes.Pool | mysqlTypes.PoolCluster, + format: formatType + ) { + return (originalGetConnection: Function) => { + const thisPlugin = this; + diag.debug( + 'MySQLInstrumentation#patch: patched mysql pool getConnection' + ); + return function getConnection( + arg1?: unknown, + arg2?: unknown, + arg3?: unknown + ) { + // Unwrap if unpatch has been called + if (!thisPlugin['_enabled']) { + thisPlugin._unwrap(pool, 'getConnection'); + return originalGetConnection.apply(pool, arguments); + } + + if (arguments.length === 1 && typeof arg1 === 'function') { + const patchFn = thisPlugin._getConnectionCallbackPatchFn( + arg1, + format + ); + return originalGetConnection.call(pool, patchFn); + } + if (arguments.length === 2 && typeof arg2 === 'function') { + const patchFn = thisPlugin._getConnectionCallbackPatchFn( + arg2, + format + ); + return originalGetConnection.call(pool, arg1, patchFn); + } + if (arguments.length === 3 && typeof arg3 === 'function') { + const patchFn = thisPlugin._getConnectionCallbackPatchFn( + arg3, + format + ); + return originalGetConnection.call(pool, arg1, arg2, patchFn); + } + + return originalGetConnection.apply(pool, arguments); + }; + }; + } + + private _getConnectionCallbackPatchFn(cb: Function, format: formatType) { + const thisPlugin = this; + return function () { + if (arguments[1]) { + // this is the callback passed into a query + // no need to unwrap + if (!isWrapped(arguments[1].query)) { + thisPlugin._wrap( + arguments[1], + 'query', + thisPlugin._patchQuery(arguments[1], format) + ); + } + } + if (typeof cb === 'function') { + cb(...arguments); + } + }; + } + + private _patchQuery( + connection: mysqlTypes.Connection | mysqlTypes.Pool, + format: formatType + ) { + return (originalQuery: Function): mysqlTypes.QueryFunction => { + const thisPlugin = this; + diag.debug('MySQLInstrumentation: patched mysql query'); + + return function query( + query: string | mysqlTypes.Query | mysqlTypes.QueryOptions, + _valuesOrCallback?: unknown[] | mysqlTypes.queryCallback, + _callback?: mysqlTypes.queryCallback + ) { + if (!thisPlugin['_enabled']) { + thisPlugin._unwrap(connection, 'query'); + return originalQuery.apply(connection, arguments); + } + + const span = thisPlugin.tracer.startSpan(getSpanName(query), { + kind: SpanKind.CLIENT, + attributes: { + ...MySQLInstrumentation.COMMON_ATTRIBUTES, + ...getConnectionAttributes(connection.config), + }, + }); + + let values; + + if (Array.isArray(_valuesOrCallback)) { + values = _valuesOrCallback; + } else if (arguments[2]) { + values = [_valuesOrCallback]; + } + + span.setAttribute( + DatabaseAttribute.DB_STATEMENT, + getDbStatement(query, format, values) + ); + + if (arguments.length === 1) { + const streamableQuery: mysqlTypes.Query = originalQuery.apply( + connection, + arguments + ); + + return streamableQuery + .on('error', err => + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }) + ) + .on('end', () => { + span.end(); + }); + } + + if (typeof arguments[1] === 'function') { + thisPlugin._wrap(arguments, 1, thisPlugin._patchCallbackQuery(span)); + } else if (typeof arguments[2] === 'function') { + thisPlugin._wrap(arguments, 2, thisPlugin._patchCallbackQuery(span)); + } + + return originalQuery.apply(connection, arguments); + }; + }; + } + + private _patchCallbackQuery(span: Span) { + return (originalCallback: Function) => { + return function ( + err: mysqlTypes.MysqlError | null, + results?: any, + fields?: mysqlTypes.FieldInfo[] + ) { + if (err) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + } + span.end(); + return originalCallback(...arguments); + }; + }; + } +} diff --git a/plugins/node/opentelemetry-instrumentation-mysql/src/types.ts b/plugins/node/opentelemetry-instrumentation-mysql/src/types.ts new file mode 100644 index 0000000000..84091aed11 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/src/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright The 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 { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export type MySQLInstrumentationConfig = InstrumentationConfig; diff --git a/plugins/node/opentelemetry-instrumentation-mysql/src/utils.ts b/plugins/node/opentelemetry-instrumentation-mysql/src/utils.ts new file mode 100644 index 0000000000..2ffe83a92c --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/src/utils.ts @@ -0,0 +1,109 @@ +/* + * Copyright The 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 { SpanAttributes } from '@opentelemetry/api'; +import { + DatabaseAttribute, + GeneralAttribute, +} from '@opentelemetry/semantic-conventions'; +import type { + ConnectionConfig, + PoolActualConfig, + Query, + QueryOptions, +} from 'mysql'; + +/** + * Get an SpanAttributes map from a mysql connection config object + * + * @param config ConnectionConfig + */ +export function getConnectionAttributes( + config: ConnectionConfig | PoolActualConfig +): SpanAttributes { + const { host, port, database, user } = getConfig(config); + + return { + [GeneralAttribute.NET_PEER_HOSTNAME]: host, + [GeneralAttribute.NET_PEER_PORT]: port, + [GeneralAttribute.NET_PEER_ADDRESS]: getJDBCString(host, port, database), + [DatabaseAttribute.DB_NAME]: database, + [DatabaseAttribute.DB_USER]: user, + }; +} + +function getConfig(config: any) { + const { host, port, database, user } = + (config && config.connectionConfig) || config || {}; + return { host, port, database, user }; +} + +function getJDBCString( + host: string | undefined, + port: number | undefined, + database: string | undefined +) { + let jdbcString = `jdbc:mysql://${host || 'localhost'}`; + + if (typeof port === 'number') { + jdbcString += `:${port}`; + } + + if (typeof database === 'string') { + jdbcString += `/${database}`; + } + + return jdbcString; +} + +/** + * Conjures up the value for the db.statement attribute by formatting a SQL query. + * + * @returns the database statement being executed. + */ +export function getDbStatement( + query: string | Query | QueryOptions, + format: ( + sql: string, + values: any[], + stringifyObjects?: boolean, + timeZone?: string + ) => string, + values?: any[] +): string { + if (typeof query === 'string') { + return values ? format(query, values) : query; + } else { + // According to https://github.com/mysqljs/mysql#performing-queries + // The values argument will override the values in the option object. + return values || query.values + ? format(query.sql, values || query.values) + : query.sql; + } +} + +/** + * The span name SHOULD be set to a low cardinality value + * representing the statement executed on the database. + * + * @returns SQL statement without variable arguments or SQL verb + */ +export function getSpanName(query: string | Query | QueryOptions): string { + if (typeof query === 'object') { + return query.sql; + } + return query.split(' ')[0]; +} diff --git a/plugins/node/opentelemetry-instrumentation-mysql/src/version.ts b/plugins/node/opentelemetry-instrumentation-mysql/src/version.ts new file mode 100644 index 0000000000..bc552fd543 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/src/version.ts @@ -0,0 +1,18 @@ +/* + * Copyright The 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. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.14.0'; diff --git a/plugins/node/opentelemetry-instrumentation-mysql/test/mysql.test.ts b/plugins/node/opentelemetry-instrumentation-mysql/test/mysql.test.ts new file mode 100644 index 0000000000..2b475c3f4d --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/test/mysql.test.ts @@ -0,0 +1,628 @@ +/* + * Copyright The 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 { context, setSpan, SpanStatusCode } from '@opentelemetry/api'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { + DatabaseAttribute, + GeneralAttribute, +} from '@opentelemetry/semantic-conventions'; +import * as testUtils from '@opentelemetry/test-utils'; +import { + BasicTracerProvider, + InMemorySpanExporter, + ReadableSpan, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import * as assert from 'assert'; +import { MySQLInstrumentation } from '../src'; + +const port = Number(process.env.MYSQL_PORT) || 33306; +const database = process.env.MYSQL_DATABASE || 'test_db'; +const host = process.env.MYSQL_HOST || '127.0.0.1'; +const user = process.env.MYSQL_USER || 'otel'; +const password = process.env.MYSQL_PASSWORD || 'secret'; + +const instrumentation = new MySQLInstrumentation(); +instrumentation.enable(); +instrumentation.disable(); + +import * as mysqlTypes from 'mysql'; + +describe('mysql@2.x', () => { + let contextManager: AsyncHooksContextManager; + let connection: mysqlTypes.Connection; + let pool: mysqlTypes.Pool; + let poolCluster: mysqlTypes.PoolCluster; + const provider = new BasicTracerProvider(); + const testMysql = process.env.RUN_MYSQL_TESTS; // For CI: assumes local mysql db is already available + const testMysqlLocally = process.env.RUN_MYSQL_TESTS_LOCAL; // For local: spins up local mysql db via docker + const shouldTest = testMysql || testMysqlLocally; // Skips these tests if false (default) + const memoryExporter = new InMemorySpanExporter(); + + before(function (done) { + if (!shouldTest) { + // this.skip() workaround + // https://github.com/mochajs/mocha/issues/2683#issuecomment-375629901 + this.test!.parent!.pending = true; + this.skip(); + } + provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + if (testMysqlLocally) { + testUtils.startDocker('mysql'); + // wait 15 seconds for docker container to start + this.timeout(20000); + setTimeout(done, 15000); + } else { + done(); + } + }); + + after(function () { + if (testMysqlLocally) { + this.timeout(5000); + testUtils.cleanUpDocker('mysql'); + } + }); + + beforeEach(() => { + instrumentation.disable(); + contextManager = new AsyncHooksContextManager().enable(); + context.setGlobalContextManager(contextManager); + instrumentation.setTracerProvider(provider); + instrumentation.enable(); + connection = mysqlTypes.createConnection({ + port, + user, + host, + password, + database, + }); + pool = mysqlTypes.createPool({ + port, + user, + host, + password, + database, + }); + poolCluster = mysqlTypes.createPoolCluster(); + poolCluster.add('name', { + port, + user, + host, + password, + database, + }); + }); + + afterEach(done => { + context.disable(); + memoryExporter.reset(); + instrumentation.disable(); + connection.end(() => { + pool.end(() => { + poolCluster.end(() => { + done(); + }); + }); + }); + }); + + describe('when the query is a string', () => { + it('should name the span accordingly ', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + const query = connection.query(sql); + + query.on('end', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans[0].name, 'SELECT'); + done(); + }); + }); + }); + }); + + describe('when the query is an object', () => { + it('should name the span accordingly ', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+? as solution'; + const query = connection.query({ sql, values: [1] }); + + query.on('end', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans[0].name, sql); + done(); + }); + }); + }); + }); + + describe('#Connection', () => { + it('should intercept connection.query(text: string)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + const query = connection.query(sql); + let rows = 0; + + query.on('result', row => { + assert.strictEqual(row.solution, 2); + rows += 1; + }); + + query.on('end', () => { + assert.strictEqual(rows, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + + it('should intercept connection.query(text: string, callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + connection.query(sql, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + + it('should intercept connection.query(text: options, callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+? as solution'; + connection.query({ sql, values: [1] }, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + + it('should intercept connection.query(text: options, values: [], callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+? as solution'; + connection.query({ sql }, [1], (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + + it('should intercept connection.query(text: string, values: [], callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + connection.query(sql, [1], (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + + it('should intercept connection.query(text: string, value: any, callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + connection.query(sql, 1, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + + it('should attach error messages to spans', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + connection.query(sql, (err, res) => { + assert.ok(err); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, undefined, err!.message); + done(); + }); + }); + }); + }); + + describe('#Pool', () => { + it('should intercept pool.query(text: string)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + const query = pool.query(sql); + let rows = 0; + + query.on('result', row => { + assert.strictEqual(row.solution, 2); + rows += 1; + }); + + query.on('end', () => { + assert.strictEqual(rows, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + + it('should intercept pool.getConnection().query(text: string)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + pool.getConnection((err, conn) => { + const query = conn.query(sql); + let rows = 0; + + query.on('result', row => { + assert.strictEqual(row.solution, 2); + rows += 1; + }); + + query.on('end', () => { + assert.strictEqual(rows, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + }); + + it('should intercept pool.query(text: string, callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + pool.query(sql, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + + it('should intercept pool.getConnection().query(text: string, callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + pool.getConnection((err, conn) => { + conn.query(sql, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + }); + + it('should intercept pool.query(text: options, callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+? as solution'; + pool.query({ sql, values: [1] }, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + + it('should intercept pool.query(text: options, values: [], callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+? as solution'; + pool.query({ sql }, [1], (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + + it('should intercept pool.query(text: string, values: [], callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + pool.query(sql, [1], (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + + it('should intercept pool.query(text: string, value: any, callback)', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + pool.query(sql, 1, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + + it('should attach error messages to spans', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + pool.query(sql, (err, res) => { + assert.ok(err); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, undefined, err!.message); + done(); + }); + }); + }); + }); + + describe('#PoolCluster', () => { + it('should intercept poolClusterConnection.query(text: string)', done => { + poolCluster.getConnection((err, poolClusterConnection) => { + assert.ifError(err); + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + const query = poolClusterConnection.query(sql); + let rows = 0; + + query.on('result', row => { + assert.strictEqual(row.solution, 2); + rows += 1; + }); + + query.on('end', () => { + assert.strictEqual(rows, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + }); + + it('should intercept poolClusterConnection.query(text: string, callback)', done => { + poolCluster.getConnection((err, poolClusterConnection) => { + assert.ifError(err); + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+1 as solution'; + poolClusterConnection.query(sql, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + }); + + it('should intercept poolClusterConnection.query(text: options, callback)', done => { + poolCluster.getConnection((err, poolClusterConnection) => { + assert.ifError(err); + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+? as solution'; + poolClusterConnection.query({ sql, values: [1] }, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + }); + + it('should intercept poolClusterConnection.query(text: options, values: [], callback)', done => { + poolCluster.getConnection((err, poolClusterConnection) => { + assert.ifError(err); + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1+? as solution'; + poolClusterConnection.query({ sql }, [1], (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 2); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + }); + + it('should intercept poolClusterConnection.query(text: string, values: [], callback)', done => { + poolCluster.getConnection((err, poolClusterConnection) => { + assert.ifError(err); + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + poolClusterConnection.query(sql, [1], (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + }); + + it('should intercept poolClusterConnection.query(text: string, value: any, callback)', done => { + poolCluster.getConnection((err, poolClusterConnection) => { + assert.ifError(err); + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + poolClusterConnection.query(sql, 1, (err, res) => { + assert.ifError(err); + assert.ok(res); + assert.strictEqual(res[0].solution, 1); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, [1]); + done(); + }); + }); + }); + }); + + it('should attach error messages to spans', done => { + poolCluster.getConnection((err, poolClusterConnection) => { + assert.ifError(err); + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT ? as solution'; + poolClusterConnection.query(sql, (err, res) => { + assert.ok(err); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql, undefined, err!.message); + done(); + }); + }); + }); + }); + + it('should get connection by name', done => { + poolCluster.getConnection('name', (err, poolClusterConnection) => { + assert.ifError(err); + const span = provider.getTracer('default').startSpan('test span'); + context.with(setSpan(context.active(), span), () => { + const sql = 'SELECT 1 as solution'; + poolClusterConnection.query(sql, (err, res) => { + assert.ifError(err); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + }); + }); + }); + + it('should get connection by name and selector', done => { + poolCluster.getConnection( + 'name', + 'ORDER', + (err, poolClusterConnection) => { + assert.ifError(err); + const sql = 'SELECT 1 as solution'; + poolClusterConnection.query(sql, (err, res) => { + assert.ifError(err); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + assertSpan(spans[0], sql); + done(); + }); + } + ); + }); + }); +}); + +function assertSpan( + span: ReadableSpan, + sql: string, + values?: any, + errorMessage?: string +) { + assert.strictEqual(span.attributes[DatabaseAttribute.DB_SYSTEM], 'mysql'); + assert.strictEqual(span.attributes[DatabaseAttribute.DB_NAME], database); + assert.strictEqual(span.attributes[GeneralAttribute.NET_PEER_PORT], port); + assert.strictEqual(span.attributes[GeneralAttribute.NET_PEER_HOSTNAME], host); + assert.strictEqual(span.attributes[DatabaseAttribute.DB_USER], user); + assert.strictEqual( + span.attributes[DatabaseAttribute.DB_STATEMENT], + mysqlTypes.format(sql, values) + ); + if (errorMessage) { + assert.strictEqual(span.status.message, errorMessage); + assert.strictEqual(span.status.code, SpanStatusCode.ERROR); + } +} diff --git a/plugins/node/opentelemetry-instrumentation-mysql/tsconfig.json b/plugins/node/opentelemetry-instrumentation-mysql/tsconfig.json new file mode 100644 index 0000000000..28be80d266 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-mysql/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/plugins/node/opentelemetry-instrumentation-net/.eslintignore b/plugins/node/opentelemetry-instrumentation-net/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/opentelemetry-instrumentation-net/.eslintrc.js b/plugins/node/opentelemetry-instrumentation-net/.eslintrc.js new file mode 100644 index 0000000000..f756f4488b --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js') +} diff --git a/plugins/node/opentelemetry-instrumentation-net/.npmignore b/plugins/node/opentelemetry-instrumentation-net/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/plugins/node/opentelemetry-instrumentation-net/LICENSE b/plugins/node/opentelemetry-instrumentation-net/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/plugins/node/opentelemetry-instrumentation-net/README.md b/plugins/node/opentelemetry-instrumentation-net/README.md new file mode 100644 index 0000000000..73ed07c3f5 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/README.md @@ -0,0 +1,64 @@ +# OpenTelemetry Net module 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 instrumentation of outgoing connections for [`net`](http://nodejs.org/dist/latest/docs/api/net.html). +Supports both TCP and IPC connections. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-net +``` + +## Usage + +```js +const { NodeTracerProvider } = require('@opentelemetry/node'); +const { NetInstrumentation } = require('@opentelemetry/instrumentation-net'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new NetInstrumentation(), + // other instrumentations + ], + tracerProvider: provider, +}); +``` + +### Attributes added to `connect` spans + +* `net.transport`: `IP.TCP`, `pipe` or `Unix` +* `net.peer.name`: host name or the IPC file path + +For TCP: +* `net.peer.ip` +* `net.peer.port` +* `net.host.ip` +* `net.host.port` + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js-contrib.svg +[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-contrib/blob/main/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-contrib/status.svg?path=packages/opentelemetry-instrumentation-net +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=packages%2Fopentelemetry-instrumentation-net +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib/dev-status.svg?path=packages/opentelemetry-instrumentation-net +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=packages%2Fopentelemetry-instrumentation-net&type=dev diff --git a/plugins/node/opentelemetry-instrumentation-net/package.json b/plugins/node/opentelemetry-instrumentation-net/package.json new file mode 100644 index 0000000000..8fbbd44b2a --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/package.json @@ -0,0 +1,68 @@ +{ + "name": "@opentelemetry/instrumentation-net", + "version": "0.14.0", + "description": "OpenTelemetry net module automatic instrumentation package.", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js-contrib", + "scripts": { + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "tdd": "npm run test -- --watch-extensions ts --watch", + "clean": "rimraf build/*", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "precompile": "tsc --version", + "prepare": "npm run compile", + "version:update": "node ../../../scripts/version-update.js", + "compile": "npm run version:update && tsc -p ." + }, + "keywords": [ + "opentelemetry", + "net", + "connect", + "nodejs", + "tracing", + "profiling", + "instrumentation" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.5.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@opentelemetry/core": "0.18.0", + "@opentelemetry/node": "0.18.0", + "@opentelemetry/tracing": "0.18.0", + "@types/mocha": "7.0.2", + "@types/node": "14.0.27", + "@types/sinon": "9.0.4", + "codecov": "3.7.2", + "gts": "3.1.0", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "3.0.2", + "sinon": "9.0.2", + "ts-mocha": "8.0.0", + "ts-node": "9.0.0", + "tslint-consistent-codestyle": "1.16.0", + "tslint-microsoft-contrib": "6.2.0", + "typescript": "4.1.3" + }, + "dependencies": { + "@opentelemetry/api": "^0.18.0", + "@opentelemetry/instrumentation": "^0.18.0", + "@opentelemetry/semantic-conventions": "^0.18.0" + } +} diff --git a/plugins/node/opentelemetry-instrumentation-net/src/index.ts b/plugins/node/opentelemetry-instrumentation-net/src/index.ts new file mode 100644 index 0000000000..73e6a33eb9 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright The 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 * from './net'; diff --git a/plugins/node/opentelemetry-instrumentation-net/src/net.ts b/plugins/node/opentelemetry-instrumentation-net/src/net.ts new file mode 100644 index 0000000000..763d6e5142 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/src/net.ts @@ -0,0 +1,189 @@ +/* + * Copyright The 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 { diag, Span, SpanKind, SpanStatusCode } from '@opentelemetry/api'; +import { + InstrumentationBase, + InstrumentationConfig, + InstrumentationNodeModuleDefinition, + isWrapped, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; +import { GeneralAttribute } from '@opentelemetry/semantic-conventions'; +import { Net, NormalizedOptions, SocketEvent } from './types'; +import { getNormalizedArgs, IPC_TRANSPORT } from './utils'; +import { VERSION } from './version'; +import { Socket } from 'net'; + +export class NetInstrumentation extends InstrumentationBase { + constructor(protected _config: InstrumentationConfig = {}) { + super('@opentelemetry/instrumentation-net', VERSION, _config); + } + + init(): InstrumentationNodeModuleDefinition[] { + return [ + new InstrumentationNodeModuleDefinition( + 'net', + ['*'], + moduleExports => { + diag.debug('Applying patch for net module'); + if (isWrapped(moduleExports.Socket.prototype.connect)) { + this._unwrap(moduleExports.Socket.prototype, 'connect'); + } + this._wrap( + moduleExports.Socket.prototype, + 'connect', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this._getPatchedConnect() as any + ); + return moduleExports; + }, + moduleExports => { + if (moduleExports === undefined) return; + diag.debug('Removing patch from net module'); + this._unwrap(moduleExports.Socket.prototype, 'connect'); + } + ), + ]; + } + + private _getPatchedConnect() { + return (original: (...args: unknown[]) => void) => { + const plugin = this; + return function patchedConnect(this: Socket, ...args: unknown[]) { + const options = getNormalizedArgs(args); + + const span = options + ? options.path + ? plugin._startIpcSpan(options, this) + : plugin._startTcpSpan(options, this) + : plugin._startGenericSpan(this); + + return safeExecuteInTheMiddle( + () => original.apply(this, args), + error => { + if (error !== undefined) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }); + span.recordException(error); + span.end(); + } + } + ); + }; + }; + } + + /* It might still be useful to pick up errors due to invalid connect arguments. */ + private _startGenericSpan(socket: Socket) { + const span = this.tracer.startSpan('connect', { + kind: SpanKind.CLIENT, + }); + + registerListeners(socket, span); + + return span; + } + + private _startIpcSpan(options: NormalizedOptions, socket: Socket) { + const span = this.tracer.startSpan('ipc.connect', { + kind: SpanKind.CLIENT, + attributes: { + [GeneralAttribute.NET_TRANSPORT]: IPC_TRANSPORT, + [GeneralAttribute.NET_PEER_NAME]: options.path, + }, + }); + + registerListeners(socket, span); + + return span; + } + + private _startTcpSpan(options: NormalizedOptions, socket: Socket) { + const span = this.tracer.startSpan('tcp.connect', { + kind: SpanKind.CLIENT, + attributes: { + [GeneralAttribute.NET_TRANSPORT]: GeneralAttribute.IP_TCP, + [GeneralAttribute.NET_PEER_NAME]: options.host, + [GeneralAttribute.NET_PEER_PORT]: options.port, + }, + }); + + registerListeners(socket, span, { hostAttributes: true }); + + return span; + } +} + +const SOCKET_EVENTS = [ + SocketEvent.CLOSE, + SocketEvent.CONNECT, + SocketEvent.ERROR, +]; + +function spanEndHandler(span: Span) { + return () => { + span.end(); + }; +} + +function spanErrorHandler(span: Span) { + return (e: Error) => { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: e.message, + }); + }; +} + +function registerListeners( + socket: Socket, + span: Span, + { hostAttributes = false }: { hostAttributes?: boolean } = {} +) { + const setSpanError = spanErrorHandler(span); + const setSpanEnd = spanEndHandler(span); + + const setHostAttributes = () => { + span.setAttributes({ + [GeneralAttribute.NET_PEER_IP]: socket.remoteAddress, + [GeneralAttribute.NET_HOST_IP]: socket.localAddress, + [GeneralAttribute.NET_HOST_PORT]: socket.localPort, + }); + }; + + socket.once(SocketEvent.ERROR, setSpanError); + + if (hostAttributes) { + socket.once(SocketEvent.CONNECT, setHostAttributes); + } + + const removeListeners = () => { + socket.removeListener(SocketEvent.ERROR, setSpanError); + socket.removeListener(SocketEvent.CONNECT, setHostAttributes); + for (const event of SOCKET_EVENTS) { + socket.removeListener(event, setSpanEnd); + socket.removeListener(event, removeListeners); + } + }; + + for (const event of SOCKET_EVENTS) { + socket.once(event, setSpanEnd); + socket.once(event, removeListeners); + } +} diff --git a/plugins/node/opentelemetry-instrumentation-net/src/types.ts b/plugins/node/opentelemetry-instrumentation-net/src/types.ts new file mode 100644 index 0000000000..f767252dcf --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/src/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright The 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 type * as net from 'net'; + +export type Net = typeof net; + +export interface NormalizedOptions { + host?: string; + port?: number; + path?: string; +} + +export enum SocketEvent { + CLOSE = 'close', + CONNECT = 'connect', + ERROR = 'error', +} diff --git a/plugins/node/opentelemetry-instrumentation-net/src/utils.ts b/plugins/node/opentelemetry-instrumentation-net/src/utils.ts new file mode 100644 index 0000000000..4f77d29a0e --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/src/utils.ts @@ -0,0 +1,51 @@ +/* + * Copyright The 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 { NormalizedOptions } from './types'; +import { platform } from 'os'; + +// @TODO Can be replaced with constants from the semantic conventions package once released. +export const IPC_PIPE = 'pipe'; +export const IPC_UNIX = 'Unix'; +export const IPC_TRANSPORT = platform() === 'win32' ? IPC_PIPE : IPC_UNIX; + +export function getNormalizedArgs( + args: unknown[] +): NormalizedOptions | null | undefined { + const opt = args[0]; + if (!opt) { + return; + } + + switch (typeof opt) { + case 'number': + return { + port: opt, + host: typeof args[1] === 'string' ? args[1] : 'localhost', + }; + case 'object': + if (Array.isArray(opt)) { + return getNormalizedArgs(opt); + } + return opt; + case 'string': + return { + path: opt, + }; + default: + return; + } +} diff --git a/plugins/node/opentelemetry-instrumentation-net/src/version.ts b/plugins/node/opentelemetry-instrumentation-net/src/version.ts new file mode 100644 index 0000000000..bc552fd543 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/src/version.ts @@ -0,0 +1,18 @@ +/* + * Copyright The 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. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.14.0'; diff --git a/plugins/node/opentelemetry-instrumentation-net/test/connect.test.ts b/plugins/node/opentelemetry-instrumentation-net/test/connect.test.ts new file mode 100644 index 0000000000..b97672c191 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/test/connect.test.ts @@ -0,0 +1,229 @@ +/* + * Copyright The 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 { SpanStatusCode } from '@opentelemetry/api'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { GeneralAttribute } from '@opentelemetry/semantic-conventions'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import * as net from 'net'; +import * as assert from 'assert'; +import { NetInstrumentation } from '../src/net'; +import { SocketEvent } from '../src/types'; +import { assertIpcSpan, assertTcpSpan, IPC_PATH, HOST, PORT } from './utils'; + +const memoryExporter = new InMemorySpanExporter(); +const provider = new NodeTracerProvider(); +provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + +function getSpan() { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + const [span] = spans; + return span; +} + +describe('NetInstrumentation', () => { + let instrumentation: NetInstrumentation; + let socket: net.Socket; + let tcpServer: net.Server; + let ipcServer: net.Server; + + before(() => { + instrumentation = new NetInstrumentation(); + instrumentation.setTracerProvider(provider); + require('net'); + }); + + before(done => { + tcpServer = net.createServer(); + tcpServer.listen(PORT, done); + }); + + before(done => { + ipcServer = net.createServer(); + ipcServer.listen(IPC_PATH, done); + }); + + beforeEach(() => { + socket = new net.Socket(); + }); + + afterEach(() => { + socket.destroy(); + memoryExporter.reset(); + }); + + after(() => { + instrumentation.disable(); + tcpServer.close(); + ipcServer.close(); + }); + + describe('successful net.connect produces a span', () => { + it('should produce a span given port and host', done => { + socket = net.connect(PORT, HOST, () => { + assertTcpSpan(getSpan(), socket); + done(); + }); + }); + + it('should produce a span for IPC', done => { + socket = net.connect(IPC_PATH, () => { + assertIpcSpan(getSpan()); + done(); + }); + }); + + it('should produce a span given options', done => { + socket = net.connect( + { + port: PORT, + host: HOST, + }, + () => { + assertTcpSpan(getSpan(), socket); + done(); + } + ); + }); + }); + + describe('successful net.createConnection produces a span', () => { + it('should produce a span given port and host', done => { + socket = net.createConnection(PORT, HOST, () => { + assertTcpSpan(getSpan(), socket); + done(); + }); + }); + + it('should produce a span for IPC', done => { + socket = net.createConnection(IPC_PATH, () => { + assertIpcSpan(getSpan()); + done(); + }); + }); + + it('should produce a span given options', done => { + socket = net.createConnection( + { + port: PORT, + host: HOST, + }, + () => { + assertTcpSpan(getSpan(), socket); + done(); + } + ); + }); + }); + + describe('successful Socket.connect produces a span', () => { + it('should produce a span given port and host', done => { + socket.connect(PORT, HOST, () => { + assertTcpSpan(getSpan(), socket); + done(); + }); + }); + + it('should produce a span for IPC', done => { + socket.connect(IPC_PATH, () => { + assertIpcSpan(getSpan()); + done(); + }); + }); + + it('should produce a span given options', done => { + socket.connect( + { + port: PORT, + host: HOST, + }, + () => { + assertTcpSpan(getSpan(), socket); + done(); + } + ); + }); + }); + + describe('invalid input', () => { + it('should produce an error span when connect throws', done => { + assert.throws(() => { + // Invalid cast on purpose to avoid compiler errors. + socket.connect({ port: {} } as { port: number }); + }); + + assert.strictEqual(getSpan().status.code, SpanStatusCode.ERROR); + + done(); + }); + + it('should produce a generic span in case transport type can not be determined', done => { + socket.once(SocketEvent.CLOSE, () => { + const span = getSpan(); + assert.strictEqual( + span.attributes[GeneralAttribute.NET_TRANSPORT], + undefined + ); + assert.strictEqual(span.status.code, SpanStatusCode.ERROR); + done(); + }); + socket.connect((undefined as unknown) as string); + }); + }); + + describe('cleanup', () => { + function assertNoDanglingListeners() { + const events = new Set(socket.eventNames()); + for (const event of [ + SocketEvent.CLOSE, + SocketEvent.CONNECT, + SocketEvent.ERROR, + ]) { + assert.equal(events.has(event), false); + } + } + + it('should clean up listeners when destroying the socket', done => { + socket.connect(PORT); + socket.destroy(); + socket.once(SocketEvent.CLOSE, () => { + assertNoDanglingListeners(); + done(); + }); + }); + + it('should clean up listeners when successfully connecting', done => { + socket.connect(PORT, () => { + assertNoDanglingListeners(); + done(); + }); + }); + + it('should finish previous span when connecting twice', done => { + socket.connect(PORT, () => { + socket.connect(PORT, () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + done(); + }); + }); + }); + }); +}); diff --git a/plugins/node/opentelemetry-instrumentation-net/test/instrument.test.ts b/plugins/node/opentelemetry-instrumentation-net/test/instrument.test.ts new file mode 100644 index 0000000000..53f3490582 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/test/instrument.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright The 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 { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; +import { isWrapped } from '@opentelemetry/instrumentation'; +import * as assert from 'assert'; +import { NodeTracerProvider } from '@opentelemetry/node'; +import { NetInstrumentation } from '../src/net'; +import * as Sinon from 'sinon'; +import * as net from 'net'; +import { HOST, PORT } from './utils'; + +const memoryExporter = new InMemorySpanExporter(); +const provider = new NodeTracerProvider(); +const tracer = provider.getTracer('default'); +provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + +describe('NetInstrumentation', () => { + let instrumentation: NetInstrumentation; + let socket: net.Socket; + let tcpServer: net.Server; + + before(() => { + instrumentation = new NetInstrumentation(); + instrumentation.setTracerProvider(provider); + require('net'); + assert.strictEqual(isWrapped(net.Socket.prototype.connect), true); + }); + + before(done => { + tcpServer = net.createServer(); + tcpServer.listen(PORT, done); + }); + + after(() => { + tcpServer.close(); + }); + + beforeEach(() => { + Sinon.spy(tracer, 'startSpan'); + }); + + afterEach(() => { + socket.destroy(); + Sinon.restore(); + }); + + describe('disabling instrumentation', () => { + it('should not call tracer methods for creating span', done => { + instrumentation.disable(); + socket = net.connect(PORT, HOST, () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + assert.strictEqual(isWrapped(net.Socket.prototype.connect), false); + assert.strictEqual((tracer.startSpan as sinon.SinonSpy).called, false); + done(); + }); + }); + }); +}); diff --git a/plugins/node/opentelemetry-instrumentation-net/test/utils.ts b/plugins/node/opentelemetry-instrumentation-net/test/utils.ts new file mode 100644 index 0000000000..1da53a4a0d --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/test/utils.ts @@ -0,0 +1,51 @@ +/* + * Copyright The 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 } from '@opentelemetry/api'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import { GeneralAttribute } from '@opentelemetry/semantic-conventions'; +import * as assert from 'assert'; +import * as path from 'path'; +import * as os from 'os'; +import { Socket } from 'net'; +import { IPC_TRANSPORT } from '../src/utils'; + +export const PORT = 42123; +export const HOST = 'localhost'; +export const IPC_PATH = path.join(os.tmpdir(), 'otel-js-net-test-ipc'); + +export function assertTcpSpan(span: ReadableSpan, socket: Socket) { + assertClientSpan(span); + assertAttrib(span, GeneralAttribute.NET_TRANSPORT, GeneralAttribute.IP_TCP); + assertAttrib(span, GeneralAttribute.NET_PEER_NAME, HOST); + assertAttrib(span, GeneralAttribute.NET_PEER_PORT, PORT); + assertAttrib(span, GeneralAttribute.NET_HOST_IP, socket.localAddress); + assertAttrib(span, GeneralAttribute.NET_HOST_PORT, socket.localPort); +} + +export function assertIpcSpan(span: ReadableSpan) { + assertClientSpan(span); + assertAttrib(span, GeneralAttribute.NET_TRANSPORT, IPC_TRANSPORT); + assertAttrib(span, GeneralAttribute.NET_PEER_NAME, IPC_PATH); +} + +export function assertClientSpan(span: ReadableSpan) { + assert.strictEqual(span.kind, SpanKind.CLIENT); +} + +export function assertAttrib(span: ReadableSpan, attrib: string, value: any) { + assert.strictEqual(span.attributes[attrib], value); +} diff --git a/plugins/node/opentelemetry-instrumentation-net/tsconfig.json b/plugins/node/opentelemetry-instrumentation-net/tsconfig.json new file mode 100644 index 0000000000..28be80d266 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-net/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/propagators/opentelemetry-propagator-grpc-census-binary/README.md b/propagators/opentelemetry-propagator-grpc-census-binary/README.md index 494968bca4..93d2e83867 100644 --- a/propagators/opentelemetry-propagator-grpc-census-binary/README.md +++ b/propagators/opentelemetry-propagator-grpc-census-binary/README.md @@ -61,9 +61,9 @@ 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-contrib/blob/main/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-contrib/status.svg?path=propagators/opentelemetry-propagator-grpc-census-binary +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib/status.svg?path=propagators%2Fopentelemetry-propagator-grpc-census-binary [dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=propagators%2Fopentelemetry-propagator-grpc-census-binary -[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib/dev-status.svg?path=propagators/opentelemetry-propagator-grpc-census-binary +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib/dev-status.svg?path=propagators%2Fopentelemetry-propagator-grpc-census-binary [devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=propagators%2Fopentelemetry-propagator-grpc-census-binary&type=dev [npm-url]: https://www.npmjs.com/package/@opentelemetry/propagator-grpc-census-binary [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fpropagator-grpc-census-binary.svg diff --git a/propagators/opentelemetry-propagator-jaeger/README.md b/propagators/opentelemetry-propagator-jaeger/README.md index ba53de8e8e..09c3f6c092 100644 --- a/propagators/opentelemetry-propagator-jaeger/README.md +++ b/propagators/opentelemetry-propagator-jaeger/README.md @@ -52,9 +52,9 @@ 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-contrib/blob/main/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-propagator-jaeger -[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-propagator-jaeger -[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-propagator-jaeger -[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-propagator-jaeger&type=dev +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=propagators%2Fopentelemetry-propagator-jaeger +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=propagators%2Fopentelemetry-propagator-jaeger +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=propagators%2Fopentelemetry-propagator-jaeger +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=propagators%2Fopentelemetry-propagator-jaeger&type=dev [npm-url]: https://www.npmjs.com/package/@opentelemetry/propagator-jaeger [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fpropagator-jaeger.svg diff --git a/propagators/opentelemetry-propagator-ot-trace/README.md b/propagators/opentelemetry-propagator-ot-trace/README.md index 0dfc787463..c102ec1a74 100644 --- a/propagators/opentelemetry-propagator-ot-trace/README.md +++ b/propagators/opentelemetry-propagator-ot-trace/README.md @@ -58,10 +58,10 @@ 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-contrib/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-contrib/status.svg?path=packages/opentelemetry-propagator-ot-trace -[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=packages%2Fopentelemetry-propagator-ot-trace -[devdependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib/dev-status.svg?path=packages/opentelemetry-propagator-ot-trace -[devdependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=packages%2Fopentelemetry-propagator-ot-trace&type=dev +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib/status.svg?path=propagators%2Fopentelemetry-propagator-ot-trace +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=propagators%2Fopentelemetry-propagator-ot-trace +[devdependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib/dev-status.svg?path=propagators/opentelemetry-propagator-ot-trace +[devdependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js-contrib?path=propagators%2Fopentelemetry-propagator-ot-trace&type=dev [npm-url]: https://www.npmjs.com/package/@opentelemetry/propagator-ot-trace [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fpropagator-ot-trace.svg [rfc7230-url]: https://tools.ietf.org/html/rfc7230#section-3.2