diff --git a/.github/component-label-map.yml b/.github/component-label-map.yml index 1ec3c1a23a..d5d893aae2 100644 --- a/.github/component-label-map.yml +++ b/.github/component-label-map.yml @@ -188,6 +188,11 @@ pkg:instrumentation-net: - changed-files: - any-glob-to-any-file: - plugins/node/opentelemetry-instrumentation-net/** +pkg:instrumentation-oracledb: + - changed-files: + - any-glob-to-any-file: + - plugins/node/opentelemetry-instrumentation-oracledb/** + - packages/opentelemetry-test-utils/** pkg:instrumentation-pg: - changed-files: - any-glob-to-any-file: diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 53ad4775c1..e21c924fef 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -123,6 +123,9 @@ components: # Unmaintained plugins/node/opentelemetry-instrumentation-net: - seemk + plugins/node/opentelemetry-instrumentation-oracledb: + - sudarshan12s + - sharadraju plugins/node/instrumentation-runtime-node: - d4nyll plugins/node/opentelemetry-instrumentation-pg: diff --git a/.github/workflows/test-all-versions.yml b/.github/workflows/test-all-versions.yml index f53511b560..48c0ea7b2e 100644 --- a/.github/workflows/test-all-versions.yml +++ b/.github/workflows/test-all-versions.yml @@ -80,6 +80,19 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + oracledb: + image: gvenzl/oracle-free:slim + env: + APP_USER: otel + APP_USER_PASSWORD: secret + ORACLE_PASSWORD: oracle + ports: + - 1521:1521 + options: >- + --health-cmd "sqlplus system/oracle@//localhost/FREEPDB1" + --health-interval 10s + --health-timeout 5s + --health-retries 5 postgres: image: postgres:16-alpine env: @@ -122,6 +135,12 @@ jobs: MYSQL_USER: otel OPENTELEMETRY_REDIS_HOST: localhost OPENTELEMETRY_REDIS_PORT: 6379 + ORACLE_HOSTNAME: localhost + ORACLE_PORT: 1521 + ORACLE_CONNECTSTRING: localhost:1521/freepdb1 + ORACLE_USER: otel + ORACLE_PASSWORD: secret + ORACLE_SERVICENAME: FREEPDB1 POSTGRES_DB: otel_pg_database POSTGRES_HOST: localhost POSTGRES_PORT: 5432 @@ -130,6 +149,7 @@ jobs: RUN_MONGODB_TESTS: 1 RUN_MSSQL_TESTS: 1 RUN_MYSQL_TESTS: 1 + RUN_ORACLEDB_TESTS: 1 RUN_POSTGRES_TESTS: 1 RUN_REDIS_TESTS: 1 NPM_CONFIG_UNSAFE_PERM: true diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index b08c06bab4..cb6ae58bb3 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -83,6 +83,19 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + oracledb: + image: gvenzl/oracle-free:slim + env: + APP_USER: otel + APP_USER_PASSWORD: secret + ORACLE_PASSWORD: oracle + ports: + - 1521:1521 + options: >- + --health-cmd "sqlplus system/oracle@//localhost/FREEPDB1" + --health-interval 10s + --health-timeout 5s + --health-retries 5 postgres: image: postgres:16-alpine env: @@ -122,6 +135,7 @@ jobs: RUN_MONGODB_TESTS: 1 RUN_MYSQL_TESTS: 1 RUN_MSSQL_TESTS: 1 + RUN_ORACLEDB_TESTS: 1 RUN_POSTGRES_TESTS: 1 RUN_REDIS_TESTS: 1 RUN_RABBIT_TESTS: 1 @@ -140,6 +154,12 @@ jobs: OPENTELEMETRY_MEMCACHED_PORT: 11211 OPENTELEMETRY_REDIS_HOST: localhost OPENTELEMETRY_REDIS_PORT: 6379 + ORACLE_HOSTNAME: localhost + ORACLE_PORT: 1521 + ORACLE_CONNECTSTRING: localhost:1521/freepdb1 + ORACLE_USER: otel + ORACLE_PASSWORD: secret + ORACLE_SERVICENAME: FREEPDB1 POSTGRES_DB: otel_pg_database POSTGRES_HOST: localhost POSTGRES_PORT: 5432 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6693797882..1c54bb4e2a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -50,6 +50,7 @@ "plugins/node/opentelemetry-instrumentation-mysql2": "0.46.0", "plugins/node/opentelemetry-instrumentation-nestjs-core": "0.46.0", "plugins/node/opentelemetry-instrumentation-net": "0.44.0", + "plugins/node/opentelemetry-instrumentation-oracledb": "0.26.0", "plugins/node/opentelemetry-instrumentation-pg": "0.52.0", "plugins/node/opentelemetry-instrumentation-pino": "0.47.0", "plugins/node/opentelemetry-instrumentation-redis": "0.47.0", diff --git a/metapackages/auto-instrumentations-node/README.md b/metapackages/auto-instrumentations-node/README.md index 8af40bb79c..dc6658c199 100644 --- a/metapackages/auto-instrumentations-node/README.md +++ b/metapackages/auto-instrumentations-node/README.md @@ -193,6 +193,7 @@ registerInstrumentations({ - [@opentelemetry/instrumentation-mysql2](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql2) - [@opentelemetry/instrumentation-nestjs-core](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-nestjs-core) - [@opentelemetry/instrumentation-net](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-net) +- [@opentelemetry/instrumentation-oracledb](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-oracledb) - [@opentelemetry/instrumentation-pg](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-pg) - [@opentelemetry/instrumentation-pino](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-pino) - [@opentelemetry/instrumentation-redis](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-redis) diff --git a/metapackages/auto-instrumentations-node/package.json b/metapackages/auto-instrumentations-node/package.json index b00928f63c..31fd441fd3 100644 --- a/metapackages/auto-instrumentations-node/package.json +++ b/metapackages/auto-instrumentations-node/package.json @@ -78,6 +78,7 @@ "@opentelemetry/instrumentation-mysql2": "^0.46.0", "@opentelemetry/instrumentation-nestjs-core": "^0.46.0", "@opentelemetry/instrumentation-net": "^0.44.0", + "@opentelemetry/instrumentation-oracledb": "^0.26.0", "@opentelemetry/instrumentation-pg": "^0.52.0", "@opentelemetry/instrumentation-pino": "^0.47.0", "@opentelemetry/instrumentation-redis": "^0.47.0", diff --git a/metapackages/auto-instrumentations-node/src/utils.ts b/metapackages/auto-instrumentations-node/src/utils.ts index b65193d9d9..82d552695a 100644 --- a/metapackages/auto-instrumentations-node/src/utils.ts +++ b/metapackages/auto-instrumentations-node/src/utils.ts @@ -46,6 +46,7 @@ import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; import { NetInstrumentation } from '@opentelemetry/instrumentation-net'; +import { OracleInstrumentation } from '@opentelemetry/instrumentation-oracledb'; import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; import { PinoInstrumentation } from '@opentelemetry/instrumentation-pino'; import { RedisInstrumentation as RedisInstrumentationV2 } from '@opentelemetry/instrumentation-redis'; @@ -124,6 +125,7 @@ const InstrumentationMap = { '@opentelemetry/instrumentation-mysql': MySQLInstrumentation, '@opentelemetry/instrumentation-nestjs-core': NestInstrumentation, '@opentelemetry/instrumentation-net': NetInstrumentation, + '@opentelemetry/instrumentation-oracledb': OracleInstrumentation, '@opentelemetry/instrumentation-pg': PgInstrumentation, '@opentelemetry/instrumentation-pino': PinoInstrumentation, '@opentelemetry/instrumentation-redis': RedisInstrumentationV2, diff --git a/package-lock.json b/package-lock.json index eeb32d5adb..a031d3c9d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -482,6 +482,7 @@ "@opentelemetry/instrumentation-mysql2": "^0.46.0", "@opentelemetry/instrumentation-nestjs-core": "^0.46.0", "@opentelemetry/instrumentation-net": "^0.44.0", + "@opentelemetry/instrumentation-oracledb": "^0.26.0", "@opentelemetry/instrumentation-pg": "^0.52.0", "@opentelemetry/instrumentation-pino": "^0.47.0", "@opentelemetry/instrumentation-redis": "^0.47.0", @@ -8429,6 +8430,10 @@ "resolved": "plugins/node/opentelemetry-instrumentation-net", "link": true }, + "node_modules/@opentelemetry/instrumentation-oracledb": { + "resolved": "plugins/node/opentelemetry-instrumentation-oracledb", + "link": true + }, "node_modules/@opentelemetry/instrumentation-pg": { "resolved": "plugins/node/opentelemetry-instrumentation-pg", "link": true @@ -10873,6 +10878,13 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/oracledb": { + "version": "6.5.2", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -26210,6 +26222,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oracledb": { + "version": "6.7.0", + "dev": true, + "hasInstallScript": true, + "license": "(Apache-2.0 OR UPL-1.0)", + "engines": { + "node": ">=14.6" + } + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -36666,6 +36687,71 @@ "node": ">=12.20" } }, + "plugins/node/opentelemetry-instrumentation-oracledb": { + "name": "@opentelemetry/instrumentation-oracledb", + "version": "0.26.0", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/oracledb": "6.5.2" + }, + "devDependencies": { + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/context-async-hooks": "^2.0.0", + "@opentelemetry/contrib-test-utils": "^0.46.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/sdk-trace-node": "^2.0.0", + "@types/mocha": "10.0.10", + "@types/node": "18.18.14", + "@types/sinon": "17.0.4", + "cross-env": "7.0.3", + "nyc": "15.1.0", + "oracledb": "^6.7.0", + "rimraf": "5.0.10", + "safe-stable-stringify": "^2.4.1", + "sinon": "15.2.0", + "test-all-versions": "6.1.0", + "typescript": "5.0.4" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "plugins/node/opentelemetry-instrumentation-oracledb/node_modules/@types/node": { + "version": "18.18.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.14.tgz", + "integrity": "sha512-iSOeNeXYNYNLLOMDSVPvIFojclvMZ/HDY2dU17kUlcsOsSQETbWIslJbYLZgA+ox8g2XQwSHKTkght1a5X26lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "plugins/node/opentelemetry-instrumentation-oracledb/node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, + "plugins/node/opentelemetry-instrumentation-oracledb/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "plugins/node/opentelemetry-instrumentation-pg": { "name": "@opentelemetry/instrumentation-pg", "version": "0.52.0", @@ -43458,6 +43544,7 @@ "@opentelemetry/instrumentation-mysql2": "^0.46.0", "@opentelemetry/instrumentation-nestjs-core": "^0.46.0", "@opentelemetry/instrumentation-net": "^0.44.0", + "@opentelemetry/instrumentation-oracledb": "^0.26.0", "@opentelemetry/instrumentation-pg": "^0.52.0", "@opentelemetry/instrumentation-pino": "^0.47.0", "@opentelemetry/instrumentation-redis": "^0.47.0", @@ -45441,6 +45528,47 @@ } } }, + "@opentelemetry/instrumentation-oracledb": { + "version": "file:plugins/node/opentelemetry-instrumentation-oracledb", + "requires": { + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/context-async-hooks": "^2.0.0", + "@opentelemetry/contrib-test-utils": "^0.46.0", + "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/sdk-trace-node": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mocha": "10.0.10", + "@types/node": "18.18.14", + "@types/oracledb": "6.5.2", + "@types/sinon": "17.0.4", + "cross-env": "7.0.3", + "nyc": "15.1.0", + "oracledb": "^6.7.0", + "rimraf": "5.0.10", + "safe-stable-stringify": "^2.4.1", + "sinon": "15.2.0", + "test-all-versions": "6.1.0", + "typescript": "5.0.4" + }, + "dependencies": { + "@types/node": { + "version": "18.18.14", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "typescript": { + "version": "5.0.4", + "dev": true + }, + "undici-types": { + "version": "5.26.5", + "dev": true + } + } + }, "@opentelemetry/instrumentation-pg": { "version": "file:plugins/node/opentelemetry-instrumentation-pg", "requires": { @@ -48855,6 +48983,12 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "@types/oracledb": { + "version": "6.5.2", + "requires": { + "@types/node": "*" + } + }, "@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -60782,6 +60916,10 @@ "wcwidth": "^1.0.1" } }, + "oracledb": { + "version": "6.7.0", + "dev": true + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", diff --git a/packages/opentelemetry-test-utils/src/test-utils.ts b/packages/opentelemetry-test-utils/src/test-utils.ts index a64c9ad56a..85be91fa1f 100644 --- a/packages/opentelemetry-test-utils/src/test-utils.ts +++ b/packages/opentelemetry-test-utils/src/test-utils.ts @@ -42,6 +42,8 @@ const dockerRunCmds = { 'docker run --rm -d --name otel-mssql -p 1433:1433 -e MSSQL_SA_PASSWORD=mssql_passw0rd -e ACCEPT_EULA=Y mcr.microsoft.com/mssql/server:2022-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 --log_output=TABLE --general_log=ON', + oracledb: + 'docker run --rm -d --name otel-oracledb -p 1521:1521 -e ORACLE_PASSWORD=oracle -e APP_USER=otel -e APP_USER_PASSWORD=secret gvenzl/oracle-free:slim', postgres: 'docker run --rm -d --name otel-postgres -p 54320:5432 -e POSTGRES_PASSWORD=postgres postgres:16-alpine', redis: 'docker run --rm -d --name otel-redis -p 63790:6379 redis:alpine', diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/.eslintignore b/plugins/node/opentelemetry-instrumentation-oracledb/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/.eslintrc.js b/plugins/node/opentelemetry-instrumentation-oracledb/.eslintrc.js new file mode 100644 index 0000000000..0116c0c123 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/.eslintrc.js @@ -0,0 +1,36 @@ +const parentConfig = require('../../../eslint.config.js'); + +module.exports = { + extends: '../../../eslint.config.js', // Extends the top-level config + ignorePatterns: [ + ...(parentConfig.ignorePatterns || []), // Retain parent's ignorePatterns array + 'src/version.ts', // ignore this file + ], + env: { + mocha: true, + node: true + }, + rules: { + "header/header": ["error", "block", [{ + pattern: /\* Copyright The OpenTelemetry Authors(?:, [^,\r\n]*)?[\r\n]+ \*[\r\n]+ \* Licensed under the Apache License, Version 2\.0 \(the "License"\);[\r\n]+ \* you may not use this file except in compliance with the License\.[\r\n]+ \* You may obtain a copy of the License at[\r\n]+ \*[\r\n]+ \* {6}https:\/\/www\.apache\.org\/licenses\/LICENSE-2\.0[\r\n]+ \*[\r\n]+ \* Unless required by applicable law or agreed to in writing, software[\r\n]+ \* distributed under the License is distributed on an "AS IS" BASIS,[\r\n]+ \* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied\.[\r\n]+ \* See the License for the specific language governing permissions and[\r\n]+ \* limitations under the License\.[\r\n]+ \*[\r\n]+ \* Copyright \(c\) 2025, Oracle and\/or its affiliates\.[\r\n]+ \*/gm, + template: ` + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * ` + }]] + } +} + diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/.tav.yml b/plugins/node/opentelemetry-instrumentation-oracledb/.tav.yml new file mode 100644 index 0000000000..07ed776993 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/.tav.yml @@ -0,0 +1,4 @@ +oracledb: + # a sample from supported versions + - versions: ">=6.7.0 <7" + commands: npm run test diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/LICENSE b/plugins/node/opentelemetry-instrumentation-oracledb/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/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-oracledb/NOTICE.txt b/plugins/node/opentelemetry-instrumentation-oracledb/NOTICE.txt new file mode 100644 index 0000000000..5c7da7c546 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/NOTICE.txt @@ -0,0 +1 @@ +Copyright (c) 2025, Oracle and/or its affiliates. diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/README.md b/plugins/node/opentelemetry-instrumentation-oracledb/README.md new file mode 100644 index 0000000000..e4c453249b --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/README.md @@ -0,0 +1,73 @@ +# OpenTelemetry oracledb Instrumentation for Node.js + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +This module provides automatic instrumentation for the [`oracledb`](https://www.npmjs.com/package/oracledb) module, which may be loaded using the [`@opentelemetry/sdk-trace-node`](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node) package and is included in the [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) bundle. + +If total installation size is not constrained, it is recommended to use the [`@opentelemetry/auto-instrumentations-node`](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node) bundle with [@opentelemetry/sdk-node](`https://www.npmjs.com/package/@opentelemetry/sdk-node`) for the most seamless instrumentation experience. + +Compatible with OpenTelemetry JS API and SDK `1.0+`. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-oracledb +``` + +## Supported Versions + +- [`oracledb`](https://www.npmjs.com/package/oracledb) versions `>=6.7.0 <7` + +## Usage + +OpenTelemetry OracleInstrumentation 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 [oracledb](https://www.npmjs.com/package/oracledb). This module works with both Thin and Thick modes of the oracledb +package, although there may be some caveats with Thick Mode now, which are listed in a later paragraph. + +To load a specific plugin (**OracleInstrumentation** in this case), specify it in the configuration of the registerInstrumentations object. + +```js +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { OracleInstrumentation } = require('@opentelemetry/instrumentation-oracledb'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new OracleInstrumentation(), + ], +}) +``` + +Caveats with ``oracledb`` Thick mode: + +- RoundTrip Spans will not appear for Thick Mode +- Hostname will not be available in Thick Mode + +### Oracle Instrumentation Options + +| Options | Type | Default | Description | +| ------- | ---- | --------| ----------- | +| `enhancedDatabaseReporting` | `boolean` | `false` | If true, details about the sql statement's bind values (being set on parameters ``db.operation.parameter.``) and the sql string (being set on parameter ``db.query.text``) will be attached to the spans generated | +| `dbStatementDump` | `boolean` | `false` | If true, ``db.query.text`` will contain the sql string in the spans generated | +| `requestHook` | `OracleInstrumentationExecutionRequestHook` (function) | | Function for adding custom span attributes using information about the data for the sql statement being executed | +| `responseHook` | `OracleInstrumentationExecutionResponseHook` (function) | | Function for adding custom span attributes from the db response | +| `requireParentSpan` | `boolean` | `false` | If true, requires a parent span to create new spans | + +## Useful links + +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us in [GitHub Discussions][discussions-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[discussions-url]: https://github.com/open-telemetry/opentelemetry-js/discussions +[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 +[npm-url]: https://www.npmjs.com/package/@opentelemetry/instrumentation-oracledb +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-oracledb.svg diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/package.json b/plugins/node/opentelemetry-instrumentation-oracledb/package.json new file mode 100644 index 0000000000..4d55501a28 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/package.json @@ -0,0 +1,74 @@ +{ + "name": "@opentelemetry/instrumentation-oracledb", + "version": "0.26.0", + "description": "OpenTelemetry instrumentation for `oracledb` database client for Oracle DB", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js-contrib", + "scripts": { + "clean": "rimraf build/*", + "compile": "tsc -p .", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "lint:readme": "node ../../../scripts/lint-readme.js", + "prewatch": "npm run precompile", + "prepublishOnly": "npm run compile", + "tdd": "npm run test -- --watch-extensions ts --watch", + "test": "nyc mocha 'test/**/*.test.ts'", + "test-all-versions": "tav", + "test-all-versions:local": "cross-env RUN_ORACLEDB_TESTS_LOCAL=true npm run test-all-versions", + "test:debug": "mocha --inspect-brk --no-timeouts 'test/**/*.test.ts'", + "test:local": "cross-env RUN_ORACLEDB_TESTS_LOCAL=true npm run test", + "version:update": "node ../../../scripts/version-update.js", + "watch": "tsc -w" + }, + "keywords": [ + "instrumentation", + "nodejs", + "opentelemetry", + "plugin", + "oracledb", + "profiling", + "tracing" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.js.map", + "build/src/**/*.d.ts" + ], + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "devDependencies": { + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/context-async-hooks": "^2.0.0", + "@opentelemetry/contrib-test-utils": "^0.46.0", + "@opentelemetry/sdk-trace-base": "^2.0.0", + "@opentelemetry/sdk-trace-node": "^2.0.0", + "@types/mocha": "10.0.10", + "@types/node": "18.18.14", + "@types/sinon": "17.0.4", + "cross-env": "7.0.3", + "nyc": "17.1.0", + "oracledb": "^6.7.0", + "rimraf": "5.0.10", + "safe-stable-stringify": "^2.4.1", + "sinon": "15.2.0", + "test-all-versions": "6.1.0", + "typescript": "5.0.4" + }, + "dependencies": { + "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/oracledb": "6.5.2" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-oracledb#readme" +} diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/src/OracleTelemetryTraceHandler.ts b/plugins/node/opentelemetry-instrumentation-oracledb/src/OracleTelemetryTraceHandler.ts new file mode 100644 index 0000000000..b4b2cb22a0 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/src/OracleTelemetryTraceHandler.ts @@ -0,0 +1,423 @@ +/* + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * */ +import { safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; +import { + Span, + SpanStatusCode, + Tracer, + context, + SpanKind, + trace, + diag, +} from '@opentelemetry/api'; +import { + ATTR_SERVER_PORT, + ATTR_SERVER_ADDRESS, + ATTR_NETWORK_TRANSPORT, +} from '@opentelemetry/semantic-conventions'; +import { + ATTR_DB_SYSTEM, + ATTR_DB_NAMESPACE, + ATTR_DB_OPERATION_NAME, + ATTR_DB_STATEMENT, + ATTR_DB_OPERATION_PARAMETER, + ATTR_DB_USER, +} from './semconv'; + +import type * as oracleDBTypes from 'oracledb'; +type TraceHandlerBaseCtor = new () => any; +const OUT_BIND = 3003; // bindinfo direction value. + +// Local modules. +import { OracleInstrumentationConfig, SpanConnectionConfig } from './types'; +import { TraceSpanData, SpanCallLevelConfig } from './internal-types'; +import { SpanNames, DB_SYSTEM_VALUE_ORACLE } from './constants'; + +// It dynamically retrieves the TraceHandlerBase class from the oracledb module +// (if available) while avoiding direct imports that could cause issues if +// the module is missing. +function getTraceHandlerBaseClass( + obj: typeof oracleDBTypes +): TraceHandlerBaseCtor | null { + try { + return (obj as any).traceHandler.TraceHandlerBase as TraceHandlerBaseCtor; + } catch (err) { + diag.error('Failed to load oracledb module.', err); + return null; + } +} + +export function getOracleTelemetryTraceHandlerClass( + obj: typeof oracleDBTypes +): any { + const traceHandlerBase = getTraceHandlerBaseClass(obj); + if (!traceHandlerBase) { + return undefined; + } + + /** + * OracleTelemetryTraceHandler extends TraceHandlerBase from oracledb module + * It implements the abstract methods; `onEnterFn`, `onExitFn`, + * `onBeginRoundTrip` and `onEndRoundTrip` of TraceHandlerBase class. + * Inside these overridden methods, the input traceContext data is used + * to generate attributes for span. + */ + class OracleTelemetryTraceHandler extends traceHandlerBase { + private _getTracer: () => Tracer; + private _instrumentConfig: OracleInstrumentationConfig; + + constructor(getTracer: () => Tracer, config: OracleInstrumentationConfig) { + super(); + this._getTracer = getTracer; + this._instrumentConfig = config; + } + + private _shouldSkipInstrumentation() { + return ( + this._instrumentConfig.requireParentSpan === true && + trace.getSpan(context.active()) === undefined + ); + } + + // It returns db.namespace as mentioned in semantic conventions + // Ex: ORCL1|PDB1|db_high.adb.oraclecloud.com + private _getDBNameSpace( + instanceName?: string, + pdbName?: string, + serviceName?: string + ): string | undefined { + if (instanceName == null && pdbName == null && serviceName == null) { + return undefined; + } + return `${instanceName ?? ''}|${pdbName ?? ''}|${serviceName ?? ''}`; + } + + // Returns the connection related Attributes for + // semantic standards and module custom keys. + private _getConnectionSpanAttributes(config: SpanConnectionConfig) { + return { + [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_ORACLE, + [ATTR_NETWORK_TRANSPORT]: config.protocol, + [ATTR_DB_USER]: config.user, + [ATTR_DB_NAMESPACE]: this._getDBNameSpace( + config.instanceName, + config.pdbName, + config.serviceName + ), + [ATTR_SERVER_ADDRESS]: config.hostName, + [ATTR_SERVER_PORT]: config.port, + }; + } + + // It returns true if object is of type oracledb.Lob. + private _isLobInstance(obj: unknown): boolean { + return ( + typeof obj === 'object' && + obj !== null && + Reflect.getPrototypeOf(obj)?.constructor?.name === 'Lob' + ); + } + + // Transforms the bind values array or bindinfo into an object + // 'db.operation.parameter'. + // Ex: + // db.operation.parameter.0 = "someval" // for bind by position + // db.operation.parameter.name = "someval" // for bind by name + // It is only called if config 'enhancedDatabaseReporting' is true. + private _getValues(values: any) { + if (!values) return undefined; + const convertedValues: Record = {}; + + try { + if (Array.isArray(values)) { + // Handle indexed (positional) parameters + values.forEach((value, index) => { + const key = `${ATTR_DB_OPERATION_PARAMETER}.${index}`; + const extractedValue = this._extractValue(value); + if (extractedValue !== undefined) { + convertedValues[key] = extractedValue; + } + }); + } else if (values && typeof values === 'object') { + // Handle named parameters + for (const [paramName, value] of Object.entries(values)) { + const key = `${ATTR_DB_OPERATION_PARAMETER}.${paramName}`; + let inVal: any = value; + + if (inVal && typeof inVal === 'object') { + // Check bind info if present. + if (inVal.dir === OUT_BIND) { + // outbinds + convertedValues[key] = ''; + continue; + } + if ('val' in inVal) { + inVal = inVal.val; + } + } + const extractedValue = this._extractValue(inVal); + if (extractedValue !== undefined) { + convertedValues[key] = extractedValue; + } + } + } + } catch (e) { + diag.error('failed to stringify bind values:', values, e); + return undefined; + } + return convertedValues; + } + + private _extractValue(value: any): string | undefined { + if (value == null) { + return 'null'; + } + if (value instanceof Buffer || this._isLobInstance(value)) { + return value.toString(); + } + if (typeof value === 'object') { + return JSON.stringify(value); + } + return value.toString(); + } + + // Updates the call level attributes in span. + // roundTrip flag will skip dumping bind values for + // internal roundtrip spans generated for oracledb exported functions. + private _setCallLevelAttributes( + span: Span, + callConfig?: SpanCallLevelConfig, + roundTrip = false + ) { + if (!callConfig) return; + + if (callConfig.statement) { + span.setAttribute( + ATTR_DB_OPERATION_NAME, + // retrieve just the first word + callConfig.statement.split(' ')[0].toUpperCase() + ); + if ( + this._instrumentConfig.dbStatementDump || + this._instrumentConfig.enhancedDatabaseReporting + ) { + span.setAttribute(ATTR_DB_STATEMENT, callConfig.statement); + if (this._instrumentConfig.enhancedDatabaseReporting && !roundTrip) { + const values = this._getValues(callConfig.values); + if (values) { + span.setAttributes(values); + } + } + } + } + } + + private _handleExecuteCustomRequest( + span: Span, + traceContext: TraceSpanData + ) { + if (typeof this._instrumentConfig.requestHook === 'function') { + safeExecuteInTheMiddle( + () => { + this._instrumentConfig.requestHook?.(span, { + connection: traceContext.connectLevelConfig, + inputArgs: traceContext.additionalConfig.args, + }); + }, + err => { + if (err) { + diag.error('Error running request hook', err); + } + }, + true + ); + } + } + + private _handleExecuteCustomResult( + span: Span, + traceContext: TraceSpanData + ) { + if (typeof this._instrumentConfig.responseHook === 'function') { + safeExecuteInTheMiddle( + () => { + this._instrumentConfig.responseHook?.(span, { + data: traceContext.additionalConfig.result, + }); + }, + err => { + if (err) { + diag.error('Error running query hook', err); + } + }, + true + ); + } + } + + // Updates the spanName following the format + // {FunctionName:[sqlCommand] db.namespace} + // Ex: 'oracledb.Pool.getConnection:[SELECT] ORCL1|PDB1|db_high.adb.oraclecloud.com' + // This function is called when connectLevelConfig has required parameters populated. + private _updateSpanName(traceContext: TraceSpanData) { + const { connectLevelConfig, callLevelConfig, userContext, operation } = + traceContext; + if ( + ![ + SpanNames.EXECUTE, + SpanNames.EXECUTE_MANY, + SpanNames.EXECUTE_MSG, + ].includes(operation as SpanNames) + ) { + // Ignore for connection establishment functions. + return; + } + + const { instanceName, pdbName, serviceName } = connectLevelConfig; + const dbName = this._getDBNameSpace(instanceName, pdbName, serviceName); + const sqlCommand = + callLevelConfig?.statement?.split(' ')[0].toUpperCase() || ''; + userContext.span.updateName( + `${operation}:${sqlCommand}${dbName && ` ${dbName}`}` + ); + } + + // Updates the span with final traceContext attributes + // which are updated after the exported function call. + // roundTrip flag will skip dumping bind values for + // internal roundtrip spans generated for exported functions. + private _updateFinalSpanAttributes( + traceContext: TraceSpanData, + roundTrip = false + ) { + const span = traceContext.userContext.span; + // Set if additional connection and call parameters + // are available + if (traceContext.connectLevelConfig) { + span.setAttributes( + this._getConnectionSpanAttributes(traceContext.connectLevelConfig) + ); + } + if (traceContext.callLevelConfig) { + this._setCallLevelAttributes( + span, + traceContext.callLevelConfig, + roundTrip + ); + } + if (traceContext.error) { + span.recordException(traceContext.error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: traceContext.error.message, + }); + } + } + + setInstrumentConfig(config: OracleInstrumentationConfig = {}) { + this._instrumentConfig = config; + } + + // This method is invoked before calling an exported function + // from oracledb module. + onEnterFn(traceContext: TraceSpanData) { + if (this._shouldSkipInstrumentation()) { + return; + } + + const spanName = traceContext.operation; + const spanAttributes = traceContext.connectLevelConfig + ? this._getConnectionSpanAttributes(traceContext.connectLevelConfig) + : {}; + + traceContext.userContext = { + span: this._getTracer().startSpan(spanName, { + kind: SpanKind.CLIENT, + attributes: spanAttributes, + }), + }; + + if (traceContext.fn) { + // wrap the active span context to the exported function. + traceContext.fn = context.bind( + trace.setSpan(context.active(), traceContext.userContext.span), + traceContext.fn + ); + } + + if (traceContext.operation === SpanNames.EXECUTE) { + this._handleExecuteCustomRequest( + traceContext.userContext.span, + traceContext + ); + } + } + + // This method is invoked after exported function from oracledb module + // completes. + onExitFn(traceContext: TraceSpanData) { + if (!traceContext.userContext?.span) { + return; + } + this._updateFinalSpanAttributes(traceContext); + switch (traceContext.operation) { + case SpanNames.EXECUTE: + this._handleExecuteCustomResult( + traceContext.userContext.span, + traceContext + ); + break; + default: + break; + } + this._updateSpanName(traceContext); + traceContext.userContext.span.end(); + } + + // This method is invoked before a round trip call to DB is done + // from the oracledb module as part of sql execution. + onBeginRoundTrip(traceContext: TraceSpanData) { + if (this._shouldSkipInstrumentation()) { + return; + } + const spanName = traceContext.operation; + const spanAttrs = {}; + traceContext.userContext = { + span: this._getTracer().startSpan(spanName, { + kind: SpanKind.CLIENT, + attributes: spanAttrs, + }), + }; + } + + // This method is invoked after a round trip call to DB is done + // from the oracledb module as part of sql execution. + onEndRoundTrip(traceContext: TraceSpanData) { + if (!traceContext.userContext?.span) { + return; + } + + // Set if additional connection and call parameters + // are available + this._updateFinalSpanAttributes(traceContext, true); + this._updateSpanName(traceContext); + traceContext.userContext.span.end(); + } + } + return OracleTelemetryTraceHandler; +} diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/src/constants.ts b/plugins/node/opentelemetry-instrumentation-oracledb/src/constants.ts new file mode 100644 index 0000000000..f20f3fd73e --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/src/constants.ts @@ -0,0 +1,47 @@ +/* + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * */ + +// Contains span names produced by instrumentation +// It lists the RPC names (suffix with _MSG like EXECUTE_MSG) and +// exported oracledb functions (like EXECUTE). +// These constants need to be in sync with what is generated by the +// 'oracledb' module. +export enum SpanNames { + CONNECT = 'oracledb.getConnection', + POOL_CONNECT = 'oracledb.Pool.getConnection', + POOL_CREATE = 'oracledb.createPool', + CONNECT_PROTOCOL_NEG = 'oracledb.ProtocolMessage', + CONNECT_DATATYPE_NEG = 'oracledb.DataTypeMessage', + CONNECT_AUTH_MSG = 'oracledb.AuthMessage', + CONNECT_FAST_AUTH = 'oracledb.FastAuthMessage', + EXECUTE_MSG = 'oracledb.ExecuteMessage', + EXECUTE = 'oracledb.Connection.execute', + EXECUTE_MANY = 'oracledb.Connection.executeMany', + LOGOFF_MSG = 'oracledb.LogOffMessage', + CONNECT_CLOSE = 'oracledb.Connection.close', + CREATE_LOB = 'oracledb.Connection.createLob', + LOB_MESSAGE = 'oracledb.LobOpMessage', + LOB_GETDATA = 'oracledb.Lob.getData', +} + +/* + * The semantic conventions defined DBSYSTEMVALUES_ORACLE as oracle, hence + * defining the new constant to explicitly mention db. + * + */ +export const DB_SYSTEM_VALUE_ORACLE = 'oracle.db'; diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/src/index.ts b/plugins/node/opentelemetry-instrumentation-oracledb/src/index.ts new file mode 100644 index 0000000000..198b2cd839 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/src/index.ts @@ -0,0 +1,20 @@ +/* + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * */ + +export * from './instrumentation'; +export * from './types'; diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-oracledb/src/instrumentation.ts new file mode 100644 index 0000000000..103f143a91 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/src/instrumentation.ts @@ -0,0 +1,77 @@ +/* + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * */ +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, +} from '@opentelemetry/instrumentation'; +import type * as oracleDBTypes from 'oracledb'; +import { OracleInstrumentationConfig } from './types'; +import { getOracleTelemetryTraceHandlerClass } from './OracleTelemetryTraceHandler'; +/** @knipignore */ +import { PACKAGE_NAME, PACKAGE_VERSION } from './version'; + +export class OracleInstrumentation extends InstrumentationBase { + private _tmHandler: any; + + constructor(config: OracleInstrumentationConfig = {}) { + super(PACKAGE_NAME, PACKAGE_VERSION, config); + } + + protected init() { + const moduleOracleDB = new InstrumentationNodeModuleDefinition( + 'oracledb', + ['>= 6.7 < 7'], + (moduleExports: typeof oracleDBTypes) => { + if (!moduleExports) { + return; + } + if (this._tmHandler) { + // Already registered, so unregister it. + (moduleExports as any).traceHandler.setTraceInstance(); + this._tmHandler = null; + } + const config = this.getConfig(); + const thClass = getOracleTelemetryTraceHandlerClass(moduleExports); + if (thClass) { + const obj = new thClass(() => this.tracer, config); + obj.enable(); + + // Register the instance with oracledb. + (moduleExports as any).traceHandler.setTraceInstance(obj); + this._tmHandler = obj; + } + return moduleExports; + }, + moduleExports => { + if (this._tmHandler) { + (moduleExports as any).traceHandler.setTraceInstance(); + this._tmHandler = null; + } + } + ); + + return [moduleOracleDB]; + } + + override setConfig(config: OracleInstrumentationConfig = {}) { + super.setConfig(config); + + // update the config in OracleTelemetryTraceHandler obj. + this._tmHandler?.setInstrumentConfig(this._config); + } +} diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/src/internal-types.ts b/plugins/node/opentelemetry-instrumentation-oracledb/src/internal-types.ts new file mode 100644 index 0000000000..0f33eed7d8 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/src/internal-types.ts @@ -0,0 +1,54 @@ +/* + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * */ + +import type * as oracledbTypes from 'oracledb'; +import type * as api from '@opentelemetry/api'; +import { SpanConnectionConfig } from './types'; + +// onEnterFn returns this Context(contains only span for now) and it is +// received in onExitFn to end the span. +export interface InstrumentationContext { + span: api.Span; +} + +// Captures the entire span data. +// This corresponds to js object filled by the 'oracledb' module +// See: https://github.com/oracle/node-oracledb/blob/main/lib/traceHandler.js +export interface TraceSpanData { + operation: string; // RPC or exported function name. + error?: oracledbTypes.DBError; + connectLevelConfig: SpanConnectionConfig; + callLevelConfig?: SpanCallLevelConfig; + additionalConfig?: any; // custom key/values associated with a function. + fn: Function; // Replaced with bind function associating the active context. + args?: any[]; // input arguments passed to the exported function. + + /** + * This value is filled by instrumented module inside 'onEnterFn', + * 'onBeginRoundTrip' hook functions, which is passed back by oracledb module + * in 'onExitFn' and 'onEndRoundTrip' hook functions respectively. + */ + userContext: InstrumentationContext; +} + +// Captures call level related span data +export interface SpanCallLevelConfig { + statement?: string; // SQL stmt. + operation?: string; // SQL op ('SELECT | INSERT ..'). + values?: any[]; // bind values. +} diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/src/semconv.ts b/plugins/node/opentelemetry-instrumentation-oracledb/src/semconv.ts new file mode 100644 index 0000000000..3f91994c1d --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/src/semconv.ts @@ -0,0 +1,101 @@ +/* + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * */ + +/** + * The database management system (DBMS) product as identified + * by the client instrumentation. + * + */ +export const ATTR_DB_SYSTEM = 'db.system.name'; + +/** + * The database associated with the connection, qualified by the instance name, database name and service name. + * + * @example ORCL1|PDB1|db_high.adb.oraclecloud.com + * @example ORCL1|DB1|db_low.adb.oraclecloud.com + * + * @note It **SHOULD** be set to the combination of instance name, database name and + * service name following the `{instance_name}|{database_name}|{service_name}` pattern. + * For CDB architecture, database name would be pdb name. For Non-CDB, it would be + * **DB_NAME** parameter. + * This attribute has stability level RELEASE CANDIDATE. + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_DB_NAMESPACE = 'db.namespace'; + +/** + * The name of the operation or command being executed. + * + * @example INSERT + * @example COMMIT + * @example SELECT + * + * @note It is **RECOMMENDED** to capture the value as provided by the application without attempting to do any case normalization. + * If the operation name is parsed from the query text, it **SHOULD** be the first operation name found in the query. + * For batch operations, if the individual operations are known to have the same operation name then that operation name **SHOULD** be used prepended by `BATCH `, otherwise `db.operation.name` **SHOULD** be `BATCH` or some other database system specific term if more applicable. + * This attribute has stability level RELEASE CANDIDATE. + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_DB_OPERATION_NAME = 'db.operation.name'; + +/** + * The database query being executed. + * + * @example SELECT * FROM wuser_table where username = :1 // bind by position + * @example SELECT * FROM wuser_table where username = :name // bind by name + * @example SELECT * FROM wuser_table where username = 'John' // literals + * + * @note For sanitization see [Sanitization of `db.query.text`](../database/database-spans.md#sanitization-of-dbquerytext). + * For batch operations, if the individual operations are known to have the same query text then + * that query text **SHOULD** be used, otherwise all of the individual query texts **SHOULD** + * be concatenated with separator `; ` or some other database system specific separator if more applicable. + * + * Non-parameterized or Parameterized query text **SHOULD NOT** be collected by default unless + * explicitly configured and sanitized to exclude sensitive data, e.g. by redacting all + * literal values present in the query text. See Sanitization of `db.query.text`. + * + * Parameterized query text MUST also NOT be collected by default unless explicitly configured. + * The query parameter values themselves are opt-in, see [`db.operation.parameter.`](../attributes-registry/db.md)) + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_DB_STATEMENT = 'db.query.text'; + +/** + * A database operation parameter, with being the parameter name, + * and the attribute value being a string representation of the parameter value. + * + * @example someval + * @example 55 + * + * @note If a parameter has no name and instead is referenced only by index, then + * **SHOULD** be the 0-based index. If `db.query.text` is also captured, then + * `db.operation.parameter.` **SHOULD** match up with the parameterized placeholders + * present in db.query.text + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + */ +export const ATTR_DB_OPERATION_PARAMETER = 'db.operation.parameter'; + +/** + * Username for accessing the database. + * + */ +export const ATTR_DB_USER = 'db.user'; diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/src/types.ts b/plugins/node/opentelemetry-instrumentation-oracledb/src/types.ts new file mode 100644 index 0000000000..5303ef6acd --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/src/types.ts @@ -0,0 +1,94 @@ +/* + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * */ +import type * as api from '@opentelemetry/api'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +// Captures connection related span data +export interface SpanConnectionConfig { + serviceName?: string; + connectString?: string; + hostName?: string; + port?: number; + user?: string; + protocol?: string; + instanceName?: string; + serverMode?: string; + pdbName?: string; + poolMin?: number; + poolMax?: number; + poolIncrement?: number; +} + +export interface OracleRequestHookInformation { + inputArgs: any; + connection: SpanConnectionConfig; +} + +export interface OracleInstrumentationExecutionRequestHook { + (span: api.Span, queryInfo: OracleRequestHookInformation): void; +} + +export interface OracleResponseHookInformation { + data: any; // the result of sql execution. +} + +export interface OracleInstrumentationExecutionResponseHook { + (span: api.Span, resultInfo: OracleResponseHookInformation): void; +} + +export interface OracleInstrumentationConfig extends InstrumentationConfig { + /** + * If true, an attribute containing the execute method + * bind values will be attached the spans generated. + * It can potentially record PII data and should be used with caution. + * + * @default false + */ + enhancedDatabaseReporting?: boolean; + + /** + * If true, db.statement will have sql which could potentially contain + * sensitive unparameterized data in the spans generated. + * + * @default false + */ + dbStatementDump?: boolean; + + /** + * Hook that allows adding custom span attributes or updating the + * span's name based on the data about the query to execute. + * + * @default undefined + */ + requestHook?: OracleInstrumentationExecutionRequestHook; + + /** + * Hook that allows adding custom span attributes based on the data + * returned from "execute" actions. + * + * @default undefined + */ + responseHook?: OracleInstrumentationExecutionResponseHook; + + /** + * If true, requires a parent span to create new spans. + * + * @default false + */ + requireParentSpan?: boolean; +} diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/test/oracle.test.ts b/plugins/node/opentelemetry-instrumentation-oracledb/test/oracle.test.ts new file mode 100644 index 0000000000..36c2e58f1d --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/test/oracle.test.ts @@ -0,0 +1,1675 @@ +/* + * 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. + * + * Copyright (c) 2025, Oracle and/or its affiliates. + * */ +import { + Attributes, + SpanStatusCode, + context, + Span, + SpanKind, + SpanStatus, + trace, +} from '@opentelemetry/api'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import * as testUtils from '@opentelemetry/contrib-test-utils'; +import { + BasicTracerProvider, + InMemorySpanExporter, + SimpleSpanProcessor, + ReadableSpan, + TimedEvent, +} from '@opentelemetry/sdk-trace-base'; +import * as assert from 'assert'; +import { OracleInstrumentation } from '../src'; +import { SpanNames, DB_SYSTEM_VALUE_ORACLE } from '../src/constants'; + +import { + ATTR_NETWORK_TRANSPORT, + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, + ATTR_EXCEPTION_MESSAGE, + ATTR_EXCEPTION_STACKTRACE, + ATTR_EXCEPTION_TYPE, +} from '@opentelemetry/semantic-conventions'; + +import { + ATTR_DB_NAMESPACE, + ATTR_DB_SYSTEM, + ATTR_DB_STATEMENT, + ATTR_DB_OPERATION_PARAMETER, + ATTR_DB_USER, + ATTR_DB_OPERATION_NAME, +} from '../src/semconv'; + +const memoryExporter = new InMemorySpanExporter(); +let contextManager: AsyncHooksContextManager; +const provider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(memoryExporter)], +}); +const tracer = provider.getTracer('external'); +const instrumentation = new OracleInstrumentation(); +instrumentation.enable(); +instrumentation.disable(); + +import * as oracledb from 'oracledb'; + +const VER_23_4 = 2304000000; +const hostname = 'localhost'; +const pno = 1521; +const serviceName = 'FREEPDB1'; +let serverVersion = 2304000000; // DB version. +let numExecSpans = 2; // Default number of Spans created for Execute API in thin mode. +let numConnSpans = 2; // Default number of spans created during connection establishment. +let poolMinSpanCount = 1; // number of spans created for createPool considering poolMin. +const CONFIG = { + user: process.env.ORACLE_USER || 'demo', + password: process.env.ORACLE_PASSWORD || 'demo', + connectString: process.env.ORACLE_CONNECTSTRING || 'localhost:1521/freepdb1', +}; +const POOL_CONFIG = { + ...CONFIG, + poolMin: 2, + poolMax: 10, + poolIncrement: 1, + poolTimeout: 28, + stmtCacheSize: 23, +}; + +// span attributes for execute method not including binds. +let executeAttributes: Record; +// span attributes for internal round trips with binds sql. +let executeAttributesInternalRoundTripBinds: Record; +// span attributes when enhancedDatabaseReporting is enabled for sql with no binds +let attributesWithSensitiveDataNoBinds: Record< + string, + string | number | string[] +>; +// span attributes when enhancedDatabaseReporting is enabled for sql with +// bind by position. +let attributesWithSensitiveDataBinds: Record< + string, + string | number | string[] +>; +// span attributes when enhancedDatabaseReporting is enabled for sql with +// bind by name. +let attributesWithSensitiveDataBindsByName: Record< + string, + string | number | string[] +>; +let connAttributes: Record; // connection related span attributes. +let poolAttributes: Record; // pool related span attributes. +let connAttrList: Record[]; // attributes per span during connection establishment. +let spanNameSuffix: string; // SpanName will be +let failedConnAttrList: Record[]; // attributes in span for failed connection. +let poolConnAttrList: Record[]; // attributes per span when connection established from pool. +let spanNamesList: string[]; // span names for roundtrips and public API spans. + +const DEFAULT_ATTRIBUTES = { + [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_ORACLE, + [ATTR_DB_NAMESPACE]: serviceName, + [ATTR_SERVER_ADDRESS]: hostname, + [ATTR_SERVER_PORT]: pno, + [ATTR_DB_USER]: CONFIG.user, + [ATTR_NETWORK_TRANSPORT]: 'TCP', +}; + +// for thick mode, we don't have support for +// hostname, port and protocol. +const DEFAULT_ATTRIBUTES_THICK = { + [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_ORACLE, + [ATTR_DB_NAMESPACE]: serviceName, + [ATTR_DB_USER]: CONFIG.user, +}; + +const POOL_ATTRIBUTES = { + [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_ORACLE, + [ATTR_DB_USER]: CONFIG.user, +}; + +const CONN_FAILED_ATTRIBUTES = { + [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_ORACLE, + [ATTR_DB_USER]: CONFIG.user, +}; + +const unsetStatus: SpanStatus = { + code: SpanStatusCode.UNSET, +}; + +const defaultEvents: testUtils.TimedEvent[] = []; + +if (process.env.NODE_ORACLEDB_DRIVER_MODE === 'thick') { + // Thick mode requires Oracle Client or Oracle Instant Client libraries. + // On Windows and macOS Intel you can specify the directory containing the + // libraries at runtime or before Node.js starts. On other platforms (where + // Oracle libraries are available) the system library search path must always + // include the Oracle library path before Node.js starts. If the search path + // is not correct, you will get a DPI-1047 error. See the node-oracledb + // installation documentation. + let clientOpts = {}; + // On Windows and macOS Intel platforms, set the environment + // variable NODE_ORACLEDB_CLIENT_LIB_DIR to the Oracle Client library path + if ( + process.platform === 'win32' || + (process.platform === 'darwin' && process.arch === 'x64') + ) { + clientOpts = { libDir: process.env.NODE_ORACLEDB_CLIENT_LIB_DIR }; + } + oracledb.initOracleClient(clientOpts); // enable node-oracledb Thick mode +} + +// Updates span attributes for both thick and thin modes. +// Additionally, adjusts the number of roundtrip spans based on the database version. +function updateAttrSpanList(connection: oracledb.Connection) { + serverVersion = connection.oracleServerVersion; + + let attributes: Record; + if (oracledb.thin) { + attributes = { ...DEFAULT_ATTRIBUTES }; + attributes[ATTR_SERVER_ADDRESS] = connAttributes[ATTR_SERVER_ADDRESS]; + attributes[ATTR_SERVER_PORT] = connAttributes[ATTR_SERVER_PORT]; + attributes[ATTR_NETWORK_TRANSPORT] = connAttributes[ATTR_NETWORK_TRANSPORT]; + } else { + attributes = { ...DEFAULT_ATTRIBUTES_THICK }; + numExecSpans = 1; + } + attributes[ATTR_DB_NAMESPACE] = `||${connection.serviceName}`; + + // initialize the span attributes list. + connAttrList = []; + poolConnAttrList = []; + spanNamesList = []; + failedConnAttrList = []; + spanNameSuffix = ` ${connAttributes[ATTR_DB_NAMESPACE]}`; + if (serverVersion >= VER_23_4) { + if (oracledb.thin) { + // for round trips. + connAttrList.push({ ...attributes }); + poolConnAttrList.push({ ...attributes, ...POOL_ATTRIBUTES }); + poolConnAttrList.push({ ...connAttributes, ...POOL_ATTRIBUTES }); + connAttrList.push({ ...connAttributes }); + failedConnAttrList = [...connAttrList]; + failedConnAttrList[2] = { ...CONN_FAILED_ATTRIBUTES }; + failedConnAttrList[1] = { ...attributes }; + poolMinSpanCount = POOL_CONFIG.poolMin * numConnSpans + 1; + spanNamesList.push(SpanNames.CONNECT_FAST_AUTH); + spanNamesList.push(SpanNames.CONNECT_AUTH_MSG); + } else { + failedConnAttrList = [{ ...CONN_FAILED_ATTRIBUTES }]; + } + } else { + if (oracledb.thin) { + // for round trips. + connAttrList.push({ ...attributes }); + connAttrList.push({ ...attributes }); + connAttrList.push({ ...attributes }); + connAttrList.push({ ...connAttributes }); + poolConnAttrList.push({ ...attributes, ...POOL_ATTRIBUTES }); + poolConnAttrList.push({ ...attributes, ...POOL_ATTRIBUTES }); + poolConnAttrList.push({ ...attributes, ...POOL_ATTRIBUTES }); + poolConnAttrList.push({ ...connAttributes, ...POOL_ATTRIBUTES }); + failedConnAttrList = [...connAttrList]; + failedConnAttrList[4] = { ...CONN_FAILED_ATTRIBUTES }; + failedConnAttrList[3] = { ...attributes }; + numConnSpans = 4; + poolMinSpanCount = POOL_CONFIG.poolMin * numConnSpans + 1; + spanNamesList.push(SpanNames.CONNECT_PROTOCOL_NEG); + spanNamesList.push(SpanNames.CONNECT_DATATYPE_NEG); + spanNamesList.push(SpanNames.CONNECT_AUTH_MSG); + spanNamesList.push(SpanNames.CONNECT_AUTH_MSG); + } else { + numConnSpans = 1; + failedConnAttrList = [{ ...CONN_FAILED_ATTRIBUTES }]; + } + } + // for getConnection + connAttrList.push({ ...connAttributes }); + poolConnAttrList.push({ ...poolAttributes }); + spanNamesList.push(SpanNames.CONNECT); +} + +const verifySpanData = ( + span: ReadableSpan, + parentSpan: ReadableSpan | null, + attributes: Attributes, + events: testUtils.TimedEvent[] = defaultEvents, + status = unsetStatus +) => { + testUtils.assertSpan( + span as unknown as ReadableSpan, + SpanKind.CLIENT, + attributes, + events, + status + ); + + if (parentSpan) { + testUtils.assertPropagation(span, parentSpan as unknown as Span); + } else { + assert(span.parentSpanContext?.spanId === undefined); + } +}; + +function checkRoundTripSpans( + spans: ReadableSpan[], + parentSpan: ReadableSpan | null, + attributesList: Attributes[], + eventList: TimedEvent[][] = [defaultEvents, defaultEvents], + statusList: SpanStatus[] = [unsetStatus, unsetStatus], + spanNamesList: string[] = [SpanNames.EXECUTE_MSG, SpanNames.EXECUTE] +) { + // verify roundtrip child span or public API span if no roundtrip + // span is generated. + for (let index = 0; index < spans.length - 1; index++) { + assert.deepStrictEqual(spans[index].name, spanNamesList[index]); + verifySpanData( + spans[index], + parentSpan, + attributesList[index], + eventList[index], + statusList[index] + ); + } +} + +function getDBNameSpace(instanceName = '', pdbName = '', servicename = '') { + return [instanceName, pdbName, servicename].join('|'); +} + +// It verifies the spans, its attributes and the parent child relationship. +function verifySpans( + parentSpan: Span | null, + attributesList: Attributes[] = [executeAttributes, executeAttributes], + spanNamesList: string[] = [ + SpanNames.EXECUTE_MSG + ':SELECT' + spanNameSuffix, + SpanNames.EXECUTE + ':SELECT' + spanNameSuffix, + ], + eventList: testUtils.TimedEvent[][] = [defaultEvents, defaultEvents], + statusList: SpanStatus[] = [unsetStatus, unsetStatus] +) { + if (!oracledb.thin) { + attributesList = attributesList.slice(attributesList.length - 1); + spanNamesList = spanNamesList.slice(spanNamesList.length - 1); + statusList = statusList.slice(statusList.length - 1); + } + const spans = memoryExporter.getFinishedSpans(); + let spanLength = 1; + const lastSpan = spans[spans.length - 1]; + if (oracledb.thin) { + spanLength = attributesList.length; + } + assert.strictEqual(spans.length, spanLength); + + // verify roundtrip child spans or public API span if no roundtrip + // span is generated (in case of thick). + checkRoundTripSpans( + spans, + lastSpan, + attributesList, + eventList, + statusList, + spanNamesList + ); + + //verify span generated from public API. + assert.deepStrictEqual(lastSpan.name, spanNamesList[spanLength - 1]); + verifySpanData( + lastSpan as unknown as ReadableSpan, + parentSpan as unknown as ReadableSpan, + attributesList[spanLength - 1], + eventList[spanLength - 1], + statusList[spanLength - 1] + ); +} + +function assertErrorSpan( + failedAttributes: Record, + numSpans: number, + error: Error & { code?: number } +) { + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, numSpans); + + const span = spans[spans.length - 1]; + const events = [ + { + name: 'exception', + droppedAttributesCount: 0, + attributes: { + [ATTR_EXCEPTION_STACKTRACE]: error.stack, + [ATTR_EXCEPTION_MESSAGE]: error.message, + [ATTR_EXCEPTION_TYPE]: String(error.code), + }, + time: span.events[0].time, + }, + ]; + const status = { + code: SpanStatusCode.ERROR, + message: error.message, + }; + testUtils.assertSpan(span, SpanKind.CLIENT, failedAttributes, events, status); +} + +const sqlDropTable = function (tableName: string) { + return ` + DECLARE + e_table_missing EXCEPTION; + PRAGMA EXCEPTION_INIT(e_table_missing, -942); + BEGIN + EXECUTE IMMEDIATE ('DROP TABLE ${tableName} PURGE'); + EXCEPTION + WHEN e_table_missing THEN NULL; + END; + `; +}; + +const sqlCreateTable = async function ( + conn: oracledb.Connection, + tableName: string, + sql: string +) { + const dropSql = sqlDropTable(tableName); + const plsql = ` + BEGIN + ${dropSql} + EXECUTE IMMEDIATE ('${sql} NOCOMPRESS'); + END; + `; + await conn.execute(plsql); +}; + +describe('oracledb', () => { + let connection: oracledb.Connection; + + const testOracleDB = process.env.RUN_ORACLEDB_TESTS; // For CI: assumes local oracledb is already available + const testOracleDBLocally = process.env.RUN_ORACLEDB_TESTS_LOCAL; // For local: spins up local oracledb via docker + const shouldTest = testOracleDB || testOracleDBLocally; // Skips these tests if false (default) + const sql = 'select 1 from dual'; + const sqlWithBinds = 'select :1 from dual'; + const sqlWithBindsByName = 'select :name from dual'; + const sqlWithOutBinds = 'begin :n := 1001; end;'; + const outBinds = { n: { dir: oracledb.BIND_OUT } }; + const binds = ['0']; + const bindsByName = { + name: { val: '0', type: oracledb.STRING, dir: oracledb.BIND_IN }, + }; + const tableName = 'oracledb_ot_execute_test'; + const sqlCreate = `create table ${tableName} (id NUMBER, val VARCHAR2(100), clobval CLOB)`; + + async function doSetup() { + const extendedConn: any = connection; + let dbName; + + if (oracledb.thin) { + connAttributes = { ...DEFAULT_ATTRIBUTES }; + } else { + connAttributes = { ...DEFAULT_ATTRIBUTES_THICK }; + } + if (oracledb.thin && extendedConn.hostName) { + connAttributes[ATTR_SERVER_ADDRESS] = extendedConn.hostName; + } + if (oracledb.thin && (extendedConn.port as number)) { + connAttributes[ATTR_SERVER_PORT] = extendedConn.port; + } + if (oracledb.thin && extendedConn.protocol) { + connAttributes[ATTR_NETWORK_TRANSPORT] = extendedConn.protocol; + } + if (connection.dbName) { + dbName = oracledb.thin + ? connection.dbName.toUpperCase() + : connection.dbName; + } + connAttributes[ATTR_DB_NAMESPACE] = getDBNameSpace( + connection.instanceName, + dbName, + connection.serviceName + ); + poolAttributes = { ...connAttributes, ...POOL_ATTRIBUTES }; + + executeAttributes = { + ...connAttributes, + [ATTR_DB_OPERATION_NAME]: 'SELECT', + }; + if (oracledb.thin) { + // internal roundtrips don't have bind values. + executeAttributesInternalRoundTripBinds = { + ...connAttributes, + [ATTR_DB_OPERATION_NAME]: 'SELECT', + [ATTR_DB_STATEMENT]: sqlWithBinds, + }; + } + attributesWithSensitiveDataNoBinds = { + ...connAttributes, + [ATTR_DB_OPERATION_NAME]: 'SELECT', + [ATTR_DB_STATEMENT]: sql, + }; + attributesWithSensitiveDataBinds = { + ...connAttributes, + [ATTR_DB_OPERATION_NAME]: 'SELECT', + [ATTR_DB_STATEMENT]: sqlWithBinds, + [`${ATTR_DB_OPERATION_PARAMETER}.0`]: '0', + }; + attributesWithSensitiveDataBindsByName = { + ...connAttributes, + [ATTR_DB_OPERATION_NAME]: 'SELECT', + [ATTR_DB_STATEMENT]: sqlWithBindsByName, + [`${ATTR_DB_OPERATION_PARAMETER}.name`]: '0', + }; + await sqlCreateTable(connection, tableName, sqlCreate); + } + + before(async function () { + const skip = () => { + // this.skip() workaround + // https://github.com/mochajs/mocha/issues/2683#issuecomment-375629901 + this.test!.parent!.pending = true; + this.skip(); + }; + + if (!shouldTest) { + skip(); + } + + if (testOracleDBLocally) { + testUtils.startDocker('oracledb'); + + // increase test time + this.timeout(50000); + + // check if docker container is up + let retries = 6; + while (retries-- > 0) { + try { + connection = await oracledb.getConnection(CONFIG); + break; + } catch (err) { + console.log('retry count %d failed waiting for DB', retries); + await new Promise(r => setTimeout(r, 10000)); + } + } + if (retries < 0) { + throw new Error('docker setup Failed'); + } + } else { + connection = await oracledb.getConnection(CONFIG); + } + await doSetup(); + updateAttrSpanList(connection); + contextManager = new AsyncHooksContextManager().enable(); + context.setGlobalContextManager(contextManager); + instrumentation.setTracerProvider(provider); + instrumentation.enable(); + }); + + after(async function () { + if (connection) { + await connection.execute(sqlDropTable(tableName)); + await connection.close(); + } + instrumentation.disable(); + if (testOracleDBLocally) { + this.timeout(5000); + testUtils.cleanUpDocker('oracledb'); + } + }); + + beforeEach(() => { + contextManager = new AsyncHooksContextManager().enable(); + context.setGlobalContextManager(contextManager); + }); + + afterEach(async () => { + memoryExporter.reset(); + context.disable(); + instrumentation.enable(); + instrumentation.setConfig({ + enhancedDatabaseReporting: false, + dbStatementDump: false, + }); + }); + + it('should return an instrumentation', () => { + assert.ok(instrumentation instanceof OracleInstrumentation); + }); + + it('should have correct name', () => { + assert.strictEqual( + instrumentation.instrumentationName, + '@opentelemetry/instrumentation-oracledb' + ); + }); + + describe('#oracledb.pool operations', () => { + let pool: oracledb.Pool; + let connection: oracledb.Connection; + + afterEach(async () => { + if (pool) { + if (connection && connection.isHealthy()) { + await connection.close(); + } + await pool.close(0); + } + }); + + async function waitForCreatePool(pool: oracledb.Pool) { + if (!oracledb.thin) { + // thick mode will create all min conns in sync fashion. + return true; + } + let retryCount = 5; // counter to wait for new connections to appear + while (pool.connectionsOpen !== POOL_CONFIG.poolMin) { + // Let background thread complete poolMin conns. + await new Promise(r => setTimeout(r, 100)); + retryCount -= 1; + if (retryCount === 0) { + // skipping the test on slow networks + return false; + } + } + return true; + } + + function verifyPoolGetConnHitAttrs(span: Span, poolAlias = false) { + // With poolAlias, It causes oracledb.getConnection to call pool.getConnection + // on pool created from pool alias. + const numSpans = poolAlias ? 2 : 1; + let parentSpan: ReadableSpan = span as unknown as ReadableSpan; + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, numSpans); + let spanName: string; + + // check oracledb.getConnection + if (poolAlias) { + parentSpan = spans[1]; + spanName = SpanNames.CONNECT; + assert.deepStrictEqual(parentSpan.name, spanName); + verifySpanData( + parentSpan, + span as unknown as ReadableSpan, + poolAttributes + ); + } + + // check pool.getConnection + spanName = SpanNames.POOL_CONNECT; + assert.deepStrictEqual(spans[0].name, spanName); + verifySpanData(spans[0], parentSpan, poolAttributes); + } + + // Checks the span attributes for all round trips made for poolMin connections + // It doesn't verify attributes of createPool public API + // which is done before calling this. + function checkPoolMinConnectSpans() { + let spans = memoryExporter.getFinishedSpans(); + const createPoolSpan = spans[0]; + spans = spans.slice(1); + const spanLength = spans.length; + const numRoundTripSpans = spanLength; + const attrList = poolConnAttrList; + attrList.pop(); // remove createPool Attributes + const attrListTotal = [...attrList]; + const poolSpanNameList = [...spanNamesList]; + poolSpanNameList.pop(); // remove spanName of createPool + const spanNamesListTotal = [...poolSpanNameList]; + const eventList = new Array(numRoundTripSpans); + const statusList = new Array(numRoundTripSpans); + eventList.fill(defaultEvents, 0, numRoundTripSpans); + statusList.fill(unsetStatus, 0, numRoundTripSpans); + for (let index = 0; index < POOL_CONFIG.poolMin - 1; index++) { + attrListTotal.push(...poolConnAttrList); + spanNamesListTotal.push(...poolSpanNameList); + } + + checkRoundTripSpans( + spans, + createPoolSpan, + attrListTotal, + eventList, + statusList, + spanNamesListTotal + ); + } + + it('should intercept oracledb.createPool', async function () { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + pool = await oracledb.createPool(POOL_CONFIG); + if (!(await waitForCreatePool(pool))) { + this.skip(); + } + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, poolMinSpanCount); + const poolSpan = spans[0]; + + // check createPool span as it appears first and + // the round trip spans as part of poolMin will be + // appearing later. + assert.deepStrictEqual(poolSpan.name, SpanNames.POOL_CREATE); + verifySpanData( + poolSpan as unknown as ReadableSpan, + span as unknown as ReadableSpan, + POOL_ATTRIBUTES + ); + + // check if poolMin connection roundtrip spans are created in the + // background async task + checkPoolMinConnectSpans(); + testUtils.assertPropagation(poolSpan, span); + span.end(); + }); + }); + + it('should intercept pool.getConnection', async function () { + // create a pool with no tracing + instrumentation.disable(); + pool = await oracledb.createPool(POOL_CONFIG); + if (!(await waitForCreatePool(pool))) { + this.skip(); + } + + instrumentation.enable(); + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + connection = await pool.getConnection(); + verifyPoolGetConnHitAttrs(span); + span.end(); + }); + }); + + it('should intercept pool.getConnection with default pool alias', async function () { + // create a pool with no tracing + instrumentation.disable(); + pool = await oracledb.createPool(POOL_CONFIG); + if (!(await waitForCreatePool(pool))) { + this.skip(); + } + + instrumentation.enable(); + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + connection = await oracledb.getConnection(); + verifyPoolGetConnHitAttrs(span, true); + span.end(); + }); + }); + + it('should intercept pool.getConnection callback', async function () { + const span = tracer.startSpan('test span'); + instrumentation.disable(); + pool = await oracledb.createPool(POOL_CONFIG); + if (!(await waitForCreatePool(pool))) { + // skipping the test. + this.skip(); + } + instrumentation.enable(); + await context.with(trace.setSpan(context.active(), span), async () => { + await new Promise(resolve => { + pool.getConnection((err, conn) => { + assert.strictEqual(err, null); + connection = conn; + verifyPoolGetConnHitAttrs(span); + resolve(); + }); + }); + span.end(); + }); + }); + + it('should intercept pool.getConnection failure', async function () { + const span = tracer.startSpan('test span'); + const poolConnFailedAttrs: Record = { + ...POOL_ATTRIBUTES, + }; + const wrongConfig = Object.assign({}, POOL_CONFIG); + wrongConfig.password = 'null'; + wrongConfig.poolMin = 1; + instrumentation.disable(); + if (!oracledb.thin) { + wrongConfig.poolMin = 0; + } + pool = await oracledb.createPool(wrongConfig); + + // wait for attempting to create a poolMin connection + await waitForCreatePool(pool); + instrumentation.enable(); + await context.with(trace.setSpan(context.active(), span), async () => { + const error = await pool.getConnection().catch(e => e); + assertErrorSpan(poolConnFailedAttrs, 1, error); + span.end(); + }); + }); + + it('should intercept pool.getConnection callback failure', async function () { + const span = tracer.startSpan('test span'); + const poolConnFailedAttrs: Record = { + ...POOL_ATTRIBUTES, + }; + const wrongConfig = Object.assign({}, POOL_CONFIG); + wrongConfig.password = 'null'; + wrongConfig.password = 'null'; + wrongConfig.poolMin = 1; + instrumentation.disable(); + if (!oracledb.thin) { + wrongConfig.poolMin = 0; + } + pool = await oracledb.createPool(wrongConfig); + + // wait for attempting to create a poolMin connection + await waitForCreatePool(pool); + instrumentation.enable(); + await context.with(trace.setSpan(context.active(), span), async () => { + await new Promise(resolve => { + pool.getConnection((err, conn) => { + connection = conn; + assertErrorSpan(poolConnFailedAttrs, 1, err as any); + resolve(); + }); + }); + span.end(); + }); + }); + }); + + describe('#oracledb.getConnection(...)', () => { + let connection: oracledb.Connection; + + afterEach(async () => { + if (connection && connection.isHealthy()) { + await connection.close(); + } + }); + + it('should intercept getConnection', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + connection = await oracledb.getConnection(CONFIG); + verifySpans(span, connAttrList, spanNamesList); + span.end(); + }); + }); + + it('should intercept connection.close', async () => { + instrumentation.disable(); + const conn = await oracledb.getConnection(CONFIG); + instrumentation.enable(); + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + // connection.close generates a single internal round trip and a + // round trip for connection.close. + // We construct attrList with span attributes for 2 roundtrips. + const attrList: Record[] = []; + if (oracledb.thin) { + attrList.push(connAttrList[connAttrList.length - 1]); + } + attrList.push(connAttrList[connAttrList.length - 1]); + + await conn.close(); + const spanNamesList = []; + spanNamesList.push(SpanNames.LOGOFF_MSG); + spanNamesList.push(SpanNames.CONNECT_CLOSE); + verifySpans(span, attrList, spanNamesList); + span.end(); + }); + }); + + it('should intercept getConnection callback', done => { + const span = tracer.startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + oracledb.getConnection(CONFIG, (err, conn) => { + connection = conn; + assert.strictEqual(err, null); + verifySpans(span, connAttrList, spanNamesList); + + // Verify spans inside callback are child of application span + const callBackSpan = tracer.startSpan('test callback span'); + callBackSpan.end(); + testUtils.assertPropagation( + callBackSpan as unknown as ReadableSpan, + span as unknown as Span + ); + + span.end(); + done(); + }); + }); + }); + + it('should intercept getConnection failure', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + const wrongConfig = Object.assign({}, CONFIG); + wrongConfig.password = 'null'; + const error = await oracledb.getConnection(wrongConfig).catch(e => e); + assertErrorSpan( + failedConnAttrList[connAttrList.length - 1], + connAttrList.length, + error + ); + span.end(); + }); + }); + + it('should intercept getConnection callback failure', done => { + const span = tracer.startSpan('test span'); + const wrongConfig = Object.assign({}, CONFIG); + wrongConfig.password = 'null'; + context.with(trace.setSpan(context.active(), span), () => { + oracledb.getConnection(wrongConfig, (err, conn) => { + connection = conn; + assertErrorSpan( + failedConnAttrList[connAttrList.length - 1], + connAttrList.length, + err as any + ); + + // Verify spans inside callback are child of app span + const callBackSpan = tracer.startSpan('test callback span'); + callBackSpan.end(); + testUtils.assertPropagation( + callBackSpan as unknown as ReadableSpan, + span as unknown as Span + ); + + span.end(); + done(); + }); + }); + }); + }); + + describe('#connection.execute(...)', () => { + it('should not return a promise if callback is provided', done => { + const res = connection.execute(sql, (err, res) => { + assert.strictEqual(err, null); + done(); + }); + assert.strictEqual(res, undefined, 'No promise is returned'); + }); + + it('should return a promise if callback is not provided', done => { + const resPromise = connection.execute(sql); + resPromise + .then(res => { + assert.ok(res); + done(); + }) + .catch((err: Error) => { + assert.ok(false, err.message); + }); + }); + + it('should intercept connection.execute(sql) ', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + const res = await connection.execute(sql); + try { + assert.ok(res); + verifySpans(span); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, numExecSpans + 1); + }); + + it('should intercept connection.execute(sql) without parent span', async () => { + const res = await connection.execute(sql); + try { + assert.ok(res); + verifySpans(null); + } catch (e: any) { + assert.ok(false, e.message); + } + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, numExecSpans); + }); + + it('should intercept connection.execute(sql, callback)', done => { + const span = tracer.startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + const res = connection.execute(sql, (err, res) => { + assert.strictEqual(err, null); + assert.ok(res); + verifySpans(span); + span.end(); + + // check callback active context is parent span. + const parentContext = context.active(); + assert.strictEqual(context.active(), parentContext); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, numExecSpans + 1); + done(); + }); + assert.strictEqual(res, undefined, 'No promise is returned'); + }); + }); + + it('should intercept connection.execute(sql, callback) with out parent span', done => { + const res = connection.execute(sql, (err, res) => { + assert.strictEqual(err, null); + assert.ok(res); + verifySpans(null); + done(); + }); + assert.strictEqual(res, undefined, 'No promise is returned'); + }); + + it('should intercept connection.execute(sql, values)', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + instrumentation.setConfig({ enhancedDatabaseReporting: true }); + const resPromise = await connection.execute(sqlWithBinds, binds); + try { + assert.ok(resPromise); + verifySpans(span, [ + executeAttributesInternalRoundTripBinds, + attributesWithSensitiveDataBinds, + ]); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + + it('should intercept connection.execute(sql, values) bind-by-name', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + instrumentation.setConfig({ enhancedDatabaseReporting: true }); + const resPromise = await connection.execute( + sqlWithBindsByName, + bindsByName + ); + + // roundtrip span wont have bind values. + const roundtripAttrs = { ...attributesWithSensitiveDataNoBinds }; + roundtripAttrs[ATTR_DB_STATEMENT] = sqlWithBindsByName; + + try { + assert.ok(resPromise); + verifySpans(span, [ + roundtripAttrs, + attributesWithSensitiveDataBindsByName, + ]); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + + it('should intercept connection.execute(sql, values) with out-binds', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + instrumentation.setConfig({ enhancedDatabaseReporting: true }); + const options = {}; + const resPromise = await connection.execute( + sqlWithOutBinds, + outBinds, + options + ); + + // update sql stmt, operation. + const attrs = { ...attributesWithSensitiveDataNoBinds }; + attrs[ATTR_DB_OPERATION_NAME] = 'BEGIN'; + attrs[ATTR_DB_STATEMENT] = sqlWithOutBinds; + + try { + assert.ok(resPromise); + verifySpans( + span, + [ + attrs, + { + ...attrs, + [`${ATTR_DB_OPERATION_PARAMETER}.n`]: '', + }, + ], + [ + SpanNames.EXECUTE_MSG + ':BEGIN' + spanNameSuffix, + SpanNames.EXECUTE + ':BEGIN' + spanNameSuffix, + ] + ); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + + it('should intercept connection.execute(sql, values) with default enhancedDatabaseReporting value', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + instrumentation.setConfig(); + const resPromise = await connection.execute(sqlWithBinds, binds); + try { + assert.ok(resPromise); + verifySpans(span); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + + it('should intercept connection.execute with bindbyPosition and enhancedDatabaseReporting = true', async () => { + const span = tracer.startSpan('test span'); + const buf = Buffer.from('hello'); + instrumentation.disable(); + const lob = await connection.createLob(oracledb.CLOB); + + // Get an pre-defined object type SYS.ODCIVARCHAR2LIST (a collection of VARCHAR2) + const ODCIVarchar2List = await connection.getDbObjectClass( + 'SYS.ODCIVARCHAR2LIST' + ); + const varchar2List = new ODCIVarchar2List([ + 'TEST OBJECT', + 'DATA', + 'from', + 'node-oracledb-instrument', + ]); + instrumentation.enable(); + const date = new Date(1969, 11, 31, 0, 0, 0, 0); + const localDateString = `"${date.toISOString()}"`; + const sql = + 'select to_clob(:1), :2, TO_NUMBER(:3), to_char(:4), :5, :6, :7, :8, :9, :10 from dual'; + const binds = [ + 'Hello é World', + lob, + '43', + 43, + date, + buf, + varchar2List, + null, + undefined, + true, + ]; + const expectedBinds = { + [`${ATTR_DB_OPERATION_PARAMETER}.0`]: 'Hello é World', + [`${ATTR_DB_OPERATION_PARAMETER}.1`]: '[object Object]', + [`${ATTR_DB_OPERATION_PARAMETER}.2`]: '43', + [`${ATTR_DB_OPERATION_PARAMETER}.3`]: '43', + [`${ATTR_DB_OPERATION_PARAMETER}.4`]: localDateString, + [`${ATTR_DB_OPERATION_PARAMETER}.5`]: 'hello', + [`${ATTR_DB_OPERATION_PARAMETER}.6`]: + '["TEST OBJECT","DATA","from","node-oracledb-instrument"]', + [`${ATTR_DB_OPERATION_PARAMETER}.7`]: 'null', + [`${ATTR_DB_OPERATION_PARAMETER}.8`]: 'null', + [`${ATTR_DB_OPERATION_PARAMETER}.9`]: 'true', + }; + await context.with(trace.setSpan(context.active(), span), async () => { + instrumentation.setConfig({ enhancedDatabaseReporting: true }); + + const resPromise = await connection.execute(sql, binds); + try { + const attributesWithSensitiveData: Record< + string, + string | number | any[] + > = { + ...connAttributes, + [ATTR_DB_OPERATION_NAME]: 'SELECT', + [ATTR_DB_STATEMENT]: sql, + ...expectedBinds, + }; + + // Attributes for roundtrips do not contain bindvalues. + const attrs = { ...attributesWithSensitiveDataNoBinds }; + attrs[ATTR_DB_STATEMENT] = sql; + + assert.ok(resPromise); + + // LOBs will cause an additional round trip for define types... + verifySpans( + span, + [attrs, attrs, attributesWithSensitiveData], + [ + SpanNames.EXECUTE_MSG + ':SELECT' + spanNameSuffix, + SpanNames.EXECUTE_MSG + ':SELECT' + spanNameSuffix, + SpanNames.EXECUTE + ':SELECT' + spanNameSuffix, + ] + ); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + lob.destroy(); + }); + }); + + it('should intercept connection.execute with bindbyName and enhancedDatabaseReporting = true', async () => { + const span = tracer.startSpan('test span'); + const buf = Buffer.from('hello'); + instrumentation.disable(); + const lob = await connection.createLob(oracledb.CLOB); + + // Get an pre-defined object type SYS.ODCIVARCHAR2LIST (a collection of VARCHAR2) + const ODCIVarchar2List = await connection.getDbObjectClass( + 'SYS.ODCIVARCHAR2LIST' + ); + const varchar2List = new ODCIVarchar2List([ + 'TEST OBJECT', + 'DATA', + 'from', + 'node-oracledb-instrument', + ]); + instrumentation.enable(); + const date = new Date(1969, 11, 31, 0, 0, 0, 0); + const localDateString = `"${date.toISOString()}"`; + const sql = + 'select to_clob(:b1), :b2, TO_NUMBER(:b3), to_char(:b4), :b5, :b6, :b7, :b8, :b9, :b10 from dual'; + const binds: any = { + b1: 'Hello é World', + b2: lob, + b3: '43', + b4: 43, + b5: date, + b6: buf, + b7: varchar2List, + b8: null, + b9: undefined, + b10: true, + }; + const expectedBinds = { + [`${ATTR_DB_OPERATION_PARAMETER}.b1`]: 'Hello é World', + [`${ATTR_DB_OPERATION_PARAMETER}.b2`]: '[object Object]', + [`${ATTR_DB_OPERATION_PARAMETER}.b3`]: '43', + [`${ATTR_DB_OPERATION_PARAMETER}.b4`]: '43', + [`${ATTR_DB_OPERATION_PARAMETER}.b5`]: localDateString, + [`${ATTR_DB_OPERATION_PARAMETER}.b6`]: 'hello', + [`${ATTR_DB_OPERATION_PARAMETER}.b7`]: + '["TEST OBJECT","DATA","from","node-oracledb-instrument"]', + [`${ATTR_DB_OPERATION_PARAMETER}.b8`]: 'null', + [`${ATTR_DB_OPERATION_PARAMETER}.b9`]: 'null', + [`${ATTR_DB_OPERATION_PARAMETER}.b10`]: 'true', + }; + await context.with(trace.setSpan(context.active(), span), async () => { + instrumentation.setConfig({ enhancedDatabaseReporting: true }); + + const resPromise = await connection.execute(sql, binds); + try { + const attributesWithSensitiveData: Record< + string, + string | number | any[] + > = { + ...connAttributes, + [ATTR_DB_OPERATION_NAME]: 'SELECT', + [ATTR_DB_STATEMENT]: sql, + ...expectedBinds, + }; + + // Attributes for roundtrips do not contain bindvalues. + const attrs = { ...attributesWithSensitiveDataNoBinds }; + attrs[ATTR_DB_STATEMENT] = sql; + + assert.ok(resPromise); + // LOBs will cause an additional round trip for define types... + verifySpans( + span, + [attrs, attrs, attributesWithSensitiveData], + [ + SpanNames.EXECUTE_MSG + ':SELECT' + spanNameSuffix, + SpanNames.EXECUTE_MSG + ':SELECT' + spanNameSuffix, + SpanNames.EXECUTE + ':SELECT' + spanNameSuffix, + ] + ); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + lob.destroy(); + }); + }); + + it('should intercept connection.execute(sql, values) with enhancedDatabaseReporting = false', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + instrumentation.setConfig({ enhancedDatabaseReporting: false }); + const resPromise = await connection.execute(sqlWithBinds, binds); + try { + assert.ok(resPromise); + verifySpans(span); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + + it('should intercept connection.execute(sql, values) with dbStatementDump = true', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + instrumentation.setConfig({ dbStatementDump: true }); + const resPromise = await connection.execute(sqlWithBinds, binds); + try { + assert.ok(resPromise); + const attrs = { ...executeAttributes }; + attrs[ATTR_DB_STATEMENT] = sqlWithBinds; + verifySpans(span, [attrs, attrs]); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + + it('should intercept connection.execute(sql, values) with dbStatementDump and enhancedDatabaseReporting as true', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + // enhancedDatabaseReporting overrides dbStatementDump config + instrumentation.setConfig({ + enhancedDatabaseReporting: true, + dbStatementDump: true, + }); + const resPromise = await connection.execute(sqlWithBinds, binds); + try { + assert.ok(resPromise); + verifySpans(span, [ + executeAttributesInternalRoundTripBinds, + attributesWithSensitiveDataBinds, + ]); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + + it('should intercept connection.execute(sql, values, callback)', done => { + const span = tracer.startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + instrumentation.setConfig({ enhancedDatabaseReporting: true }); + const res = connection.execute(sqlWithBinds, binds, (err, res) => { + assert.strictEqual(err, null); + assert.ok(res); + verifySpans(span, [ + executeAttributesInternalRoundTripBinds, + attributesWithSensitiveDataBinds, + ]); + span.end(); + done(); + }); + assert.strictEqual(res, undefined, 'No promise is returned'); + }); + }); + + it('should intercept connection.executeMany(sql, binds)', async () => { + const span = tracer.startSpan('test span'); + const binds = [ + { a: 1, b: 'Test 1 (One)' }, + { a: 2, b: 'Test 2 (Two)' }, + { a: 3, b: 'Test 3 (Three)' }, + { a: 4 }, + { a: 5, b: 'Test 5 (Five)' }, + ]; + const sqlInsert = `INSERT INTO ${tableName} VALUES (:a, :b, 'clob')`; + + await context.with(trace.setSpan(context.active(), span), async () => { + const res = await connection.executeMany>( + sqlInsert, + binds + ); + try { + assert.ok(res); + verifySpans( + span, + [connAttributes, connAttributes], + [ + SpanNames.EXECUTE_MSG + ':' + spanNameSuffix, + SpanNames.EXECUTE_MANY + ':' + spanNameSuffix, + ] + ); + } catch (e: any) { + assert.ok(false, e.message); + } finally { + await connection.commit(); + span.end(); + } + }); + }); + + it('should intercept connection.executeMany(sql, binds) with out parent span', async () => { + instrumentation.enable(); + const binds = [ + { a: 1, b: 'Test 1 (One)' }, + { a: 2, b: 'Test 2 (Two)' }, + { a: 3, b: 'Test 3 (Three)' }, + { a: 4 }, + { a: 5, b: 'Test 5 (Five)' }, + ]; + const sqlInsert = `INSERT INTO ${tableName} VALUES (:a, :b, 'clob')`; + const res = await connection.executeMany>(sqlInsert, binds); + try { + assert.ok(res); + verifySpans( + null, + [connAttributes, connAttributes], + [ + SpanNames.EXECUTE_MSG + ':' + spanNameSuffix, + SpanNames.EXECUTE_MANY + ':' + spanNameSuffix, + ] + ); + } catch (e: any) { + assert.ok(false, e.message); + } finally { + await connection.commit(); + } + }); + + it('Verify error message for negative tests', async () => { + // The error message remains same with instrumented module. + let error = await (connection as any).execute().catch((e: unknown) => e); + assertErrorSpan(connAttributes, 1, error); + memoryExporter.reset(); + + error = await (connection as any).execute(null).catch((e: unknown) => e); + assertErrorSpan(connAttributes, 1, error); + memoryExporter.reset(); + + error = await (connection as any) + .execute(undefined) + .catch((e: unknown) => e); + assertErrorSpan(connAttributes, 1, error); + memoryExporter.reset(); + + const wrongSql = 'select 1 from nonExistTable'; + error = await (connection as any) + .execute(wrongSql) + .catch((e: unknown) => e); + assertErrorSpan(executeAttributes, numExecSpans, error); + memoryExporter.reset(); + }); + + it('should not generate traces when requireParentSpan=true is specified', async () => { + instrumentation.setConfig({ + requireParentSpan: true, + }); + memoryExporter.reset(); + await connection.execute(sql); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 0); + instrumentation.setConfig({ + requireParentSpan: false, + }); + memoryExporter.reset(); + }); + + it('verify updating Tracer', async () => { + instrumentation.setTracerProvider(provider); + + const tracer = provider.getTracer( + instrumentation.instrumentationName, + instrumentation.instrumentationVersion + ); + assert.strictEqual(!!tracer, true); + assert.ok(tracer, 'Tracer instance should not be null'); + const res = await connection.execute(sql); + try { + assert.ok(res); + verifySpans(null); + } catch (e: any) { + assert.ok(false, e.message); + } + const spans = memoryExporter.getFinishedSpans().filter(s => { + assert.strictEqual( + s.instrumentationScope.name, + '@opentelemetry/instrumentation-oracledb', + `Unexpected instrumentation scope name: ${s.instrumentationScope.name}` + ); + return true; + }); + assert.strictEqual(spans.length, numExecSpans); + }); + }); + + describe('#LOB operations (...)', () => { + let lob: oracledb.Lob; + + afterEach(function () { + if (lob) { + lob.destroy(); + } + }); + + it('should intercept create Templob ', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + lob = await connection.createLob(oracledb.CLOB); + try { + assert.ok(lob); + verifySpans( + span, + [connAttributes, connAttributes, connAttributes], + [SpanNames.LOB_MESSAGE, SpanNames.LOB_MESSAGE, SpanNames.CREATE_LOB] + ); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + + it('should intercept lob getData ', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + const sqlInsert = `INSERT INTO ${tableName} VALUES (:a, :b, 'clob')`; + instrumentation.disable(); + let result = await connection.execute(sqlInsert, [1, 'test']); + result = await connection.execute(` select clobval from ${tableName}`); + try { + assert.ok(result.rows); + assert.ok(Array.isArray(result.rows[0])); + const lob: oracledb.Lob = result.rows[0][0] as oracledb.Lob; + instrumentation.enable(); + await lob.getData(); + verifySpans( + span, + [connAttributes, connAttributes], + [SpanNames.LOB_MESSAGE, SpanNames.LOB_GETDATA] + ); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + }); + }); + + describe('when specifying a requestHook configuration', () => { + const key1 = 'CONNECT_DATA'; + const key2 = 'ARGS_DATA'; + const argVal = { 0: sql }; + + describe('AND valid requestHook', () => { + beforeEach(async () => { + instrumentation.setConfig({ + enhancedDatabaseReporting: true, + requestHook: (span, requestInfo) => { + if (requestInfo) { + span.setAttribute(key1, JSON.stringify(requestInfo.connection)); + span.setAttribute(key2, JSON.stringify(requestInfo.inputArgs)); + } + }, + }); + }); + + it('should attach request hook data to resulting spans for query returning a Promise', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + const res = await connection.execute(sql); + try { + assert.ok(res); + const extendedConn: any = connection; + verifySpans(span, [ + attributesWithSensitiveDataNoBinds, + { + ...attributesWithSensitiveDataNoBinds, + [key1]: JSON.stringify(extendedConn.connectTraceConfig), + [key2]: JSON.stringify(argVal), + }, + ]); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, numExecSpans + 1); + }); + + it('should attach request hook data to resulting spans for query with callback)', done => { + const span = tracer.startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + const res = connection.execute(sql, (err, res) => { + assert.strictEqual(err, null); + assert.ok(res); + const extendedConn: any = connection; + verifySpans(span, [ + attributesWithSensitiveDataNoBinds, + { + ...attributesWithSensitiveDataNoBinds, + [key1]: JSON.stringify(extendedConn.connectTraceConfig), + [key2]: JSON.stringify(argVal), + }, + ]); + span.end(); + done(); + }); + assert.strictEqual(res, undefined, 'No promise is returned'); + }); + }); + }); + + describe('AND invalid requestHook', () => { + beforeEach(async () => { + instrumentation.setConfig({ + enhancedDatabaseReporting: true, + requestHook: (span, requestInfo) => { + throw 'Failed!'; + }, + }); + }); + + it('should not do any harm when throwing an exception', done => { + const span = tracer.startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + const res = connection.execute(sql, (err, res) => { + assert.strictEqual(err, null); + assert.ok(res); + verifySpans(span, [ + attributesWithSensitiveDataNoBinds, + attributesWithSensitiveDataNoBinds, + ]); + span.end(); + done(); + }); + assert.strictEqual(res, undefined, 'No promise is returned'); + }); + }); + }); + }); + + describe('when specifying a responseHook configuration', () => { + const key1 = 'QUERY_RESULT'; + + describe('AND valid responseHook', () => { + beforeEach(async () => { + instrumentation.setConfig({ + enhancedDatabaseReporting: true, + responseHook: (span, responseInfo) => { + if (responseInfo) { + span.setAttribute(key1, JSON.stringify(responseInfo.data)); + } + }, + }); + }); + + it('should attach response hook data to resulting spans for query returning a Promise', async () => { + const span = tracer.startSpan('test span'); + await context.with(trace.setSpan(context.active(), span), async () => { + const res = await connection.execute(sql); + try { + assert.ok(res); + verifySpans(span, [ + attributesWithSensitiveDataNoBinds, + { + ...attributesWithSensitiveDataNoBinds, + [key1]: JSON.stringify(res), + }, + ]); + } catch (e: any) { + assert.ok(false, e.message); + } + span.end(); + }); + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, numExecSpans + 1); + }); + + it('should attach response hook data to resulting spans for query with callback)', done => { + const span = tracer.startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + const res = connection.execute(sql, (err, res) => { + assert.strictEqual(err, null); + assert.ok(res); + verifySpans(span, [ + attributesWithSensitiveDataNoBinds, + { + ...attributesWithSensitiveDataNoBinds, + [key1]: JSON.stringify(res), + }, + ]); + span.end(); + done(); + }); + assert.strictEqual(res, undefined, 'No promise is returned'); + }); + }); + }); + + describe('AND invalid responseHook', () => { + beforeEach(async () => { + instrumentation.setConfig({ + enhancedDatabaseReporting: true, + responseHook: (span, responseInfo) => { + throw 'Failure!'; + }, + }); + }); + + it('should not do any harm when throwing an exception', done => { + const span = tracer.startSpan('test span'); + context.with(trace.setSpan(context.active(), span), () => { + const res = connection.execute(sql, (err, res) => { + assert.strictEqual(err, null); + assert.ok(res); + verifySpans(span, [ + attributesWithSensitiveDataNoBinds, + attributesWithSensitiveDataNoBinds, + ]); + span.end(); + done(); + }); + assert.strictEqual(res, undefined, 'No promise is returned'); + }); + }); + }); + }); +}); diff --git a/plugins/node/opentelemetry-instrumentation-oracledb/tsconfig.json b/plugins/node/opentelemetry-instrumentation-oracledb/tsconfig.json new file mode 100644 index 0000000000..28be80d266 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-oracledb/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/release-please-config.json b/release-please-config.json index c66d81cda9..9bbf8c8763 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -57,6 +57,7 @@ "plugins/node/opentelemetry-instrumentation-mysql2": {}, "plugins/node/opentelemetry-instrumentation-nestjs-core": {}, "plugins/node/opentelemetry-instrumentation-net": {}, + "plugins/node/opentelemetry-instrumentation-oracledb": {}, "plugins/node/opentelemetry-instrumentation-pg": {}, "plugins/node/opentelemetry-instrumentation-pino": {}, "plugins/node/opentelemetry-instrumentation-redis": {},