diff --git a/.github/component_owners.yml b/.github/component_owners.yml
index e9d657e9d8..155822db67 100644
--- a/.github/component_owners.yml
+++ b/.github/component_owners.yml
@@ -14,6 +14,8 @@ components:
packages/opentelemetry-id-generator-aws-xray:
- NathanielRN
- willarmiros
+ plugins/node/instrumentation-tedious:
+ - rauno56
plugins/node/opentelemetry-instrumentation-aws-lambda:
- NathanielRN
- willarmiros
diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml
index 1c58b9287f..95b8d6e82b 100644
--- a/.github/workflows/unit-test.yml
+++ b/.github/workflows/unit-test.yml
@@ -15,6 +15,7 @@ jobs:
lerna-extra-args: >-
--ignore @opentelemetry/instrumentation-aws-sdk
--ignore @opentelemetry/instrumentation-pino
+ --ignore @opentelemetry/instrumentation-tedious
- node: "10"
lerna-extra-args: >-
--ignore @opentelemetry/instrumentation-pino
@@ -28,6 +29,19 @@ jobs:
image: mongo
ports:
- 27017:27017
+ mssql:
+ image: mcr.microsoft.com/mssql/server:2017-latest
+ env:
+ SA_PASSWORD: mssql_passw0rd
+ ACCEPT_EULA: Y
+ ports:
+ - 1433:1433
+ options: >-
+ --health-cmd "/opt/mssql-tools/bin/sqlcmd -U sa -P $SA_PASSWORD -Q 'select 1' -b -o /dev/null"
+ --health-interval 1s
+ --health-timeout 30s
+ --health-start-period 10s
+ --health-retries 20
mysql:
image: circleci/mysql:5.7
env:
@@ -72,12 +86,14 @@ jobs:
RUN_MEMCACHED_TESTS: 1
RUN_MONGODB_TESTS: 1
RUN_MYSQL_TESTS: 1
+ RUN_MSSQL_TESTS: 1
RUN_POSTGRES_TESTS: 1
RUN_REDIS_TESTS: 1
CASSANDRA_HOST: localhost
MONGODB_DB: opentelemetry-tests
MONGODB_HOST: localhost
MONGODB_PORT: 27017
+ MSSQL_PASSWORD: mssql_passw0rd
MYSQL_DATABASE: circle_database
MYSQL_HOST: localhost
MYSQL_PASSWORD: secret
diff --git a/README.md b/README.md
index 93ec004e0d..a4b7b896d4 100644
--- a/README.md
+++ b/README.md
@@ -68,8 +68,8 @@ OpenTelemetry can collect tracing data automatically using instrumentations. Ven
- [@opentelemetry/instrumentation-koa][otel-contrib-instrumentation-koa]
- [@opentelemetry/instrumentation-memcached][otel-contrib-instrumentation-memcached]
- [@opentelemetry/instrumentation-mongodb][otel-contrib-instrumentation-mongodb]
-- [@opentelemetry/instrumentation-mysql][otel-contrib-instrumentation-mysql]
- [@opentelemetry/instrumentation-mysql2][otel-contrib-instrumentation-mysql2]
+- [@opentelemetry/instrumentation-mysql][otel-contrib-instrumentation-mysql]
- [@opentelemetry/instrumentation-nestjs-core][otel-contrib-instrumentation-nestjs-core]
- [@opentelemetry/instrumentation-net][otel-contrib-instrumentation-net]
- [@opentelemetry/instrumentation-pg][otel-contrib-instrumentation-pg]
@@ -77,6 +77,7 @@ OpenTelemetry can collect tracing data automatically using instrumentations. Ven
- [@opentelemetry/instrumentation-redis][otel-contrib-instrumentation-redis]
- [@opentelemetry/instrumentation-restify][otel-contrib-instrumentation-restify]
- [@opentelemetry/instrumentation-router][otel-contrib-instrumentation-router]
+- [@opentelemetry/instrumentation-tedious][otel-contrib-instrumentation-tedious]
- [@opentelemetry/instrumentation-winston][otel-contrib-instrumentation-winston]
### Web Instrumentations
@@ -167,10 +168,11 @@ Apache 2.0 - See [LICENSE][license-url] for more information.
[otel-contrib-instrumentation-ioredis]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-ioredis
[otel-contrib-instrumentation-knex]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-knex
[otel-contrib-instrumentation-koa]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-koa
+[otel-contrib-instrumentation-long-task]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-long-task
[otel-contrib-instrumentation-memcached]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-memcached
[otel-contrib-instrumentation-mongodb]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mongodb
-[otel-contrib-instrumentation-mysql]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql
[otel-contrib-instrumentation-mysql2]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql2
+[otel-contrib-instrumentation-mysql]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-mysql
[otel-contrib-instrumentation-nestjs-core]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-nestjs-core
[otel-contrib-instrumentation-net]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-net
[otel-contrib-instrumentation-pg]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-pg
@@ -178,9 +180,9 @@ Apache 2.0 - See [LICENSE][license-url] for more information.
[otel-contrib-instrumentation-redis]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-redis
[otel-contrib-instrumentation-restify]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-restify
[otel-contrib-instrumentation-router]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-router
-[otel-contrib-instrumentation-winston]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-winston
-[otel-contrib-instrumentation-long-task]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-long-task
+[otel-contrib-instrumentation-tedious]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-tedious
[otel-contrib-instrumentation-user-interaction]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-instrumentation-user-interaction
+[otel-contrib-instrumentation-winston]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-winston
[otel-contrib-plugin-react-load]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/web/opentelemetry-plugin-react-load
[otel-contrib-auto-instr-node]: https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/metapackages/auto-instrumentations-node
diff --git a/packages/opentelemetry-test-utils/src/test-utils.ts b/packages/opentelemetry-test-utils/src/test-utils.ts
index e72d66fcf6..de3338b974 100644
--- a/packages/opentelemetry-test-utils/src/test-utils.ts
+++ b/packages/opentelemetry-test-utils/src/test-utils.ts
@@ -32,14 +32,16 @@ import * as path from 'path';
const dockerRunCmds = {
cassandra:
- 'docker run -d -p 9042:9042 --name otel-cassandra bitnami/cassandra:3',
- redis: 'docker run --rm -d --name otel-redis -p 63790:6379 redis:alpine',
+ 'docker run --rm -d --name otel-cassandra -p 9042:9042 bitnami/cassandra:3',
+ memcached:
+ 'docker run --rm -d --name otel-memcached -p 11211:11211 memcached:1.6.9-alpine',
+ mssql:
+ 'docker run --rm -d --name otel-mssql -p 1433:1433 -e SA_PASSWORD=mssql_passw0rd -e ACCEPT_EULA=Y mcr.microsoft.com/mssql/server:2017-latest',
mysql:
'docker run --rm -d --name otel-mysql -p 33306:3306 -e MYSQL_ROOT_PASSWORD=rootpw -e MYSQL_DATABASE=test_db -e MYSQL_USER=otel -e MYSQL_PASSWORD=secret circleci/mysql:5.7',
postgres:
'docker run --rm -d --name otel-postgres -p 54320:5432 -e POSTGRES_PASSWORD=postgres postgres:13-alpine',
- memcached:
- 'docker run --rm -d --name otel-memcached -p 11211:11211 memcached:1.6.9-alpine',
+ redis: 'docker run --rm -d --name otel-redis -p 63790:6379 redis:alpine',
};
export function startDocker(db: keyof typeof dockerRunCmds) {
@@ -66,6 +68,10 @@ function run(cmd: string) {
const proc = childProcess.spawnSync(cmd, {
shell: true,
});
+ if (proc.status !== 0) {
+ console.error('Failed run command:', cmd);
+ console.error(proc.output);
+ }
return {
code: proc.status,
output: proc.output
diff --git a/plugins/node/instrumentation-tedious/.eslintignore b/plugins/node/instrumentation-tedious/.eslintignore
new file mode 100644
index 0000000000..378eac25d3
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/.eslintignore
@@ -0,0 +1 @@
+build
diff --git a/plugins/node/instrumentation-tedious/.eslintrc.js b/plugins/node/instrumentation-tedious/.eslintrc.js
new file mode 100644
index 0000000000..f756f4488b
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/.eslintrc.js
@@ -0,0 +1,7 @@
+module.exports = {
+ "env": {
+ "mocha": true,
+ "node": true
+ },
+ ...require('../../../eslint.config.js')
+}
diff --git a/plugins/node/instrumentation-tedious/.npmignore b/plugins/node/instrumentation-tedious/.npmignore
new file mode 100644
index 0000000000..9505ba9450
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/.npmignore
@@ -0,0 +1,4 @@
+/bin
+/coverage
+/doc
+/test
diff --git a/plugins/node/instrumentation-tedious/.tav.yml b/plugins/node/instrumentation-tedious/.tav.yml
new file mode 100644
index 0000000000..6c0737e66e
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/.tav.yml
@@ -0,0 +1,7 @@
+tedious:
+ # 4.0.0 is broken: https://github.com/tediousjs/tedious/commit/4eceb48
+ versions: ">=1.11.0 <4 || >=4.0.1"
+ commands: npm run test
+
+ # Fix missing `test-utils` package
+ pretest: npm run --prefix ../../../ lerna:link
diff --git a/plugins/node/instrumentation-tedious/LICENSE b/plugins/node/instrumentation-tedious/LICENSE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/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/instrumentation-tedious/README.md b/plugins/node/instrumentation-tedious/README.md
new file mode 100644
index 0000000000..8c8f6e67b5
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/README.md
@@ -0,0 +1,58 @@
+# OpenTelemetry Tedious Instrumentation for Node.js
+
+[![NPM Published Version][npm-img]][npm-url]
+[![Apache License][license-image]][license-image]
+
+This module provides automatic instrumentation for [`tedious`](https://github.com/tediousjs/tedious).
+
+For automatic instrumentation see the
+[`@opentelemetry/sdk-trace-node`](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-node) package.
+
+Compatible with OpenTelemetry JS API and SDK `1.0+`.
+
+## Installation
+
+```bash
+npm install --save @opentelemetry/instrumentation-tedious
+```
+
+## Supported Versions
+
+- `>=1.11.0`
+
+## Usage
+
+OpenTelemetry Tedious Instrumentation allows the user to automatically collect trace data and export them to the backend of choice, to give observability to distributed systems when working with [`tedious`](https://github.com/tediousjs/tedious).
+
+To load a specific plugin, specify it in the registerInstrumentations's configuration:
+
+```js
+const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
+const { TediousInstrumentation } = require('@opentelemetry/instrumentation-tedious');
+const { registerInstrumentations } = require('@opentelemetry/instrumentation');
+
+const provider = new NodeTracerProvider();
+provider.register();
+
+registerInstrumentations({
+ instrumentations: [
+ new TediousInstrumentation(),
+ ],
+})
+```
+
+## 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-tedious
+[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-tedious.svg
diff --git a/plugins/node/instrumentation-tedious/package.json b/plugins/node/instrumentation-tedious/package.json
new file mode 100644
index 0000000000..49c477a622
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/package.json
@@ -0,0 +1,72 @@
+{
+ "name": "@opentelemetry/instrumentation-tedious",
+ "version": "0.27.0",
+ "description": "OpenTelemetry instrumentation for `tedious`",
+ "main": "build/src/index.js",
+ "types": "build/src/index.d.ts",
+ "repository": "open-telemetry/opentelemetry-js-contrib",
+ "scripts": {
+ "clean": "rimraf build/*",
+ "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../",
+ "compile": "npm run version:update && tsc -p .",
+ "lint:fix": "eslint . --ext .ts --fix",
+ "lint": "eslint . --ext .ts",
+ "precompile": "tsc --version && lerna run version --scope @opentelemetry/instrumentation-tedious --include-dependencies",
+ "prewatch": "npm run precompile",
+ "prepare": "npm run compile",
+ "tdd": "npm run test -- --watch-extensions ts --watch",
+ "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'",
+ "test-all-versions": "tav",
+ "version:update": "node ../../../scripts/version-update.js"
+ },
+ "keywords": [
+ "instrumentation",
+ "microsoft",
+ "mssql",
+ "nodejs",
+ "opentelemetry",
+ "profiling",
+ "sql server",
+ "tds",
+ "tedious",
+ "tracing"
+ ],
+ "author": "OpenTelemetry Authors",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.5.0"
+ },
+ "files": [
+ "build/src/**/*.js",
+ "build/src/**/*.js.map",
+ "build/src/**/*.d.ts"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.0.2"
+ },
+ "devDependencies": {
+ "@opentelemetry/api": "1.0.2",
+ "@opentelemetry/context-async-hooks": "1.0.1",
+ "@opentelemetry/contrib-test-utils": "^0.28.0",
+ "@opentelemetry/sdk-trace-base": "1.0.1",
+ "@types/mocha": "7.0.2",
+ "@types/node": "14.17.9",
+ "codecov": "3.8.3",
+ "gts": "3.1.0",
+ "mocha": "^7.2.0",
+ "nyc": "15.1.0",
+ "rimraf": "3.0.2",
+ "tedious": "^14.0.0",
+ "test-all-versions": "5.0.1",
+ "ts-mocha": "8.0.0",
+ "typescript": "4.3.5"
+ },
+ "dependencies": {
+ "@opentelemetry/instrumentation": "^0.27.0",
+ "@opentelemetry/semantic-conventions": "^1.0.0",
+ "@types/tedious": "^4.0.6"
+ }
+}
diff --git a/plugins/node/instrumentation-tedious/src/index.ts b/plugins/node/instrumentation-tedious/src/index.ts
new file mode 100644
index 0000000000..6f2dd724b7
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/src/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { TediousInstrumentation } from './instrumentation';
+
+export * from './instrumentation';
+export default TediousInstrumentation;
+
+export { TediousInstrumentationConfig } from './types';
diff --git a/plugins/node/instrumentation-tedious/src/instrumentation.ts b/plugins/node/instrumentation-tedious/src/instrumentation.ts
new file mode 100644
index 0000000000..b7f4a03c53
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/src/instrumentation.ts
@@ -0,0 +1,240 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as api from '@opentelemetry/api';
+import { EventEmitter } from 'events';
+import {
+ InstrumentationBase,
+ InstrumentationNodeModuleDefinition,
+ isWrapped,
+} from '@opentelemetry/instrumentation';
+import {
+ DbSystemValues,
+ SemanticAttributes,
+} from '@opentelemetry/semantic-conventions';
+import type * as tedious from 'tedious';
+import { TediousInstrumentationConfig } from './types';
+import { getSpanName, once } from './utils';
+import { VERSION } from './version';
+
+const CURRENT_DATABASE = Symbol(
+ 'opentelemetry.instrumentation-tedious.current-database'
+);
+const PATCHED_METHODS = [
+ 'callProcedure',
+ 'execSql',
+ 'execSqlBatch',
+ 'execBulkLoad',
+ 'prepare',
+ 'execute',
+];
+
+type UnknownFunction = (...args: any[]) => any;
+type ApproxConnection = EventEmitter & {
+ [CURRENT_DATABASE]: string;
+ config: any;
+};
+type ApproxRequest = EventEmitter & {
+ sqlTextOrProcedure: string | undefined;
+ callback: any;
+ table: string | undefined;
+ parametersByName: any;
+};
+
+function setDatabase(this: ApproxConnection, databaseName: string) {
+ Object.defineProperty(this, CURRENT_DATABASE, {
+ value: databaseName,
+ writable: true,
+ });
+}
+
+export class TediousInstrumentation extends InstrumentationBase<
+ typeof tedious
+> {
+ static readonly COMPONENT = 'tedious';
+
+ constructor(config?: TediousInstrumentationConfig) {
+ super('@opentelemetry/instrumentation-tedious', VERSION, config);
+ }
+
+ protected init() {
+ return [
+ new InstrumentationNodeModuleDefinition(
+ TediousInstrumentation.COMPONENT,
+ ['*'],
+ (moduleExports: any, moduleVersion) => {
+ this._diag.debug(`Patching tedious@${moduleVersion}`);
+
+ const ConnectionPrototype: any = moduleExports.Connection.prototype;
+ for (const method of PATCHED_METHODS) {
+ if (isWrapped(ConnectionPrototype[method])) {
+ this._unwrap(ConnectionPrototype, method);
+ }
+ this._wrap(
+ ConnectionPrototype,
+ method,
+ this._patchQuery(method) as any
+ );
+ }
+
+ if (isWrapped(ConnectionPrototype.connect)) {
+ this._unwrap(ConnectionPrototype, 'connect');
+ }
+ this._wrap(ConnectionPrototype, 'connect', this._patchConnect);
+
+ return moduleExports;
+ },
+ (moduleExports: any) => {
+ if (moduleExports === undefined) return;
+ const ConnectionPrototype: any = moduleExports.Connection.prototype;
+ for (const method of PATCHED_METHODS) {
+ this._unwrap(ConnectionPrototype, method);
+ }
+ this._unwrap(ConnectionPrototype, 'connect');
+ }
+ ),
+ ];
+ }
+
+ private _patchConnect(original: UnknownFunction): UnknownFunction {
+ return function patchedConnect(this: ApproxConnection) {
+ setDatabase.call(this, this.config?.options?.database);
+
+ // remove the listener first in case it's already added
+ this.removeListener('databaseChange', setDatabase);
+ this.on('databaseChange', setDatabase);
+
+ this.once('end', () => {
+ this.removeListener('databaseChange', setDatabase);
+ });
+ return original.apply(this, arguments as unknown as any[]);
+ };
+ }
+
+ private _patchQuery(operation: string) {
+ return (originalMethod: UnknownFunction): UnknownFunction => {
+ const thisPlugin = this;
+ this._diag.debug(
+ `TediousInstrumentation: patched Connection.prototype.${operation}`
+ );
+
+ function patchedMethod(this: ApproxConnection, request: ApproxRequest) {
+ if (!(request instanceof EventEmitter)) {
+ thisPlugin._diag.warn(
+ `Unexpected invocation of patched ${operation} method. Span not recorded`
+ );
+ return originalMethod.apply(this, arguments as unknown as any[]);
+ }
+ let procCount = 0;
+ let statementCount = 0;
+ const incrementStatementCount = () => statementCount++;
+ const incrementProcCount = () => procCount++;
+ const databaseName = this[CURRENT_DATABASE];
+ const sql = (request => {
+ // Required for <11.0.9
+ if (
+ request.sqlTextOrProcedure === 'sp_prepare' &&
+ request.parametersByName?.stmt?.value
+ ) {
+ return request.parametersByName.stmt.value;
+ }
+ return request.sqlTextOrProcedure;
+ })(request);
+
+ const span = thisPlugin.tracer.startSpan(
+ getSpanName(operation, databaseName, sql, request.table),
+ {
+ kind: api.SpanKind.CLIENT,
+ attributes: {
+ [SemanticAttributes.DB_SYSTEM]: DbSystemValues.MSSQL,
+ [SemanticAttributes.DB_NAME]: databaseName,
+ [SemanticAttributes.NET_PEER_PORT]: this.config?.options?.port,
+ [SemanticAttributes.NET_PEER_NAME]: this.config?.server,
+ // >=4 uses `authentication` object, older versions just userName and password pair
+ [SemanticAttributes.DB_USER]:
+ this.config?.userName ??
+ this.config?.authentication?.options?.userName,
+ [SemanticAttributes.DB_STATEMENT]: sql,
+ [SemanticAttributes.DB_SQL_TABLE]: request.table,
+ },
+ }
+ );
+
+ const endSpan = once((err?: any) => {
+ request.removeListener('done', incrementStatementCount);
+ request.removeListener('doneInProc', incrementStatementCount);
+ request.removeListener('doneProc', incrementProcCount);
+ request.removeListener('error', endSpan);
+ this.removeListener('end', endSpan);
+
+ span.setAttribute('tedious.procedure_count', procCount);
+ span.setAttribute('tedious.statement_count', statementCount);
+ if (err) {
+ span.setStatus({
+ code: api.SpanStatusCode.ERROR,
+ message: err.message,
+ });
+ }
+ span.end();
+ });
+
+ request.on('done', incrementStatementCount);
+ request.on('doneInProc', incrementStatementCount);
+ request.on('doneProc', incrementProcCount);
+ request.once('error', endSpan);
+ this.on('end', endSpan);
+
+ if (typeof request.callback === 'function') {
+ thisPlugin._wrap(
+ request,
+ 'callback',
+ thisPlugin._patchCallbackQuery(endSpan)
+ );
+ } else {
+ thisPlugin._diag.error('Expected request.callback to be a function');
+ }
+
+ return api.context.with(
+ api.trace.setSpan(api.context.active(), span),
+ originalMethod,
+ this,
+ ...arguments
+ );
+ }
+
+ Object.defineProperty(patchedMethod, 'length', {
+ value: originalMethod.length,
+ writable: false,
+ });
+
+ return patchedMethod;
+ };
+ }
+
+ private _patchCallbackQuery(endSpan: Function) {
+ return (originalCallback: Function) => {
+ return function (
+ this: any,
+ err: Error | undefined | null,
+ rowCount?: number,
+ rows?: any
+ ) {
+ endSpan(err);
+ return originalCallback.apply(this, arguments);
+ };
+ };
+ }
+}
diff --git a/plugins/node/instrumentation-tedious/src/types.ts b/plugins/node/instrumentation-tedious/src/types.ts
new file mode 100644
index 0000000000..3342997061
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/src/types.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { InstrumentationConfig } from '@opentelemetry/instrumentation';
+
+export type TediousInstrumentationConfig = InstrumentationConfig;
diff --git a/plugins/node/instrumentation-tedious/src/utils.ts b/plugins/node/instrumentation-tedious/src/utils.ts
new file mode 100644
index 0000000000..36e65129a3
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/src/utils.ts
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+/**
+ * The span name SHOULD be set to a low cardinality value
+ * representing the statement executed on the database.
+ *
+ * @returns Operation executed on Tedious Connection. Does not map to SQL statement in any way.
+ */
+export function getSpanName(
+ operation: string,
+ db: string | undefined,
+ sql: string | undefined,
+ bulkLoadTable: string | undefined
+): string {
+ if (operation === 'execBulkLoad' && bulkLoadTable && db) {
+ return `${operation} ${bulkLoadTable} ${db}`;
+ }
+ if (operation === 'callProcedure') {
+ // `sql` refers to procedure name with `callProcedure`
+ if (db) {
+ return `${operation} ${sql} ${db}`;
+ }
+ return `${operation} ${sql}`;
+ }
+ // do not use `sql` in general case because of high-cardinality
+ if (db) {
+ return `${operation} ${db}`;
+ }
+ return `${operation}`;
+}
+
+export const once = (fn: Function) => {
+ let called = false;
+ return (...args: unknown[]) => {
+ if (called) return;
+ called = true;
+ return fn(...args);
+ };
+};
diff --git a/plugins/node/instrumentation-tedious/test/api.ts b/plugins/node/instrumentation-tedious/test/api.ts
new file mode 100644
index 0000000000..d01a986d31
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/test/api.ts
@@ -0,0 +1,333 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as assert from 'assert';
+import { promisify } from 'util';
+import type { Connection, Request, TYPES, ConnectionConfig } from 'tedious';
+
+type Method = keyof Connection & ('execSql' | 'execSqlBatch' | 'prepare');
+export type tedious = {
+ Connection: typeof Connection;
+ Request: typeof Request;
+ TYPES: typeof TYPES;
+ ConnectionConfig: ConnectionConfig;
+};
+
+export const makeApi = (tedious: tedious) => {
+ const fullName = (resource: string) => {
+ assert.strictEqual(typeof resource, 'string');
+ return `[dbo].[${resource}]`;
+ };
+
+ const createConnection = (config: ConnectionConfig): Promise => {
+ return new Promise((resolve, reject) => {
+ const connection = new tedious.Connection(config);
+
+ connection.on('connect', err => {
+ if (err) {
+ return reject(err);
+ }
+ return resolve(connection);
+ });
+
+ // <8.3.0 autoconnects
+ // `state` and `STATE` are private API
+ if ((connection as any).state !== (connection as any).STATE.CONNECTING) {
+ connection.connect();
+ }
+ });
+ };
+ const closeConnection = (connection: Connection): Promise => {
+ assert(connection);
+ assert(connection.once);
+ return new Promise((resolve, reject) => {
+ connection.once('end', err => {
+ if (err) {
+ return reject(err);
+ }
+ return resolve(true);
+ });
+ connection.close();
+ });
+ };
+
+ const query = (
+ connection: Connection,
+ params: string,
+ method: Method = 'execSql'
+ ): Promise => {
+ return new Promise((resolve, reject) => {
+ const result: any[] = [];
+ const request = new tedious.Request(params, (err, rowCount, rows) => {
+ if (err) {
+ return reject(err);
+ } else {
+ resolve(result);
+ }
+ });
+
+ // request.on('returnValue', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'returnValue:'));
+ // request.on('error', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'error:'));
+ // request.on('row', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'row:'));
+ // request.on('done', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'done:'));
+ // request.on('doneInProc', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'doneInProc:'));
+ // request.on('doneProc', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'doneProc:'));
+ // request.on('prepared', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'prepared:'));
+ // request.on('columnMetadata', console.log.bind(console, /*request.sqlTextOrProcedure,*/ 'columnMetadata:'));
+
+ request.on('row', (rows: any[]) => {
+ result.push(...rows.map(r => r.value));
+ });
+
+ connection[method](request);
+ });
+ };
+
+ const storedProcedure = {
+ procedureName: '[dbo].[test_proced]',
+ create: (connection: Connection): Promise => {
+ return new Promise((resolve, reject) => {
+ const sql = `
+ CREATE OR ALTER PROCEDURE ${storedProcedure.procedureName}
+ @inputVal varchar(30),
+ @outputCount int OUTPUT
+ AS
+ set @outputCount = LEN(@inputVal);`.trim();
+
+ const request = new tedious.Request(sql, err => {
+ if (err) {
+ return reject(err);
+ }
+
+ resolve(true);
+ });
+
+ connection.execSql(request);
+ });
+ },
+ call: (connection: Connection): Promise => {
+ return new Promise((resolve, reject) => {
+ const result: any = {};
+ const request = new tedious.Request(
+ storedProcedure.procedureName,
+ err => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(result);
+ }
+ );
+
+ request.addParameter('inputVal', tedious.TYPES.VarChar, 'hello world');
+ request.addOutputParameter('outputCount', tedious.TYPES.Int);
+
+ request.on('returnValue', (paramName, value, metadata) => {
+ result[paramName] = value;
+ });
+
+ connection.callProcedure(request);
+ });
+ },
+ };
+
+ const preparedSQL = {
+ tableName: '[dbo].[test_prepared]',
+ createTable: (connection: Connection): Promise => {
+ return new Promise((resolve, reject) => {
+ const sql = `
+ if not exists(SELECT * FROM sysobjects WHERE name='test_prepared' AND xtype='U')
+ CREATE TABLE ${preparedSQL.tableName} (c1 int, c2 int)`.trim();
+ const request = new tedious.Request(sql, (err, rowCount) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(true);
+ });
+
+ connection.execSql(request);
+ });
+ },
+ prepare: (connection: Connection): Promise => {
+ return new Promise((resolve, reject) => {
+ const sql = `INSERT INTO ${preparedSQL.tableName} VALUES (@val1, @val2)`;
+ const request = new tedious.Request(sql, (err, rowCount) => {
+ if (err) {
+ return reject(err);
+ }
+ });
+
+ // Types for tedious doesn't take this usecase into account, thus the cast to any
+ (request as any).addParameter('val1', tedious.TYPES.Int);
+ (request as any).addParameter('val2', tedious.TYPES.Int);
+
+ request.on('prepared', () => {
+ resolve(request);
+ });
+
+ connection.prepare(request);
+ });
+ },
+ execute: (connection: Connection, request: Request): Promise => {
+ return new Promise((resolve, reject) => {
+ request.on('error', reject);
+ request.on('requestCompleted', () => {
+ resolve(true);
+ });
+ connection.execute(request, { val1: 1, val2: 2 });
+ });
+ },
+ };
+
+ /*
+ Connection has `inTransaction` boolean and `transactionDepth` property, but the
+ reliablility of those are questionable with `abortTransactionOnError` option enabled.
+ Usecases to test for in the future:
+ */
+ const transaction = {
+ tableName: '[dbo].[test_transact]',
+ execute: async (connection: Connection) => {
+ const tx = transaction.api(connection);
+ await tx.begin();
+ await query(
+ connection,
+ `CREATE TABLE ${transaction.tableName} (c1 int UNIQUE)`
+ );
+ await query(
+ connection,
+ `INSERT INTO ${transaction.tableName} VALUES ('1')`
+ );
+ await tx.commit();
+
+ return query(connection, `SELECT * FROM ${transaction.tableName}`);
+ },
+ fail: async (connection: Connection) => {
+ const tx = transaction.api(connection);
+ await tx.begin();
+ await query(
+ connection,
+ `CREATE TABLE ${transaction.tableName} (c1 int UNIQUE)`
+ );
+ await query(
+ connection,
+ `INSERT INTO ${transaction.tableName} VALUES ('1')`
+ );
+ await query(
+ connection,
+ `INSERT INTO ${transaction.tableName} VALUES ('1')`
+ ).catch(() => {});
+ await tx.rollback();
+ return query(connection, `SELECT * FROM ${transaction.tableName}`).catch(
+ () => true
+ );
+ },
+ api: (connection: Connection) => {
+ return {
+ begin: () => {
+ return promisify(connection.beginTransaction).call(connection);
+ },
+ commit: () => {
+ return promisify(connection.commitTransaction).call(connection);
+ },
+ rollback: () => {
+ return promisify(connection.rollbackTransaction).call(connection);
+ },
+ };
+ },
+ };
+
+ const bulkLoad = {
+ tableName: '[dbo].[test_bulk]',
+ createTable: (connection: Connection): Promise => {
+ return new Promise((resolve, reject) => {
+ const sql = `
+ if not exists(SELECT * FROM sysobjects WHERE name='test_bulk' AND xtype='U')
+ CREATE TABLE ${bulkLoad.tableName} ([c1] [int] DEFAULT 58, [c2] [varchar](30))`.trim();
+ const request = new tedious.Request(sql, (err, rowCount) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(true);
+ });
+
+ connection.execSql(request);
+ });
+ },
+ execute: (connection: Connection): Promise => {
+ return new Promise((resolve, reject) => {
+ const requestDoneCb = (err: any, rowCount: number) => {
+ if (err) {
+ return reject(err);
+ }
+ resolve(rowCount);
+ };
+ // <2.2.0 didn't take bulkOptions
+ const request =
+ connection.newBulkLoad.length === 2
+ ? connection.newBulkLoad(bulkLoad.tableName, requestDoneCb)
+ : (connection.newBulkLoad as any)(
+ bulkLoad.tableName,
+ { keepNulls: true },
+ requestDoneCb
+ );
+
+ request.addColumn('c1', tedious.TYPES.Int, { nullable: true });
+ request.addColumn('c2', tedious.TYPES.NVarChar, {
+ length: 50,
+ nullable: true,
+ });
+
+ if (connection.execBulkLoad.length === 1) {
+ // required in <=11.5. not supported in 14
+ request.addRow({ c1: 1 });
+ request.addRow({ c1: 2, c2: 'hello' });
+ return connection.execBulkLoad(request);
+ }
+
+ (connection.execBulkLoad as any)(request, [
+ { c1: 1 },
+ { c1: 2, c2: 'hello' },
+ ]);
+ });
+ },
+ };
+
+ const cleanup = (connection: Connection) => {
+ return query(
+ connection,
+ `
+ if exists(SELECT * FROM sysobjects WHERE name='test_prepared' AND xtype='U') DROP TABLE ${preparedSQL.tableName};
+ if exists(SELECT * FROM sysobjects WHERE name='test_bulk' AND xtype='U') DROP TABLE ${bulkLoad.tableName};
+ if exists(SELECT * FROM sysobjects WHERE name='test_transact' AND xtype='U') DROP TABLE ${transaction.tableName};
+ if exists(SELECT * FROM sysobjects WHERE name='test_proced' AND xtype='U') DROP PROCEDURE ${storedProcedure.procedureName};
+ if exists(SELECT * FROM sys.databases WHERE name = 'temp_otel_db') DROP DATABASE temp_otel_db;
+ `.trim()
+ );
+ };
+
+ return {
+ fullName,
+ bulkLoad,
+ cleanup,
+ closeConnection,
+ createConnection,
+ preparedSQL,
+ query,
+ storedProcedure,
+ transaction,
+ };
+};
+
+export default makeApi;
diff --git a/plugins/node/instrumentation-tedious/test/instrumentation.test.ts b/plugins/node/instrumentation-tedious/test/instrumentation.test.ts
new file mode 100644
index 0000000000..245f2d9f3c
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/test/instrumentation.test.ts
@@ -0,0 +1,393 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { context, trace, SpanStatusCode, SpanKind } from '@opentelemetry/api';
+import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
+import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
+import * as util from 'util';
+import * as testUtils from '@opentelemetry/contrib-test-utils';
+import {
+ BasicTracerProvider,
+ InMemorySpanExporter,
+ ReadableSpan,
+ SimpleSpanProcessor,
+} from '@opentelemetry/sdk-trace-base';
+import * as assert from 'assert';
+import { TediousInstrumentation } from '../src';
+import makeApi from './api';
+import type { Connection, ConnectionConfig } from 'tedious';
+
+process.env.RUN_MSSQL_TESTS = 'true';
+
+const port = Number(process.env.MSSQL_PORT) || 1433;
+const database = process.env.MSSQL_DATABASE || 'master';
+const host = process.env.MSSQL_HOST || '127.0.0.1';
+const user = process.env.MSSQL_USER || 'sa';
+const password = process.env.MSSQL_PASSWORD || 'mssql_passw0rd';
+
+const instrumentation = new TediousInstrumentation();
+instrumentation.enable();
+instrumentation.disable();
+
+const config: ConnectionConfig & { userName: string; password: string } = {
+ userName: user,
+ password,
+ server: host,
+ authentication: {
+ type: 'default',
+ options: {
+ userName: user,
+ password,
+ },
+ },
+ options: {
+ port,
+ database,
+ encrypt: true,
+ // Required for <11.0.8
+ trustServerCertificate: true,
+ rowCollectionOnRequestCompletion: true,
+ rowCollectionOnDone: true,
+ },
+};
+
+describe('tedious', () => {
+ let tedious: any;
+ let contextManager: AsyncHooksContextManager;
+ let connection: Connection;
+ const provider = new BasicTracerProvider();
+ const shouldTest = process.env.RUN_MSSQL_TESTS; // For CI: assumes local db is already available
+ const shouldTestLocally = process.env.RUN_MSSQL_TESTS_LOCAL; // For local: spins up local db via docker
+ const memoryExporter = new InMemorySpanExporter();
+
+ before(function (done) {
+ if (!(shouldTest || shouldTestLocally)) {
+ // this.skip() workaround
+ // https://github.com/mochajs/mocha/issues/2683#issuecomment-375629901
+ this.test!.parent!.pending = true;
+ this.skip();
+ }
+ provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter));
+ if (shouldTestLocally) {
+ testUtils.startDocker('mssql');
+ // wait 15 seconds for docker container to start
+ this.timeout(20000);
+ setTimeout(done, 15000);
+ } else {
+ done();
+ }
+ });
+
+ after(function () {
+ if (shouldTestLocally) {
+ this.timeout(5000);
+ testUtils.cleanUpDocker('mssql');
+ }
+ });
+
+ beforeEach(async function () {
+ // connecting often takes more time even if the DB is running locally
+ this.timeout(10000);
+ instrumentation.disable();
+ contextManager = new AsyncHooksContextManager().enable();
+ context.setGlobalContextManager(contextManager);
+ instrumentation.setTracerProvider(provider);
+ instrumentation.enable();
+ tedious = makeApi(require('tedious'));
+ connection = await tedious.createConnection(config).catch((err: any) => {
+ console.error('with config:', config);
+ throw err;
+ });
+ await tedious.cleanup(connection);
+ memoryExporter.reset();
+ });
+
+ afterEach(async () => {
+ context.disable();
+ memoryExporter.reset();
+ instrumentation.disable();
+ if (connection) {
+ await tedious.closeConnection(connection);
+ }
+ });
+
+ it('should instrument execSql calls', async () => {
+ const queryString = "SELECT 42, 'hello world'";
+ const PARENT_NAME = 'parentSpan';
+ const parentSpan = provider.getTracer('default').startSpan(PARENT_NAME);
+ assert.deepStrictEqual(
+ await context.with(trace.setSpan(context.active(), parentSpan), () =>
+ tedious.query(connection, queryString)
+ ),
+ [42, 'hello world']
+ );
+ parentSpan.end();
+
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, 2, 'Received incorrect number of spans');
+
+ assertSpan(spans[0], {
+ name: 'execSql master',
+ sql: queryString,
+ parentSpan,
+ });
+
+ assert.strictEqual(spans[1].name, PARENT_NAME);
+ });
+
+ it('should catch errors', async () => {
+ const queryString = 'select !';
+
+ await assertRejects(
+ () => tedious.query(connection, queryString),
+ /incorrect syntax/i
+ );
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, 1, 'Received incorrect number of spans');
+
+ assertSpan(spans[0], {
+ name: 'execSql master',
+ sql: queryString,
+ error: /incorrect syntax/i,
+ statementCount: 0,
+ });
+ });
+
+ it('should instrument execSql calls containing multiple queries', async () => {
+ /*
+ Since we do not know how many queries are there without parsing the request
+ there may be cases where there is more than one SQL query done in the context
+ of one span.
+ */
+ const queryString = 'SELECT 42; SELECT 42; SELECT 42;';
+ assert.deepStrictEqual(
+ await tedious.query(connection, queryString),
+ [42, 42, 42]
+ );
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, 1, 'Received incorrect number of spans');
+
+ assertSpan(spans[0], {
+ name: 'execSql master',
+ sql: queryString,
+ procCount: 1,
+ statementCount: 3,
+ });
+ });
+
+ it('should instrument execSqlBatch calls containing multiple queries', async () => {
+ const queryString = 'SELECT 42; SELECT 42; SELECT 42;';
+ assert.deepStrictEqual(
+ await tedious.query(connection, queryString, 'execSqlBatch'),
+ [42, 42, 42]
+ );
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, 1, 'Received incorrect number of spans');
+
+ assertSpan(spans[0], {
+ name: 'execSqlBatch master',
+ sql: queryString,
+ procCount: 0,
+ statementCount: 3,
+ });
+ });
+
+ it('should instrument stored procedure calls', async () => {
+ assert.strictEqual(await tedious.storedProcedure.create(connection), true);
+ assert.deepStrictEqual(await tedious.storedProcedure.call(connection), {
+ outputCount: 11,
+ });
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, 2, 'Received incorrect number of spans');
+
+ assertSpan(spans[0], {
+ name: 'execSql master',
+ sql: /create or alter procedure/i,
+ });
+ assertSpan(spans[1], {
+ name: `callProcedure ${tedious.storedProcedure.procedureName} master`,
+ sql: tedious.storedProcedure.procedureName,
+ });
+ });
+
+ it('should instrument prepared statement calls', async () => {
+ assert.strictEqual(await tedious.preparedSQL.createTable(connection), true);
+ const request = await tedious.preparedSQL.prepare(connection);
+ assert.strictEqual(
+ await tedious.preparedSQL.execute(connection, request),
+ true
+ );
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, 3, 'Received incorrect number of spans');
+
+ assertSpan(spans[0], {
+ name: 'execSql master',
+ sql: /create table/i,
+ statementCount: 2,
+ });
+ assertSpan(spans[1], {
+ name: 'prepare master',
+ sql: /INSERT INTO/,
+ });
+ assertSpan(spans[2], {
+ name: 'execute master',
+ sql: /INSERT INTO/,
+ });
+ });
+
+ it('should track database changes', async () => {
+ const sql = {
+ create: 'create database temp_otel_db;',
+ use: 'use temp_otel_db;',
+ select: "SELECT 42, 'hello world'",
+ };
+ await tedious.query(connection, sql.create);
+ await tedious.query(connection, sql.use);
+ assert.deepStrictEqual(await tedious.query(connection, sql.select), [
+ 42,
+ 'hello world',
+ ]);
+
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, 3, 'Received incorrect number of spans');
+
+ assertSpan(spans[0], {
+ name: 'execSql master',
+ sql: sql.create,
+ });
+ assertSpan(spans[1], {
+ name: 'execSql master',
+ sql: sql.use,
+ });
+ assertSpan(spans[2], {
+ name: 'execSql temp_otel_db',
+ sql: sql.select,
+ database: 'temp_otel_db',
+ });
+ });
+
+ it('should instrument BulkLoads', async () => {
+ assert.strictEqual(await tedious.bulkLoad.createTable(connection), true);
+ assert.strictEqual(await tedious.bulkLoad.execute(connection), 2);
+ const spans = memoryExporter.getFinishedSpans();
+ assert.strictEqual(spans.length, 3, 'Received incorrect number of spans');
+
+ assertSpan(spans[0], {
+ name: 'execSql master',
+ sql: /create table/i,
+ statementCount: 2,
+ });
+ assertSpan(spans[1], {
+ name: 'execSqlBatch master',
+ sql: /insert bulk/,
+ procCount: 0,
+ });
+ assertSpan(spans[2], {
+ name: 'execBulkLoad [dbo].[test_bulk] master',
+ procCount: 0,
+ table: '[dbo].[test_bulk]',
+ });
+ });
+});
+
+const assertMatch = (actual: string | undefined, expected: RegExp) => {
+ assert(
+ actual && expected.test(actual),
+ `Expected ${util.inspect(actual)} to match ${expected}`
+ );
+};
+
+const assertRejects = (
+ asyncFn: () => Promise,
+ expectedMessageRegexp: RegExp | undefined
+) => {
+ const error = new Error('Missing expected rejection.');
+ return Promise.resolve()
+ .then(() => asyncFn())
+ .then(() => {
+ throw error;
+ })
+ .catch(err => {
+ if (err === error) {
+ throw error;
+ }
+ if (expectedMessageRegexp) {
+ assertMatch(err?.message || err, expectedMessageRegexp);
+ }
+ });
+};
+
+function assertSpan(span: ReadableSpan, expected: any) {
+ assert(span);
+ assert.strictEqual(span.name, expected.name);
+ assert.strictEqual(span.kind, SpanKind.CLIENT);
+ assert.strictEqual(span.attributes[SemanticAttributes.DB_SYSTEM], 'mssql');
+ assert.strictEqual(
+ span.attributes[SemanticAttributes.DB_NAME],
+ expected.database ?? database
+ );
+ assert.strictEqual(span.attributes[SemanticAttributes.NET_PEER_PORT], port);
+ assert.strictEqual(span.attributes[SemanticAttributes.NET_PEER_NAME], host);
+ assert.strictEqual(span.attributes[SemanticAttributes.DB_USER], user);
+ assert.strictEqual(
+ span.attributes['tedious.procedure_count'],
+ expected.procCount ?? 1,
+ 'Invalid procedure_count'
+ );
+ assert.strictEqual(
+ span.attributes['tedious.statement_count'],
+ expected.statementCount ?? 1,
+ 'Invalid statement_count'
+ );
+ if (expected.parentSpan) {
+ assert.strictEqual(
+ span.parentSpanId,
+ expected.parentSpan.spanContext().spanId
+ );
+ }
+ assert.strictEqual(
+ span.attributes[SemanticAttributes.DB_SQL_TABLE],
+ expected.table
+ );
+ if (expected.sql) {
+ if (expected.sql instanceof RegExp) {
+ assertMatch(
+ span.attributes[SemanticAttributes.DB_STATEMENT] as string | undefined,
+ expected.sql
+ );
+ } else {
+ assert.strictEqual(
+ span.attributes[SemanticAttributes.DB_STATEMENT],
+ expected.sql
+ );
+ }
+ } else {
+ assert.strictEqual(
+ span.attributes[SemanticAttributes.DB_STATEMENT],
+ undefined
+ );
+ }
+ if (expected.error) {
+ assert(
+ expected.error.test(span.status.message),
+ `Expected "${span.status.message}" to match ${expected.error}`
+ );
+ assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
+ } else {
+ assert.strictEqual(span.status.message, undefined);
+ assert.strictEqual(span.status.code, SpanStatusCode.UNSET);
+ }
+}
diff --git a/plugins/node/instrumentation-tedious/tsconfig.json b/plugins/node/instrumentation-tedious/tsconfig.json
new file mode 100644
index 0000000000..28be80d266
--- /dev/null
+++ b/plugins/node/instrumentation-tedious/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "../../../tsconfig.base",
+ "compilerOptions": {
+ "rootDir": ".",
+ "outDir": "build"
+ },
+ "include": [
+ "src/**/*.ts",
+ "test/**/*.ts"
+ ]
+}