diff --git a/.github/workflows/test-all-versions.yml b/.github/workflows/test-all-versions.yml index ddc3f66793..bc0b611932 100644 --- a/.github/workflows/test-all-versions.yml +++ b/.github/workflows/test-all-versions.yml @@ -93,8 +93,9 @@ jobs: MONGODB_PORT: 27017 MSSQL_PASSWORD: mssql_passw0rd MYSQL_DATABASE: otel_mysql_database - MYSQL_HOST: localhost + MYSQL_HOST: 127.0.0.1 MYSQL_PASSWORD: secret + MYSQL_ROOT_PASSWORD: rootpw MYSQL_PORT: 3306 MYSQL_USER: otel OPENTELEMETRY_REDIS_HOST: localhost @@ -118,6 +119,8 @@ jobs: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} + - name: Set MySQL variables + run: mysql --user=root --password=${MYSQL_ROOT_PASSWORD} --host=${MYSQL_HOST} --port=${MYSQL_PORT} -e "SET GLOBAL log_output='TABLE'; SET GLOBAL general_log = 1;" mysql - name: Cache Dependencies uses: actions/cache@v3 with: diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 50179edd3a..57c9f4e143 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -101,8 +101,9 @@ jobs: MONGODB_PORT: 27017 MSSQL_PASSWORD: mssql_passw0rd MYSQL_DATABASE: otel_mysql_database - MYSQL_HOST: localhost + MYSQL_HOST: 127.0.0.1 MYSQL_PASSWORD: secret + MYSQL_ROOT_PASSWORD: rootpw MYSQL_PORT: 3306 MYSQL_USER: otel OPENTELEMETRY_MEMCACHED_HOST: localhost @@ -123,6 +124,8 @@ jobs: - uses: actions/setup-node@v3 with: node-version: ${{ matrix.node }} + - name: Set MySQL variables + run: mysql --user=root --password=${MYSQL_ROOT_PASSWORD} --host=${MYSQL_HOST} --port=${MYSQL_PORT} -e "SET GLOBAL log_output='TABLE'; SET GLOBAL general_log = 1;" mysql - name: Cache Dependencies uses: actions/cache@v3 with: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0d35de43a7..b914256972 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1 +1 @@ -{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.7","detectors/node/opentelemetry-resource-detector-aws":"1.2.5","detectors/node/opentelemetry-resource-detector-container":"0.2.5","detectors/node/opentelemetry-resource-detector-gcp":"0.28.3","detectors/node/opentelemetry-resource-detector-github":"0.27.1","detectors/node/opentelemetry-resource-detector-instana":"0.4.4","metapackages/auto-instrumentations-node":"0.37.1","metapackages/auto-instrumentations-web":"0.32.3","packages/opentelemetry-host-metrics":"0.32.2","packages/opentelemetry-id-generator-aws-xray":"1.1.2","packages/opentelemetry-propagation-utils":"0.29.5","packages/opentelemetry-redis-common":"0.35.1","packages/opentelemetry-test-utils":"0.33.4","plugins/node/instrumentation-amqplib":"0.32.5","plugins/node/instrumentation-dataloader":"0.4.3","plugins/node/instrumentation-fs":"0.7.4","plugins/node/instrumentation-lru-memoizer":"0.32.4","plugins/node/instrumentation-mongoose":"0.32.4","plugins/node/instrumentation-socket.io":"0.33.4","plugins/node/instrumentation-tedious":"0.5.4","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.35.3","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.34.3","plugins/node/opentelemetry-instrumentation-bunyan":"0.31.4","plugins/node/opentelemetry-instrumentation-cassandra":"0.32.4","plugins/node/opentelemetry-instrumentation-connect":"0.31.4","plugins/node/opentelemetry-instrumentation-dns":"0.31.5","plugins/node/opentelemetry-instrumentation-express":"0.32.4","plugins/node/opentelemetry-instrumentation-fastify":"0.31.4","plugins/node/opentelemetry-instrumentation-generic-pool":"0.31.4","plugins/node/opentelemetry-instrumentation-graphql":"0.34.3","plugins/node/opentelemetry-instrumentation-hapi":"0.31.4","plugins/node/opentelemetry-instrumentation-ioredis":"0.34.3","plugins/node/opentelemetry-instrumentation-knex":"0.31.4","plugins/node/opentelemetry-instrumentation-koa":"0.34.6","plugins/node/opentelemetry-instrumentation-memcached":"0.31.4","plugins/node/opentelemetry-instrumentation-mongodb":"0.35.0","plugins/node/opentelemetry-instrumentation-mysql":"0.33.3","plugins/node/opentelemetry-instrumentation-mysql2":"0.33.4","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.32.5","plugins/node/opentelemetry-instrumentation-net":"0.31.4","plugins/node/opentelemetry-instrumentation-pg":"0.35.3","plugins/node/opentelemetry-instrumentation-pino":"0.33.4","plugins/node/opentelemetry-instrumentation-redis":"0.34.7","plugins/node/opentelemetry-instrumentation-redis-4":"0.34.6","plugins/node/opentelemetry-instrumentation-restify":"0.32.4","plugins/node/opentelemetry-instrumentation-router":"0.32.4","plugins/node/opentelemetry-instrumentation-winston":"0.31.4","plugins/web/opentelemetry-instrumentation-document-load":"0.32.3","plugins/web/opentelemetry-instrumentation-long-task":"0.32.5","plugins/web/opentelemetry-instrumentation-user-interaction":"0.32.4","plugins/web/opentelemetry-plugin-react-load":"0.28.2","propagators/opentelemetry-propagator-aws-xray":"1.2.1","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.1","propagators/opentelemetry-propagator-instana":"0.2.2","propagators/opentelemetry-propagator-ot-trace":"0.26.3"} +{"detectors/node/opentelemetry-resource-detector-alibaba-cloud":"0.27.7","detectors/node/opentelemetry-resource-detector-aws":"1.2.5","detectors/node/opentelemetry-resource-detector-container":"0.2.5","detectors/node/opentelemetry-resource-detector-gcp":"0.28.3","detectors/node/opentelemetry-resource-detector-github":"0.27.1","detectors/node/opentelemetry-resource-detector-instana":"0.4.4","metapackages/auto-instrumentations-node":"0.37.1","metapackages/auto-instrumentations-web":"0.32.3","packages/opentelemetry-host-metrics":"0.32.2","packages/opentelemetry-id-generator-aws-xray":"1.1.2","packages/opentelemetry-propagation-utils":"0.29.5","packages/opentelemetry-redis-common":"0.35.1","packages/opentelemetry-sql-common":"0.39.0","packages/opentelemetry-test-utils":"0.33.4","plugins/node/instrumentation-amqplib":"0.32.5","plugins/node/instrumentation-dataloader":"0.4.3","plugins/node/instrumentation-fs":"0.7.4","plugins/node/instrumentation-lru-memoizer":"0.32.4","plugins/node/instrumentation-mongoose":"0.32.4","plugins/node/instrumentation-socket.io":"0.33.4","plugins/node/instrumentation-tedious":"0.5.4","plugins/node/opentelemetry-instrumentation-aws-lambda":"0.35.3","plugins/node/opentelemetry-instrumentation-aws-sdk":"0.34.3","plugins/node/opentelemetry-instrumentation-bunyan":"0.31.4","plugins/node/opentelemetry-instrumentation-cassandra":"0.32.4","plugins/node/opentelemetry-instrumentation-connect":"0.31.4","plugins/node/opentelemetry-instrumentation-dns":"0.31.5","plugins/node/opentelemetry-instrumentation-express":"0.32.4","plugins/node/opentelemetry-instrumentation-fastify":"0.31.4","plugins/node/opentelemetry-instrumentation-generic-pool":"0.31.4","plugins/node/opentelemetry-instrumentation-graphql":"0.34.3","plugins/node/opentelemetry-instrumentation-hapi":"0.31.4","plugins/node/opentelemetry-instrumentation-ioredis":"0.34.3","plugins/node/opentelemetry-instrumentation-knex":"0.31.4","plugins/node/opentelemetry-instrumentation-koa":"0.34.6","plugins/node/opentelemetry-instrumentation-memcached":"0.31.4","plugins/node/opentelemetry-instrumentation-mongodb":"0.35.0","plugins/node/opentelemetry-instrumentation-mysql":"0.33.3","plugins/node/opentelemetry-instrumentation-mysql2":"0.33.4","plugins/node/opentelemetry-instrumentation-nestjs-core":"0.32.5","plugins/node/opentelemetry-instrumentation-net":"0.31.4","plugins/node/opentelemetry-instrumentation-pg":"0.35.3","plugins/node/opentelemetry-instrumentation-pino":"0.33.4","plugins/node/opentelemetry-instrumentation-redis":"0.34.7","plugins/node/opentelemetry-instrumentation-redis-4":"0.34.6","plugins/node/opentelemetry-instrumentation-restify":"0.32.4","plugins/node/opentelemetry-instrumentation-router":"0.32.4","plugins/node/opentelemetry-instrumentation-winston":"0.31.4","plugins/web/opentelemetry-instrumentation-document-load":"0.32.3","plugins/web/opentelemetry-instrumentation-long-task":"0.32.5","plugins/web/opentelemetry-instrumentation-user-interaction":"0.32.4","plugins/web/opentelemetry-plugin-react-load":"0.28.2","propagators/opentelemetry-propagator-aws-xray":"1.2.1","propagators/opentelemetry-propagator-grpc-census-binary":"0.26.1","propagators/opentelemetry-propagator-instana":"0.2.2","propagators/opentelemetry-propagator-ot-trace":"0.26.3"} diff --git a/packages/opentelemetry-sql-common/.eslintignore b/packages/opentelemetry-sql-common/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/packages/opentelemetry-sql-common/.eslintignore @@ -0,0 +1 @@ +build diff --git a/packages/opentelemetry-sql-common/.eslintrc.js b/packages/opentelemetry-sql-common/.eslintrc.js new file mode 100644 index 0000000000..15096b6658 --- /dev/null +++ b/packages/opentelemetry-sql-common/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + env: { + mocha: true, + node: true, + }, + ...require('../../eslint.config.js'), +}; diff --git a/packages/opentelemetry-sql-common/LICENSE b/packages/opentelemetry-sql-common/LICENSE new file mode 100644 index 0000000000..e50e8c80f9 --- /dev/null +++ b/packages/opentelemetry-sql-common/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 [2022] 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 + + 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/packages/opentelemetry-sql-common/README.md b/packages/opentelemetry-sql-common/README.md new file mode 100644 index 0000000000..8a7ba446d8 --- /dev/null +++ b/packages/opentelemetry-sql-common/README.md @@ -0,0 +1,6 @@ +# Common Utils for OpenTelemetry SQL packages + +This is an internal utils package used for the different SQL instrumentations: + +1. mysql2 +2. pg diff --git a/packages/opentelemetry-sql-common/package.json b/packages/opentelemetry-sql-common/package.json new file mode 100644 index 0000000000..a194f92524 --- /dev/null +++ b/packages/opentelemetry-sql-common/package.json @@ -0,0 +1,57 @@ +{ + "name": "@opentelemetry/sql-common", + "version": "0.39.0", + "description": "Utilities for SQL instrumentations", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "publishConfig": { + "access": "public" + }, + "scripts": { + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "compile": "tsc --build tsconfig.json", + "precompile": "tsc --version && lerna run version:update --scope @opentelemetry/sql-common --include-dependencies", + "prewatch": "npm run precompile", + "prepare": "npm run compile", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", + "watch": "tsc -w" + }, + "repository": "open-telemetry/opentelemetry-js-contrib", + "keywords": [ + "opentelemetry", + "contrib", + "sql" + ], + "files": [ + "build/**/*.js", + "build/**/*.js.map", + "build/**/*.d.ts", + "LICENSE", + "README.md" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "bugs": { + "url": "https://github.com/open-telemetry/opentelemetry-js-contrib/issues" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/sql-common#readme", + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + }, + "dependencies": { + "@opentelemetry/core": "^1.1.0" + }, + "devDependencies": { + "@opentelemetry/api": "^1.1.0", + "@types/mocha": "^7.0.2", + "@types/node": "18.11.7", + "mocha": "7.2.0", + "nyc": "15.1.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + } +} diff --git a/packages/opentelemetry-sql-common/src/index.ts b/packages/opentelemetry-sql-common/src/index.ts new file mode 100644 index 0000000000..b5d2e5fa46 --- /dev/null +++ b/packages/opentelemetry-sql-common/src/index.ts @@ -0,0 +1,89 @@ +/* + * 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 { + trace, + Span, + ROOT_CONTEXT, + defaultTextMapSetter, +} from '@opentelemetry/api'; +import { W3CTraceContextPropagator } from '@opentelemetry/core'; + +// NOTE: This function currently is returning false-positives +// in cases where comment characters appear in string literals +// ("SELECT '-- not a comment';" would return true, although has no comment) +function hasValidSqlComment(query: string): boolean { + const indexOpeningDashDashComment = query.indexOf('--'); + if (indexOpeningDashDashComment >= 0) { + return true; + } + + const indexOpeningSlashComment = query.indexOf('/*'); + if (indexOpeningSlashComment < 0) { + return false; + } + + const indexClosingSlashComment = query.indexOf('*/'); + return indexOpeningDashDashComment < indexClosingSlashComment; +} + +// sqlcommenter specification (https://google.github.io/sqlcommenter/spec/#value-serialization) +// expects us to URL encode based on the RFC 3986 spec (https://en.wikipedia.org/wiki/Percent-encoding), +// but encodeURIComponent does not handle some characters correctly (! ' ( ) *), +// which means we need special handling for this +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent +function fixedEncodeURIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + c => `%${c.charCodeAt(0).toString(16).toUpperCase()}` + ); +} + +export function addSqlCommenterComment(span: Span, query: string): string { + if (typeof query !== 'string' || query.length === 0) { + return query; + } + + // As per sqlcommenter spec we shall not add a comment if there already is a comment + // in the query + if (hasValidSqlComment(query)) { + return query; + } + + const propagator = new W3CTraceContextPropagator(); + const headers: { [key: string]: string } = {}; + propagator.inject( + trace.setSpan(ROOT_CONTEXT, span), + headers, + defaultTextMapSetter + ); + + // sqlcommenter spec requires keys in the comment to be sorted lexicographically + const sortedKeys = Object.keys(headers).sort(); + + if (sortedKeys.length === 0) { + return query; + } + + const commentString = sortedKeys + .map(key => { + const encodedValue = fixedEncodeURIComponent(headers[key]); + return `${key}='${encodedValue}'`; + }) + .join(','); + + return `${query} /*${commentString}*/`; +} diff --git a/packages/opentelemetry-sql-common/test/sql-common.test.ts b/packages/opentelemetry-sql-common/test/sql-common.test.ts new file mode 100644 index 0000000000..16bc2d228a --- /dev/null +++ b/packages/opentelemetry-sql-common/test/sql-common.test.ts @@ -0,0 +1,115 @@ +/* + * 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 * as assert from 'assert'; +import { + trace, + SpanContext, + TraceFlags, + INVALID_SPAN_CONTEXT, + createTraceState, +} from '@opentelemetry/api'; +import { addSqlCommenterComment } from '../src/index'; + +describe('addSqlCommenterComment', () => { + it('adds comment to a simple query', () => { + const spanContext: SpanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + + const query = 'SELECT * from FOO;'; + assert.strictEqual( + addSqlCommenterComment(trace.wrapSpanContext(spanContext), query), + "SELECT * from FOO; /*traceparent='00-d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-01'*/" + ); + }); + + it('does not add a comment if query already has a comment', () => { + const span = trace.wrapSpanContext({ + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }); + + const blockComment = 'SELECT * from FOO; /* Test comment */'; + assert.strictEqual( + addSqlCommenterComment(span, blockComment), + blockComment + ); + + const dashedComment = 'SELECT * from FOO; -- Test comment'; + assert.strictEqual( + addSqlCommenterComment(span, dashedComment), + dashedComment + ); + }); + + it('does not add a comment to an empty query', () => { + const spanContext: SpanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + }; + + assert.strictEqual( + addSqlCommenterComment(trace.wrapSpanContext(spanContext), ''), + '' + ); + }); + + it('does not add a comment if span context is invalid', () => { + const query = 'SELECT * from FOO;'; + assert.strictEqual( + addSqlCommenterComment( + trace.wrapSpanContext(INVALID_SPAN_CONTEXT), + query + ), + query + ); + }); + + it('correctly also sets trace state', () => { + const spanContext: SpanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + traceState: createTraceState('foo=bar,baz=qux'), + }; + + const query = 'SELECT * from FOO;'; + assert.strictEqual( + addSqlCommenterComment(trace.wrapSpanContext(spanContext), query), + "SELECT * from FOO; /*traceparent='00-d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-01',tracestate='foo%3Dbar%2Cbaz%3Dqux'*/" + ); + }); + + it('escapes special characters in values', () => { + const spanContext: SpanContext = { + traceId: 'd4cda95b652f4a1592b449d5929fda1b', + spanId: '6e0c63257de34c92', + traceFlags: TraceFlags.SAMPLED, + traceState: createTraceState("foo='bar,baz='qux!()*',hack='DROP TABLE"), + }; + + const query = 'SELECT * from FOO;'; + assert.strictEqual( + addSqlCommenterComment(trace.wrapSpanContext(spanContext), query), + "SELECT * from FOO; /*traceparent='00-d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-01',tracestate='foo%3D%27bar%2Cbaz%3D%27qux%21%28%29%2A%27%2Chack%3D%27DROP%20TABLE'*/" + ); + }); +}); diff --git a/packages/opentelemetry-sql-common/tsconfig.json b/packages/opentelemetry-sql-common/tsconfig.json new file mode 100644 index 0000000000..8f29202a96 --- /dev/null +++ b/packages/opentelemetry-sql-common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/packages/opentelemetry-test-utils/src/test-utils.ts b/packages/opentelemetry-test-utils/src/test-utils.ts index 4e2dfbb6a2..d9d7f3e2e3 100644 --- a/packages/opentelemetry-test-utils/src/test-utils.ts +++ b/packages/opentelemetry-test-utils/src/test-utils.ts @@ -38,7 +38,7 @@ const dockerRunCmds = { mssql: 'docker run --rm -d --name otel-mssql -p 1433:1433 -e SA_PASSWORD=mssql_passw0rd -e ACCEPT_EULA=Y mcr.microsoft.com/mssql/server:2017-latest', mysql: - 'docker run --rm -d --name otel-mysql -p 33306:3306 -e MYSQL_ROOT_PASSWORD=rootpw -e MYSQL_DATABASE=test_db -e MYSQL_USER=otel -e MYSQL_PASSWORD=secret mysql:5.7', + 'docker run --rm -d --name otel-mysql -p 33306:3306 -e MYSQL_ROOT_PASSWORD=rootpw -e MYSQL_DATABASE=test_db -e MYSQL_USER=otel -e MYSQL_PASSWORD=secret mysql:5.7 --log_output=TABLE --general_log=ON', postgres: 'docker run --rm -d --name otel-postgres -p 54320:5432 -e POSTGRES_PASSWORD=postgres postgres:15-alpine', redis: 'docker run --rm -d --name otel-redis -p 63790:6379 redis:alpine', diff --git a/plugins/node/opentelemetry-instrumentation-mysql2/README.md b/plugins/node/opentelemetry-instrumentation-mysql2/README.md index d0940894b7..04f15355ef 100644 --- a/plugins/node/opentelemetry-instrumentation-mysql2/README.md +++ b/plugins/node/opentelemetry-instrumentation-mysql2/README.md @@ -47,6 +47,7 @@ You can set the following instrumentation options: | Options | Type | Description | | ------- | ---- | ----------- | | `responseHook` | `MySQL2InstrumentationExecutionResponseHook` (function) | Function for adding custom attributes from db response | +| `addSqlCommenterCommentToQueries` | `boolean` | If true, adds [sqlcommenter](https://github.com/open-telemetry/opentelemetry-sqlcommenter) specification compliant comment to queries with tracing context (default false). _NOTE: A comment will not be added to queries that already contain `--` or `/* ... */` in them, even if these are not actually part of comments_ | ## Useful links diff --git a/plugins/node/opentelemetry-instrumentation-mysql2/package.json b/plugins/node/opentelemetry-instrumentation-mysql2/package.json index 21014d84fb..0ca042b944 100644 --- a/plugins/node/opentelemetry-instrumentation-mysql2/package.json +++ b/plugins/node/opentelemetry-instrumentation-mysql2/package.json @@ -51,6 +51,7 @@ "@opentelemetry/context-async-hooks": "^1.8.0", "@opentelemetry/contrib-test-utils": "^0.33.4", "@opentelemetry/sdk-trace-base": "^1.8.0", + "@opentelemetry/sql-common": "^0.39.0", "@types/mocha": "7.0.2", "@types/mysql2": "github:types/mysql2", "@types/node": "18.11.7", diff --git a/plugins/node/opentelemetry-instrumentation-mysql2/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-mysql2/src/instrumentation.ts index 682ebe0b04..ea792f8025 100644 --- a/plugins/node/opentelemetry-instrumentation-mysql2/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-mysql2/src/instrumentation.ts @@ -25,6 +25,7 @@ import { DbSystemValues, SemanticAttributes, } from '@opentelemetry/semantic-conventions'; +import { addSqlCommenterComment } from '@opentelemetry/sql-common'; import type * as mysqlTypes from 'mysql2'; import { MySQL2InstrumentationConfig } from './types'; import { @@ -52,7 +53,7 @@ export class MySQL2Instrumentation extends InstrumentationBase { 'mysql2', ['>= 1.4.2 < 4.0'], (moduleExports: any, moduleVersion) => { - api.diag.debug(`Patching mysql@${moduleVersion}`); + api.diag.debug(`Patching mysql2@${moduleVersion}`); const ConnectionPrototype: mysqlTypes.Connection = moduleExports.Connection.prototype; @@ -63,7 +64,7 @@ export class MySQL2Instrumentation extends InstrumentationBase { this._wrap( ConnectionPrototype, 'query', - this._patchQuery(moduleExports.format) as any + this._patchQuery(moduleExports.format, false) as any ); if (isWrapped(ConnectionPrototype.execute)) { @@ -72,7 +73,7 @@ export class MySQL2Instrumentation extends InstrumentationBase { this._wrap( ConnectionPrototype, 'execute', - this._patchQuery(moduleExports.format) as any + this._patchQuery(moduleExports.format, true) as any ); return moduleExports; @@ -88,7 +89,7 @@ export class MySQL2Instrumentation extends InstrumentationBase { ]; } - private _patchQuery(format: formatType) { + private _patchQuery(format: formatType, isPrepared: boolean) { return (originalQuery: Function): Function => { const thisPlugin = this; api.diag.debug('MySQL2Instrumentation: patched mysql query/execute'); @@ -99,6 +100,9 @@ export class MySQL2Instrumentation extends InstrumentationBase { _valuesOrCallback?: unknown[] | Function, _callback?: Function ) { + const thisPluginConfig: MySQL2InstrumentationConfig = + thisPlugin._config; + let values; if (Array.isArray(_valuesOrCallback)) { values = _valuesOrCallback; @@ -118,6 +122,16 @@ export class MySQL2Instrumentation extends InstrumentationBase { ), }, }); + + if (!isPrepared && thisPluginConfig.addSqlCommenterCommentToQueries) { + arguments[0] = query = + typeof query === 'string' + ? addSqlCommenterComment(span, query) + : Object.assign(query, { + sql: addSqlCommenterComment(span, query.sql), + }); + } + const endSpan = once((err?: any, results?: any) => { if (err) { span.setStatus({ @@ -125,11 +139,12 @@ export class MySQL2Instrumentation extends InstrumentationBase { message: err.message, }); } else { - const config: MySQL2InstrumentationConfig = thisPlugin._config; - if (typeof config.responseHook === 'function') { + if (typeof thisPluginConfig.responseHook === 'function') { safeExecuteInTheMiddle( () => { - config.responseHook!(span, { queryResults: results }); + thisPluginConfig.responseHook!(span, { + queryResults: results, + }); }, err => { if (err) { diff --git a/plugins/node/opentelemetry-instrumentation-mysql2/src/types.ts b/plugins/node/opentelemetry-instrumentation-mysql2/src/types.ts index c14743495b..01e9f8a434 100644 --- a/plugins/node/opentelemetry-instrumentation-mysql2/src/types.ts +++ b/plugins/node/opentelemetry-instrumentation-mysql2/src/types.ts @@ -33,4 +33,10 @@ export interface MySQL2InstrumentationConfig extends InstrumentationConfig { * @default undefined */ responseHook?: MySQL2InstrumentationExecutionResponseHook; + + /** + * If true, queries are modified to also include a comment with + * the tracing context, following the {@link https://github.com/open-telemetry/opentelemetry-sqlcommenter sqlcommenter} format + */ + addSqlCommenterCommentToQueries?: boolean; } diff --git a/plugins/node/opentelemetry-instrumentation-mysql2/test/mysql.test.ts b/plugins/node/opentelemetry-instrumentation-mysql2/test/mysql.test.ts index 3874ccf177..0e81e55a66 100644 --- a/plugins/node/opentelemetry-instrumentation-mysql2/test/mysql.test.ts +++ b/plugins/node/opentelemetry-instrumentation-mysql2/test/mysql.test.ts @@ -37,6 +37,7 @@ 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 rootPassword = process.env.MYSQL_ROOT_PASSWORD || 'rootpw'; const instrumentation = new MySQL2Instrumentation(); instrumentation.enable(); @@ -48,9 +49,10 @@ interface Result extends mysqlTypes.RowDataPacket { solution: number; } -describe('mysql@2.x', () => { +describe('mysql2@2.x', () => { let contextManager: AsyncHooksContextManager; let connection: mysqlTypes.Connection; + let rootConnection: mysqlTypes.Connection; let pool: mysqlTypes.Pool; let poolCluster: mysqlTypes.PoolCluster; const provider = new BasicTracerProvider(); @@ -59,6 +61,24 @@ describe('mysql@2.x', () => { const shouldTest = testMysql || testMysqlLocally; // Skips these tests if false (default) const memoryExporter = new InMemorySpanExporter(); + const getLastQueries = (count: number) => + new Promise(res => { + const queries: string[] = []; + const query = rootConnection.query({ + sql: "SELECT * FROM mysql.general_log WHERE command_type = 'Query' ORDER BY event_time DESC LIMIT ? OFFSET 1", + values: [count], + }); + + query.on('result', (row: { argument: string | Buffer }) => { + if (typeof row.argument === 'string') { + queries.push(row.argument); + } else { + queries.push(row.argument.toString('utf-8')); + } + }); + query.on('end', () => res(queries)); + }); + before(function (done) { if (!shouldTest) { // this.skip() workaround @@ -67,6 +87,13 @@ describe('mysql@2.x', () => { this.skip(); } provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); + rootConnection = mysqlTypes.createConnection({ + port, + user: 'root', + host, + password: rootPassword, + database, + }); if (testMysqlLocally) { testUtils.startDocker('mysql'); // wait 15 seconds for docker container to start @@ -77,11 +104,14 @@ describe('mysql@2.x', () => { } }); - after(function () { - if (testMysqlLocally) { - this.timeout(5000); - testUtils.cleanUpDocker('mysql'); - } + after(function (done) { + rootConnection.end(() => { + if (testMysqlLocally) { + this.timeout(5000); + testUtils.cleanUpDocker('mysql'); + } + done(); + }); }); beforeEach(() => { @@ -119,6 +149,7 @@ describe('mysql@2.x', () => { afterEach(done => { context.disable(); memoryExporter.reset(); + instrumentation.setConfig(); instrumentation.disable(); connection.end(() => { pool.end(() => { @@ -299,6 +330,71 @@ describe('mysql@2.x', () => { }); }); }); + + it('should not add comment by default', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + connection.query('SELECT 1+1 as solution', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + getLastQueries(1).then(([query]) => { + assert.doesNotMatch(query, /.*traceparent.*/); + done(); + }); + }); + }); + }); + + it('should not add comment when specified if existing block comment', done => { + instrumentation.setConfig({ + addSqlCommenterCommentToQueries: true, + } as any); + const span = provider.getTracer('default').startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + connection.query('SELECT 1+1 as solution /*block comment*/', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + getLastQueries(1).then(([query]) => { + assert.doesNotMatch(query, /.*traceparent.*/); + done(); + }); + }); + }); + }); + + it('should not add comment when specified if existing line comment', done => { + instrumentation.setConfig({ + addSqlCommenterCommentToQueries: true, + } as any); + const span = provider.getTracer('default').startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + connection.query('SELECT 1+1 as solution -- line comment', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + getLastQueries(1).then(([query]) => { + assert.doesNotMatch(query, /.*traceparent.*/); + done(); + }); + }); + }); + }); + + it('should add comment when specified if no existing comment', done => { + instrumentation.setConfig({ + addSqlCommenterCommentToQueries: true, + } as any); + const span = provider.getTracer('default').startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + connection.query('SELECT 1+1 as solution', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + getLastQueries(1).then(([query]) => { + assert.match(query, /.*traceparent.*/); + done(); + }); + }); + }); + }); }); describe('#Connection.execute', () => { @@ -587,6 +683,71 @@ describe('mysql@2.x', () => { }); }); }); + + it('should not add comment by default', done => { + const span = provider.getTracer('default').startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + pool.query('SELECT 1+1 as solution', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + getLastQueries(1).then(([query]) => { + assert.doesNotMatch(query, /.*traceparent.*/); + done(); + }); + }); + }); + }); + + it('should not add comment when specified if existing block comment', done => { + instrumentation.setConfig({ + addSqlCommenterCommentToQueries: true, + } as any); + const span = provider.getTracer('default').startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + pool.query('SELECT 1+1 as solution /*block comment*/', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + getLastQueries(1).then(([query]) => { + assert.doesNotMatch(query, /.*traceparent.*/); + done(); + }); + }); + }); + }); + + it('should not add comment when specified if existing line comment', done => { + instrumentation.setConfig({ + addSqlCommenterCommentToQueries: true, + } as any); + const span = provider.getTracer('default').startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + pool.query('SELECT 1+1 as solution -- line comment', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + getLastQueries(1).then(([query]) => { + assert.doesNotMatch(query, /.*traceparent.*/); + done(); + }); + }); + }); + }); + + it('should add comment when specified if no existing comment', done => { + instrumentation.setConfig({ + addSqlCommenterCommentToQueries: true, + } as any); + const span = provider.getTracer('default').startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + pool.query('SELECT 1+1 as solution', () => { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 1); + getLastQueries(1).then(([query]) => { + assert.match(query, /.*traceparent.*/); + done(); + }); + }); + }); + }); }); describe('#Pool.execute', () => { @@ -940,21 +1101,14 @@ describe('mysql@2.x', () => { describe('#responseHook', () => { const queryResultAttribute = 'query_result'; - after(() => { - instrumentation.setConfig({}); - }); - describe('invalid repsonse hook', () => { - before(() => { - instrumentation.disable(); - instrumentation.setTracerProvider(provider); + beforeEach(() => { const config: MySQL2InstrumentationConfig = { responseHook: (span, responseHookInfo) => { throw new Error('random failure!'); }, }; instrumentation.setConfig(config); - instrumentation.enable(); }); it('should not affect the behavior of the query', done => { @@ -972,9 +1126,7 @@ describe('mysql@2.x', () => { }); describe('valid response hook', () => { - before(() => { - instrumentation.disable(); - instrumentation.setTracerProvider(provider); + beforeEach(() => { const config: MySQL2InstrumentationConfig = { responseHook: (span, responseHookInfo) => { span.setAttribute( @@ -984,7 +1136,6 @@ describe('mysql@2.x', () => { }, }; instrumentation.setConfig(config); - instrumentation.enable(); }); it('should extract data from responseHook - connection', done => { diff --git a/plugins/node/opentelemetry-instrumentation-pg/package.json b/plugins/node/opentelemetry-instrumentation-pg/package.json index 8d9dd67f29..94b881c9b4 100644 --- a/plugins/node/opentelemetry-instrumentation-pg/package.json +++ b/plugins/node/opentelemetry-instrumentation-pg/package.json @@ -77,6 +77,7 @@ "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.40.0", "@opentelemetry/semantic-conventions": "^1.0.0", + "@opentelemetry/sql-common": "^0.39.0", "@types/pg": "8.6.1", "@types/pg-pool": "2.0.3" }, diff --git a/plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts index 00ea20e789..f47b1e81b0 100644 --- a/plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-pg/src/instrumentation.ts @@ -44,6 +44,7 @@ import { SemanticAttributes, DbSystemValues, } from '@opentelemetry/semantic-conventions'; +import { addSqlCommenterComment } from '@opentelemetry/sql-common'; import { VERSION } from './version'; const PG_POOL_COMPONENT = 'pg-pool'; @@ -214,20 +215,13 @@ export class PgInstrumentation extends InstrumentationBase { // Modify query text w/ a tracing comment before invoking original for // tracing, but only if args[0] has one of our expected shapes. - // - // TODO: remove the `as ...` casts below when the TS version is upgraded. - // Newer TS versions will use the result of firstArgIsQueryObjectWithText - // to properly narrow arg0, but TS 4.3.5 does not. if (instrumentationConfig.addSqlCommenterCommentToQueries) { args[0] = firstArgIsString - ? utils.addSqlCommenterComment(span, arg0 as string) + ? addSqlCommenterComment(span, arg0) : firstArgIsQueryObjectWithText ? { - ...(arg0 as utils.ObjectWithText), - text: utils.addSqlCommenterComment( - span, - (arg0 as utils.ObjectWithText).text - ), + ...arg0, + text: addSqlCommenterComment(span, arg0.text), } : args[0]; } diff --git a/plugins/node/opentelemetry-instrumentation-pg/src/utils.ts b/plugins/node/opentelemetry-instrumentation-pg/src/utils.ts index 1565d6d03b..d97f7f9c3e 100644 --- a/plugins/node/opentelemetry-instrumentation-pg/src/utils.ts +++ b/plugins/node/opentelemetry-instrumentation-pg/src/utils.ts @@ -22,10 +22,7 @@ import { Tracer, SpanKind, diag, - defaultTextMapSetter, - ROOT_CONTEXT, } from '@opentelemetry/api'; -import { W3CTraceContextPropagator } from '@opentelemetry/core'; import { AttributeNames } from './enums/AttributeNames'; import { SemanticAttributes, @@ -266,72 +263,6 @@ export function patchClientConnectCallback(span: Span, cb: Function): Function { }; } -// NOTE: This function currently is returning false-positives -// in cases where comment characters appear in string literals -// ("SELECT '-- not a comment';" would return true, although has no comment) -function hasValidSqlComment(query: string): boolean { - const indexOpeningDashDashComment = query.indexOf('--'); - if (indexOpeningDashDashComment >= 0) { - return true; - } - - const indexOpeningSlashComment = query.indexOf('/*'); - if (indexOpeningSlashComment < 0) { - return false; - } - - const indexClosingSlashComment = query.indexOf('*/'); - return indexOpeningDashDashComment < indexClosingSlashComment; -} - -// sqlcommenter specification (https://google.github.io/sqlcommenter/spec/#value-serialization) -// expects us to URL encode based on the RFC 3986 spec (https://en.wikipedia.org/wiki/Percent-encoding), -// but encodeURIComponent does not handle some characters correctly (! ' ( ) *), -// which means we need special handling for this -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent -function fixedEncodeURIComponent(str: string) { - return encodeURIComponent(str).replace( - /[!'()*]/g, - c => `%${c.charCodeAt(0).toString(16).toUpperCase()}` - ); -} - -export function addSqlCommenterComment(span: Span, query: string): string { - if (typeof query !== 'string' || query.length === 0) { - return query; - } - - // As per sqlcommenter spec we shall not add a comment if there already is a comment - // in the query - if (hasValidSqlComment(query)) { - return query; - } - - const propagator = new W3CTraceContextPropagator(); - const headers: { [key: string]: string } = {}; - propagator.inject( - trace.setSpan(ROOT_CONTEXT, span), - headers, - defaultTextMapSetter - ); - - // sqlcommenter spec requires keys in the comment to be sorted lexicographically - const sortedKeys = Object.keys(headers).sort(); - - if (sortedKeys.length === 0) { - return query; - } - - const commentString = sortedKeys - .map(key => { - const encodedValue = fixedEncodeURIComponent(headers[key]); - return `${key}='${encodedValue}'`; - }) - .join(','); - - return `${query} /*${commentString}*/`; -} - /** * Attempt to get a message string from a thrown value, while being quite * defensive, to recognize the fact that, in JS, any kind of value (even diff --git a/plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts b/plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts index a6c368ad32..dec92cd7ce 100644 --- a/plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts +++ b/plugins/node/opentelemetry-instrumentation-pg/test/pg.test.ts @@ -45,7 +45,7 @@ import { SemanticAttributes, DbSystemValues, } from '@opentelemetry/semantic-conventions'; -import { addSqlCommenterComment } from '../src/utils'; +import { addSqlCommenterComment } from '@opentelemetry/sql-common'; const memoryExporter = new InMemorySpanExporter(); diff --git a/plugins/node/opentelemetry-instrumentation-pg/test/utils.test.ts b/plugins/node/opentelemetry-instrumentation-pg/test/utils.test.ts index d7c4f1f6aa..ec32b55694 100644 --- a/plugins/node/opentelemetry-instrumentation-pg/test/utils.test.ts +++ b/plugins/node/opentelemetry-instrumentation-pg/test/utils.test.ts @@ -14,14 +14,7 @@ * limitations under the License. */ -import { - context, - createTraceState, - INVALID_SPAN_CONTEXT, - SpanContext, - trace, - TraceFlags, -} from '@opentelemetry/api'; +import { context, trace } from '@opentelemetry/api'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { @@ -200,94 +193,4 @@ describe('utils.ts', () => { assert.deepStrictEqual(pgValues, ['0']); }); }); - - describe('addSqlCommenterComment', () => { - it('adds comment to a simple query', () => { - const spanContext: SpanContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - }; - - const query = 'SELECT * from FOO;'; - assert.strictEqual( - utils.addSqlCommenterComment(trace.wrapSpanContext(spanContext), query), - "SELECT * from FOO; /*traceparent='00-d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-01'*/" - ); - }); - - it('does not add a comment if query already has a comment', () => { - const span = trace.wrapSpanContext({ - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - }); - - const blockComment = 'SELECT * from FOO; /* Test comment */'; - assert.strictEqual( - utils.addSqlCommenterComment(span, blockComment), - blockComment - ); - - const dashedComment = 'SELECT * from FOO; -- Test comment'; - assert.strictEqual( - utils.addSqlCommenterComment(span, dashedComment), - dashedComment - ); - }); - - it('does not add a comment to an empty query', () => { - const spanContext: SpanContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - }; - - assert.strictEqual( - utils.addSqlCommenterComment(trace.wrapSpanContext(spanContext), ''), - '' - ); - }); - - it('does not add a comment if span context is invalid', () => { - const query = 'SELECT * from FOO;'; - assert.strictEqual( - utils.addSqlCommenterComment( - trace.wrapSpanContext(INVALID_SPAN_CONTEXT), - query - ), - query - ); - }); - - it('correctly also sets trace state', () => { - const spanContext: SpanContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - traceState: createTraceState('foo=bar,baz=qux'), - }; - - const query = 'SELECT * from FOO;'; - assert.strictEqual( - utils.addSqlCommenterComment(trace.wrapSpanContext(spanContext), query), - "SELECT * from FOO; /*traceparent='00-d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-01',tracestate='foo%3Dbar%2Cbaz%3Dqux'*/" - ); - }); - - it('escapes special characters in values', () => { - const spanContext: SpanContext = { - traceId: 'd4cda95b652f4a1592b449d5929fda1b', - spanId: '6e0c63257de34c92', - traceFlags: TraceFlags.SAMPLED, - traceState: createTraceState("foo='bar,baz='qux!()*',hack='DROP TABLE"), - }; - - const query = 'SELECT * from FOO;'; - assert.strictEqual( - utils.addSqlCommenterComment(trace.wrapSpanContext(spanContext), query), - "SELECT * from FOO; /*traceparent='00-d4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-01',tracestate='foo%3D%27bar%2Cbaz%3D%27qux%21%28%29%2A%27%2Chack%3D%27DROP%20TABLE'*/" - ); - }); - }); }); diff --git a/release-please-config.json b/release-please-config.json index 424fe6824a..c92ac926df 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -18,6 +18,7 @@ "packages/opentelemetry-id-generator-aws-xray": {}, "packages/opentelemetry-propagation-utils": {}, "packages/opentelemetry-redis-common": {}, + "packages/opentelemetry-sql-common": {}, "packages/opentelemetry-test-utils": {}, "plugins/node/instrumentation-amqplib": {}, "plugins/node/instrumentation-dataloader": {},