From 3dabf33d6c1e3949f82dee7da6deab0a5ed81d93 Mon Sep 17 00:00:00 2001 From: Siim Kallas Date: Thu, 22 Aug 2024 00:00:18 +0300 Subject: [PATCH 1/5] feat: add sequelize instrumentation --- package-lock.json | 367 +++++++++++- .../instrumentation-sequelize/.eslintignore | 1 + .../instrumentation-sequelize/.eslintrc.js | 7 + .../node/instrumentation-sequelize/LICENSE | 201 +++++++ plugins/node/instrumentation-sequelize/NOTICE | 6 + .../instrumentation-sequelize/package.json | 65 ++ .../instrumentation-sequelize/src/index.ts | 17 + .../src/instrumentation.ts | 252 ++++++++ .../instrumentation-sequelize/src/types.ts | 52 ++ .../instrumentation-sequelize/src/utils.ts | 34 ++ .../test/sequelize.test.ts | 559 ++++++++++++++++++ .../instrumentation-sequelize/tsconfig.json | 11 + 12 files changed, 1552 insertions(+), 20 deletions(-) create mode 100644 plugins/node/instrumentation-sequelize/.eslintignore create mode 100644 plugins/node/instrumentation-sequelize/.eslintrc.js create mode 100644 plugins/node/instrumentation-sequelize/LICENSE create mode 100644 plugins/node/instrumentation-sequelize/NOTICE create mode 100644 plugins/node/instrumentation-sequelize/package.json create mode 100644 plugins/node/instrumentation-sequelize/src/index.ts create mode 100644 plugins/node/instrumentation-sequelize/src/instrumentation.ts create mode 100644 plugins/node/instrumentation-sequelize/src/types.ts create mode 100644 plugins/node/instrumentation-sequelize/src/utils.ts create mode 100644 plugins/node/instrumentation-sequelize/test/sequelize.test.ts create mode 100644 plugins/node/instrumentation-sequelize/tsconfig.json diff --git a/package-lock.json b/package-lock.json index da3f5cf81a..6c386f7f29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10606,6 +10606,10 @@ "resolved": "plugins/node/instrumentation-runtime-node", "link": true }, + "node_modules/@opentelemetry/instrumentation-sequelize": { + "resolved": "plugins/node/instrumentation-sequelize", + "link": true + }, "node_modules/@opentelemetry/instrumentation-socket.io": { "resolved": "plugins/node/instrumentation-socket.io", "link": true @@ -12520,6 +12524,16 @@ "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", "dev": true }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -12741,6 +12755,13 @@ "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", "dev": true }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mysql": { "version": "2.15.26", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", @@ -12962,6 +12983,13 @@ "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -18429,6 +18457,13 @@ "node": ">=10" } }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dtrace-provider": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", @@ -21912,6 +21947,16 @@ "node": ">= 0.8.0" } }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "dev": true, + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -27245,7 +27290,19 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "dev": true, - "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, "engines": { "node": "*" } @@ -32238,6 +32295,13 @@ "node": ">= 4" } }, + "node_modules/retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==", + "dev": true, + "license": "MIT" + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -32610,6 +32674,86 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", "dev": true }, + "node_modules/sequelize": { + "version": "6.37.3", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz", + "integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==", + "dev": true, + "license": "MIT" + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -34697,6 +34841,13 @@ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", "dev": true }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "dev": true, + "license": "MIT" + }, "node_modules/tr46": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", @@ -35627,6 +35778,16 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -36124,6 +36285,16 @@ "node": ">=0.1.90" } }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -37493,7 +37664,7 @@ "@opentelemetry/contrib-test-utils": "^0.40.0", "@types/mocha": "^9.1.1", "@types/node": "18.6.5", - "@types/sinon": "^10.0.20", + "@types/sinon": "^10.0.11", "expect": "29.2.0", "mocha": "7.2.0", "nyc": "15.1.0", @@ -37591,7 +37762,7 @@ "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "^0.52.0", - "winston-transport": "^4.7.1" + "winston-transport": "4.*" }, "devDependencies": { "@types/mocha": "7.0.2", @@ -37734,7 +37905,7 @@ "@opentelemetry/sdk-trace-node": "^1.8.0", "@types/mocha": "7.0.2", "@types/node": "18.6.5", - "@types/sinon": "^10.0.20", + "@types/sinon": "^10.0.11", "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "5.0.10", @@ -37763,7 +37934,7 @@ "@opentelemetry/sdk-trace-base": "^1.24.0", "@types/mocha": "7.0.2", "@types/node": "18.6.5", - "@types/sinon": "^10.0.20", + "@types/sinon": "^10.0.11", "kafkajs": "^2.2.4", "mocha": "7.2.0", "nyc": "15.1.0", @@ -37891,6 +38062,36 @@ "undici-types": "~5.26.4" } }, + "plugins/node/instrumentation-sequelize": { + "name": "@opentelemetry/instrumentation-sequelize", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.1", + "@opentelemetry/semantic-conventions": "^1.25.1" + }, + "devDependencies": { + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/contrib-test-utils": "^0.40.0", + "@opentelemetry/sdk-trace-base": "^1.25.1", + "@types/mocha": "7.0.2", + "@types/node": "18.6.5", + "@types/sinon": "^10.0.11", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "5.0.10", + "sequelize": "^6.37.3", + "sinon": "15.2.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "plugins/node/instrumentation-socket.io": { "name": "@opentelemetry/instrumentation-socket.io", "version": "0.41.0", @@ -37909,8 +38110,8 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "5.0.10", - "socket.io": "^4.7.5", - "socket.io-client": "^4.7.5", + "socket.io": "^4.1.3", + "socket.io-client": "^4.1.3", "test-all-versions": "6.1.0", "ts-mocha": "10.0.0", "typescript": "4.4.4" @@ -39458,13 +39659,13 @@ "@opentelemetry/api": "^1.3.0", "@rollup/plugin-commonjs": "^26.0.0", "@rollup/plugin-node-resolve": "^15.2.3", - "@types/chai": "^4.3.17", + "@types/chai": "^4.3.10", "@types/mocha": "8.2.3", "@types/node": "18.6.5", "@types/sinon": "10.0.20", "@web/dev-server-esbuild": "^1.0.1", - "@web/dev-server-rollup": "^0.6.4", - "@web/test-runner": "^0.18.3", + "@web/dev-server-rollup": "^0.6.1", + "@web/test-runner": "^0.18.0", "chai": "^4.3.10", "sinon": "15.2.0", "typescript": "4.4.4" @@ -51260,13 +51461,13 @@ "@opentelemetry/semantic-conventions": "^1.22.0", "@rollup/plugin-commonjs": "^26.0.0", "@rollup/plugin-node-resolve": "^15.2.3", - "@types/chai": "^4.3.17", + "@types/chai": "^4.3.10", "@types/mocha": "8.2.3", "@types/node": "18.6.5", "@types/sinon": "10.0.20", "@web/dev-server-esbuild": "^1.0.1", - "@web/dev-server-rollup": "^0.6.4", - "@web/test-runner": "^0.18.3", + "@web/dev-server-rollup": "^0.6.1", + "@web/test-runner": "^0.18.0", "chai": "^4.3.10", "sinon": "15.2.0", "typescript": "4.4.4" @@ -51363,7 +51564,7 @@ "@opentelemetry/sdk-trace-node": "^1.8.0", "@types/mocha": "7.0.2", "@types/node": "18.6.5", - "@types/sinon": "^10.0.20", + "@types/sinon": "^10.0.11", "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "5.0.10", @@ -51498,7 +51699,7 @@ "@opentelemetry/semantic-conventions": "^1.24.0", "@types/mocha": "7.0.2", "@types/node": "18.6.5", - "@types/sinon": "^10.0.20", + "@types/sinon": "^10.0.11", "kafkajs": "^2.2.4", "mocha": "7.2.0", "nyc": "15.1.0", @@ -52502,6 +52703,26 @@ } } }, + "@opentelemetry/instrumentation-sequelize": { + "version": "file:plugins/node/instrumentation-sequelize", + "requires": { + "@opentelemetry/api": "^1.3.0", + "@opentelemetry/contrib-test-utils": "^0.40.0", + "@opentelemetry/instrumentation": "^0.52.1", + "@opentelemetry/sdk-trace-base": "^1.25.1", + "@opentelemetry/semantic-conventions": "^1.25.1", + "@types/mocha": "7.0.2", + "@types/node": "18.6.5", + "@types/sinon": "^10.0.11", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "5.0.10", + "sequelize": "^6.37.3", + "sinon": "15.2.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + } + }, "@opentelemetry/instrumentation-socket.io": { "version": "file:plugins/node/instrumentation-socket.io", "requires": { @@ -52516,8 +52737,8 @@ "mocha": "7.2.0", "nyc": "15.1.0", "rimraf": "5.0.10", - "socket.io": "^4.7.5", - "socket.io-client": "^4.7.5", + "socket.io": "^4.1.3", + "socket.io-client": "^4.1.3", "test-all-versions": "6.1.0", "ts-mocha": "10.0.0", "typescript": "4.4.4" @@ -53798,7 +54019,7 @@ "@opentelemetry/contrib-test-utils": "^0.40.0", "@types/mocha": "^9.1.1", "@types/node": "18.6.5", - "@types/sinon": "^10.0.20", + "@types/sinon": "^10.0.11", "expect": "29.2.0", "mocha": "7.2.0", "nyc": "15.1.0", @@ -55032,7 +55253,7 @@ "sinon": "15.2.0", "ts-mocha": "10.0.0", "typescript": "4.4.4", - "winston-transport": "^4.7.1" + "winston-transport": "4.*" }, "dependencies": { "@types/triple-beam": { @@ -56313,6 +56534,15 @@ "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", "dev": true }, + "@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, "@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -56531,6 +56761,12 @@ "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==", "dev": true }, + "@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true + }, "@types/mysql": { "version": "2.15.26", "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", @@ -56754,6 +56990,12 @@ "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", "dev": true }, + "@types/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==", + "dev": true + }, "@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -61174,6 +61416,12 @@ "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", "dev": true }, + "dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", + "dev": true + }, "dtrace-provider": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", @@ -63881,6 +64129,12 @@ "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", "dev": true }, + "inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -68055,8 +68309,16 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "dev": true + }, + "moment-timezone": { + "version": "0.5.45", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.45.tgz", + "integrity": "sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==", "dev": true, - "optional": true + "requires": { + "moment": "^2.29.4" + } }, "mongodb": { "version": "4.17.2", @@ -71922,6 +72184,12 @@ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true }, + "retry-as-promised": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.0.4.tgz", + "integrity": "sha512-XgmCoxKWkDofwH8WddD0w85ZfqYz+ZHlr5yo+3YUCfycWawU56T5ckWXsScsj5B8tqUcIG67DxXByo3VUgiAdA==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -72212,6 +72480,44 @@ "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", "dev": true }, + "sequelize": { + "version": "6.37.3", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.3.tgz", + "integrity": "sha512-V2FTqYpdZjPy3VQrZvjTPnOoLm0KudCRXfGWp48QwhyPPp2yW8z0p0sCYZd/em847Tl2dVxJJ1DR+hF+O77T7A==", + "dev": true, + "requires": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "dependencies": { + "pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==", + "dev": true + } + } + }, + "sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "dev": true + }, "serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -73803,6 +74109,12 @@ "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", "dev": true }, + "toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "dev": true + }, "tr46": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", @@ -74493,6 +74805,12 @@ "builtins": "^5.0.0" } }, + "validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dev": true + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -74874,6 +75192,15 @@ } } }, + "wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/plugins/node/instrumentation-sequelize/.eslintignore b/plugins/node/instrumentation-sequelize/.eslintignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/plugins/node/instrumentation-sequelize/.eslintignore @@ -0,0 +1 @@ +build diff --git a/plugins/node/instrumentation-sequelize/.eslintrc.js b/plugins/node/instrumentation-sequelize/.eslintrc.js new file mode 100644 index 0000000000..f756f4488b --- /dev/null +++ b/plugins/node/instrumentation-sequelize/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + "env": { + "mocha": true, + "node": true + }, + ...require('../../../eslint.config.js') +} diff --git a/plugins/node/instrumentation-sequelize/LICENSE b/plugins/node/instrumentation-sequelize/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/node/instrumentation-sequelize/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-sequelize/NOTICE b/plugins/node/instrumentation-sequelize/NOTICE new file mode 100644 index 0000000000..4fee72c6a6 --- /dev/null +++ b/plugins/node/instrumentation-sequelize/NOTICE @@ -0,0 +1,6 @@ +[Based on the instrumentation written by Aspecto](https://github.com/aspecto-io/opentelemetry-ext-js/tree/master/packages/instrumentation-sequelize). + +The library contains the following changes compared to the original: +* Removed `moduleVersionAttributeName` configuration option. +* Various type declaration fixes. +* Upgraded to latest instrumentation and semantic conventions. \ No newline at end of file diff --git a/plugins/node/instrumentation-sequelize/package.json b/plugins/node/instrumentation-sequelize/package.json new file mode 100644 index 0000000000..48c85ed58d --- /dev/null +++ b/plugins/node/instrumentation-sequelize/package.json @@ -0,0 +1,65 @@ +{ + "name": "@opentelemetry/instrumentation-sequelize", + "version": "0.1.0", + "description": "OpenTelemetry instrumentation for `sequelize` ORM", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js-contrib", + "scripts": { + "test": "ts-mocha --require @opentelemetry/contrib-test-utils -p tsconfig.json 'test/**/*.test.ts'", + "tdd": "npm run test -- --watch-extensions ts --watch", + "clean": "rimraf build/*", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "lint:readme": "node ../../../scripts/lint-readme", + "precompile": "tsc --version && lerna run version:update --scope @opentelemetry/instrumentation-sequelize --include-dependencies", + "prewatch": "npm run precompile", + "prepublishOnly": "npm run compile", + "version:update": "node ../../../scripts/version-update.js", + "compile": "tsc -p ." + }, + "keywords": [ + "sequelize", + "instrumentation", + "nodejs", + "opentelemetry", + "profiling", + "tracing" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "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/contrib-test-utils": "^0.40.0", + "@opentelemetry/sdk-trace-base": "^1.25.1", + "@types/mocha": "7.0.2", + "@types/node": "18.6.5", + "@types/sinon": "^10.0.11", + "mocha": "7.2.0", + "nyc": "15.1.0", + "rimraf": "5.0.10", + "sequelize": "^6.37.3", + "sinon": "15.2.0", + "ts-mocha": "10.0.0", + "typescript": "4.4.4" + }, + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.1", + "@opentelemetry/semantic-conventions": "^1.25.1" + }, + "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-sequelize#readme" +} diff --git a/plugins/node/instrumentation-sequelize/src/index.ts b/plugins/node/instrumentation-sequelize/src/index.ts new file mode 100644 index 0000000000..7d06c03d6e --- /dev/null +++ b/plugins/node/instrumentation-sequelize/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright The OpenTelemetry Authors, Aspecto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './instrumentation'; +export * from './types'; diff --git a/plugins/node/instrumentation-sequelize/src/instrumentation.ts b/plugins/node/instrumentation-sequelize/src/instrumentation.ts new file mode 100644 index 0000000000..354a48589a --- /dev/null +++ b/plugins/node/instrumentation-sequelize/src/instrumentation.ts @@ -0,0 +1,252 @@ +/* + * Copyright The OpenTelemetry Authors, Aspecto + * + * 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, + Span, + SpanKind, + SpanStatusCode, + trace, + diag, +} from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; +import { + NETTRANSPORTVALUES_IP_TCP, + SEMATTRS_DB_NAME, + SEMATTRS_DB_OPERATION, + SEMATTRS_DB_SQL_TABLE, + SEMATTRS_DB_STATEMENT, + SEMATTRS_DB_SYSTEM, + SEMATTRS_DB_USER, + SEMATTRS_NET_PEER_NAME, + SEMATTRS_NET_PEER_PORT, + SEMATTRS_NET_TRANSPORT, +} from '@opentelemetry/semantic-conventions'; +import type * as sequelize from 'sequelize'; +import { SequelizeInstrumentationConfig } from './types'; +import { PACKAGE_NAME, PACKAGE_VERSION } from './version'; +import { extractTableFromQuery } from './utils'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; + +export class SequelizeInstrumentation extends InstrumentationBase { + static readonly component = 'sequelize'; + static readonly supportedVersions = '>=6 <7'; + + constructor(config: SequelizeInstrumentationConfig = {}) { + super(PACKAGE_NAME, PACKAGE_VERSION, config); + } + + protected init() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const unpatchConnectionManager = (moduleExports: any) => { + if ( + isWrapped(moduleExports?.ConnectionManager?.prototype?.getConnection) + ) { + this._unwrap( + moduleExports.ConnectionManager.prototype, + 'getConnection' + ); + } + return moduleExports; + }; + const connectionManagerInstrumentation = new InstrumentationNodeModuleFile( + 'sequelize/lib/dialects/abstract/connection-manager.js', + [SequelizeInstrumentation.supportedVersions], + moduleExports => { + if (moduleExports === undefined || moduleExports === null) { + return moduleExports; + } + unpatchConnectionManager(moduleExports); + this._wrap( + moduleExports.ConnectionManager.prototype, + 'getConnection', + this._getConnectionPatch() + ); + return moduleExports; + }, + unpatchConnectionManager + ); + + const unpatch = (moduleExports: typeof sequelize) => { + if (isWrapped(moduleExports.Sequelize.prototype.query)) { + this._unwrap(moduleExports.Sequelize.prototype, 'query'); + } + }; + const module = new InstrumentationNodeModuleDefinition( + SequelizeInstrumentation.component, + [SequelizeInstrumentation.supportedVersions], + moduleExports => { + if (moduleExports === undefined || moduleExports === null) { + return moduleExports; + } + + unpatch(moduleExports); + this._wrap( + moduleExports.Sequelize.prototype, + 'query', + this._createQueryPatch() + ); + + return moduleExports; + }, + unpatch, + [connectionManagerInstrumentation] + ); + return module; + } + + // run getConnection with suppressTracing, as it might call internally to `databaseVersion` function + // which calls `query` and create internal span which we don't need to instrument + private _getConnectionPatch() { + return (original: Function) => { + return function (this: unknown, ...args: unknown[]) { + return context.with(suppressTracing(context.active()), () => + original.apply(this, args) + ); + }; + }; + } + + private _createQueryPatch() { + const self = this; + return (original: sequelize.Sequelize['query']) => { + return function query( + this: sequelize.Sequelize, + ...args: Parameters + ) { + if ( + self.getConfig().ignoreOrphanedSpans && + !trace.getSpan(context.active()) + ) { + return original.apply(this, args); + } + + const sqlOrQuery = args[0]; + const extractStatement = (sql: typeof sqlOrQuery) => { + if (typeof sql === 'string') return sql; + return sql?.query || ''; + }; + const statement = extractStatement(args[0]).trim(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const option = args[1] as any; + let operation = option?.type; + + if (!operation) operation = statement.split(' ')[0]; + + const sequelizeInstance: sequelize.Sequelize = this; + const config = sequelizeInstance?.config; + + let tableName = option?.instance?.constructor?.tableName; + if (!tableName) { + if (Array.isArray(option?.tableNames) && option.tableNames.length > 0) + tableName = option?.tableNames.sort().join(','); + else tableName = extractTableFromQuery(statement); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const attributes: Record = { + [SEMATTRS_DB_SYSTEM]: sequelizeInstance.getDialect(), + [SEMATTRS_DB_USER]: config?.username, + [SEMATTRS_NET_PEER_NAME]: config?.host, + [SEMATTRS_NET_PEER_PORT]: config?.port + ? Number(config?.port) + : undefined, + [SEMATTRS_NET_TRANSPORT]: self._getNetTransport(config?.protocol), + [SEMATTRS_DB_NAME]: config?.database, + [SEMATTRS_DB_OPERATION]: operation, + [SEMATTRS_DB_STATEMENT]: statement, + [SEMATTRS_DB_SQL_TABLE]: tableName, + }; + + Object.entries(attributes).forEach(([key, value]) => { + if (value === undefined) delete attributes[key]; + }); + + const newSpan: Span = self.tracer.startSpan(`Sequelize ${operation}`, { + kind: SpanKind.CLIENT, + attributes, + }); + + const activeContextWithSpan = trace.setSpan(context.active(), newSpan); + + const hook = self.getConfig().queryHook; + if (hook !== undefined && sqlOrQuery !== undefined) { + safeExecuteInTheMiddle( + () => hook(newSpan, { sql: sqlOrQuery, option }), + e => { + if (e) + diag.error('sequelize instrumentation: queryHook error', e); + }, + true + ); + } + + return ( + context + .with( + self.getConfig().suppressInternalInstrumentation + ? suppressTracing(activeContextWithSpan) + : activeContextWithSpan, + () => original.apply(this, args) + ) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .then((response: any) => { + const responseHook = self.getConfig().responseHook; + if (responseHook !== undefined) { + safeExecuteInTheMiddle( + () => responseHook(newSpan, response), + e => { + if (e) + diag.error( + 'sequelize instrumentation: responseHook error', + e + ); + }, + true + ); + } + return response; + }) + .catch((err: Error) => { + newSpan.setStatus({ + code: SpanStatusCode.ERROR, + message: err.message, + }); + throw err; + }) + .finally(() => { + newSpan.end(); + }) + ); + }; + }; + } + + private _getNetTransport(protocol: string) { + switch (protocol) { + case 'tcp': + return NETTRANSPORTVALUES_IP_TCP; + default: + return undefined; + } + } +} diff --git a/plugins/node/instrumentation-sequelize/src/types.ts b/plugins/node/instrumentation-sequelize/src/types.ts new file mode 100644 index 0000000000..287f0becd6 --- /dev/null +++ b/plugins/node/instrumentation-sequelize/src/types.ts @@ -0,0 +1,52 @@ +/* + * Copyright The OpenTelemetry Authors, Aspecto + * + * 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 { Span } from '@opentelemetry/api'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +export interface SequelizeQueryHookParams { + /** The type of sql parameter depends on the database dialect. */ + sql: string | { query: string; values: unknown[] }; + /** The type of option parameter depends on the database dialect. */ + option: any; +} + +export type SequelizeQueryHook = ( + span: Span, + params: T +) => void; + +export type SequelizeResponseCustomAttributesFunction = ( + span: Span, + response: any +) => void; + +export interface SequelizeInstrumentationConfig extends InstrumentationConfig { + /** Hook for adding custom attributes using the query */ + queryHook?: SequelizeQueryHook; + /** Hook for adding custom attributes using the response payload */ + responseHook?: SequelizeResponseCustomAttributesFunction; + /** Set to true if you only want to trace operation which has parent spans */ + ignoreOrphanedSpans?: boolean; + /** + * Sequelize operation use postgres/mysql/mariadb/etc. under the hood. + * If, for example, postgres instrumentation is enabled, a postgres operation will also create + * a postgres span describing the communication. + * Setting the `suppressInternalInstrumentation` config value to `true` will + * cause the instrumentation to suppress instrumentation of underlying operations. + */ + suppressInternalInstrumentation?: boolean; +} diff --git a/plugins/node/instrumentation-sequelize/src/utils.ts b/plugins/node/instrumentation-sequelize/src/utils.ts new file mode 100644 index 0000000000..d31f7541d0 --- /dev/null +++ b/plugins/node/instrumentation-sequelize/src/utils.ts @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors, Aspecto + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const extractTableFromQuery = (query: string | null | undefined) => { + try { + const result = query?.match(/(?<=from|join|truncate)\s+"?`?(\w+)"?`?/gi); + if (!Array.isArray(result)) return; + + return result + .map(table => + table + .trim() + .replace(/^"(.*)"$/, '$1') + .replace(/^`(.*)`$/, '$1') + ) + .sort() + .join(','); + } catch { + return; + } +}; diff --git a/plugins/node/instrumentation-sequelize/test/sequelize.test.ts b/plugins/node/instrumentation-sequelize/test/sequelize.test.ts new file mode 100644 index 0000000000..72062feb7e --- /dev/null +++ b/plugins/node/instrumentation-sequelize/test/sequelize.test.ts @@ -0,0 +1,559 @@ +/* + * 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 { SequelizeInstrumentation } from '../src'; +import { extractTableFromQuery } from '../src/utils'; +import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import { + context, + diag, + SpanStatusCode, + DiagConsoleLogger, + ROOT_CONTEXT, +} from '@opentelemetry/api'; +import { + SEMATTRS_DB_NAME, + SEMATTRS_DB_STATEMENT, + SEMATTRS_DB_SQL_TABLE, + SEMATTRS_NET_PEER_NAME, + SEMATTRS_NET_PEER_PORT, + SEMATTRS_DB_OPERATION, + SEMATTRS_DB_SYSTEM, + SEMATTRS_DB_USER, +} from '@opentelemetry/semantic-conventions'; +import { + getTestSpans, + registerInstrumentationTesting, +} from '@opentelemetry/contrib-test-utils'; + +const instrumentation = registerInstrumentationTesting( + new SequelizeInstrumentation() +); + +import * as sequelize from 'sequelize'; + +type QueryFunction = typeof sequelize.Sequelize.prototype.query; + +describe('instrumentation-sequelize', () => { + const getSequelizeSpans = (): ReadableSpan[] => { + return getTestSpans().filter(s => + s.instrumentationLibrary.name.includes('sequelize') + ) as ReadableSpan[]; + }; + + beforeEach(() => { + instrumentation.enable(); + }); + + afterEach(() => { + instrumentation.disable(); + }); + + describe('postgres', () => { + const DB_SYSTEM = 'postgres'; + const DB_USER = 'some-user'; + const NET_PEER_NAME = 'localhost'; + const NET_PEER_PORT = 12345; + const DB_NAME = 'my-db'; + + const instance = new sequelize.Sequelize( + `${DB_SYSTEM}://${DB_USER}@${NET_PEER_NAME}:${NET_PEER_PORT}/${DB_NAME}`, + { logging: false } + ); + class User extends sequelize.Model { + firstName: string = ''; + } + + User.init( + { firstName: { type: sequelize.DataTypes.STRING } }, + { sequelize: instance } + ); + + it('create is instrumented', async () => { + try { + await User.create({ firstName: 'OpenTelemetry' }); + } catch { + // Error is thrown but we don't care + } + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].status.code, SpanStatusCode.ERROR); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_SYSTEM], DB_SYSTEM); + assert.strictEqual(attributes[SEMATTRS_DB_USER], DB_USER); + assert.strictEqual(attributes[SEMATTRS_NET_PEER_NAME], NET_PEER_NAME); + assert.strictEqual(attributes[SEMATTRS_NET_PEER_PORT], NET_PEER_PORT); + assert.strictEqual(attributes[SEMATTRS_DB_NAME], DB_NAME); + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'INSERT'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); + assert.match( + attributes[SEMATTRS_DB_STATEMENT] as string, + /INSERT INTO "Users" \("id","firstName","createdAt","updatedAt"\) VALUES \(DEFAULT,\$1,\$2,\$3\) RETURNING (\*|"id","firstName","createdAt","updatedAt");/, + ); + }); + + it('findAll is instrumented', async () => { + await User.findAll().catch(() => {}); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'SELECT'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); + assert.strictEqual( + attributes[SEMATTRS_DB_STATEMENT], + 'SELECT "id", "firstName", "createdAt", "updatedAt" FROM "Users" AS "User";' + ); + }); + + it('destroy is instrumented', async () => { + await User.destroy({ where: {}, truncate: true }).catch(() => {}); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'BULKDELETE'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); + assert.strictEqual(attributes[SEMATTRS_DB_STATEMENT], 'TRUNCATE "Users"'); + }); + + it('count is instrumented', async () => { + await User.count().catch(() => {}); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'SELECT'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); + assert.strictEqual( + attributes[SEMATTRS_DB_STATEMENT], + 'SELECT count(*) AS "count" FROM "Users" AS "User";' + ); + }); + + it('handled complex query', async () => { + const Op = sequelize.Op; + await User.findOne({ + where: { + username: 'Shlomi', + rank: { + [Op.or]: { + [Op.lt]: 1000, + [Op.eq]: null, + }, + }, + }, + attributes: ['id', 'username'], + order: [['username', 'DESC']], + limit: 10, + offset: 5, + }).catch(() => {}); + + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'SELECT'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); + assert.strictEqual( + attributes[SEMATTRS_DB_STATEMENT], + 'SELECT "id", "username" FROM "Users" AS "User" WHERE "User"."username" = \'Shlomi\' AND ("User"."rank" < 1000 OR "User"."rank" IS NULL) ORDER BY "User"."username" DESC LIMIT 10 OFFSET 5;' + ); + }); + + it('tableName is taken from init override', async () => { + class Planet extends sequelize.Model {} + const expectedTableName = 'solar-system'; + Planet.init({}, { sequelize: instance, tableName: expectedTableName }); + + await Planet.findAll().catch(() => {}); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], expectedTableName); + }); + + it('handles JOIN queries', async () => { + class Dog extends sequelize.Model { + firstName: string = ''; + } + + Dog.init( + { + firstName: { type: sequelize.DataTypes.STRING }, + owner: { type: sequelize.DataTypes.STRING }, + }, + { sequelize: instance } + ); + Dog.belongsTo(User, { foreignKey: 'firstName' }); + User.hasMany(Dog, { foreignKey: 'firstName' }); + + await Dog.findOne({ + attributes: ['firstName', 'owner'], + include: [ + { + model: User, + attributes: ['firstName'], + required: true, + }, + ], + }).catch(() => {}); + + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'SELECT'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Dogs,Users'); + assert.strictEqual( + attributes[SEMATTRS_DB_STATEMENT], + 'SELECT "Dog"."id", "Dog"."firstName", "Dog"."owner", "User"."id" AS "User.id", "User"."firstName" AS "User.firstName" FROM "Dogs" AS "Dog" INNER JOIN "Users" AS "User" ON "Dog"."firstName" = "User"."id" LIMIT 1;' + ); + }); + }); + + describe('mysql', () => { + const DB_SYSTEM = 'mysql'; + const DB_USER = 'RickSanchez'; + const NET_PEER_NAME = 'localhost'; + const NET_PEER_PORT = 34567; + const DB_NAME = 'mysql-db'; + + const instance = new sequelize.Sequelize(DB_NAME, DB_USER, 'password', { + host: NET_PEER_NAME, + port: NET_PEER_PORT, + dialect: DB_SYSTEM, + }); + + instance.define('User', { + firstName: { type: sequelize.DataTypes.STRING }, + }); + + it('create is instrumented', async () => { + await instance.models.User.create({ firstName: 'OpenTelemetry' }).catch( + () => {} + ); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual(spans[0].status.code, SpanStatusCode.ERROR); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_SYSTEM], DB_SYSTEM); + assert.strictEqual(attributes[SEMATTRS_DB_USER], DB_USER); + assert.strictEqual(attributes[SEMATTRS_NET_PEER_NAME], NET_PEER_NAME); + assert.strictEqual(attributes[SEMATTRS_NET_PEER_PORT], NET_PEER_PORT); + assert.strictEqual(attributes[SEMATTRS_DB_NAME], DB_NAME); + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'INSERT'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); + assert.strictEqual( + attributes[SEMATTRS_DB_STATEMENT], + 'INSERT INTO `Users` (`id`,`firstName`,`createdAt`,`updatedAt`) VALUES (DEFAULT,$1,$2,$3);' + ); + }); + + it('findAll is instrumented', async () => { + await instance.models.User.findAll().catch(() => {}); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'SELECT'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); + assert.strictEqual( + attributes[SEMATTRS_DB_STATEMENT], + 'SELECT `id`, `firstName`, `createdAt`, `updatedAt` FROM `Users` AS `User`;' + ); + }); + + describe('query is instrumented', () => { + it('with options not specified', async () => { + try { + await instance.query('SELECT 1 + 1'); + } catch { + // Do not care about the error + } + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'SELECT'); + assert.strictEqual(attributes[SEMATTRS_DB_STATEMENT], 'SELECT 1 + 1'); + }); + it('with type not specified in options', async () => { + try { + await instance.query('SELECT 1 + 1', {}); + } catch { + // Do not care about the error + } + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'SELECT'); + assert.strictEqual(attributes[SEMATTRS_DB_STATEMENT], 'SELECT 1 + 1'); + }); + + it('with type specified in options', async () => { + try { + await instance.query('SELECT 1 + 1', { + type: sequelize.QueryTypes.RAW, + }); + } catch { + // Do not care about the error + } + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'RAW'); + assert.strictEqual(attributes[SEMATTRS_DB_STATEMENT], 'SELECT 1 + 1'); + }); + }); + }); + + describe('sqlite', () => { + const instance = new sequelize.Sequelize('sqlite:memory', { + logging: false, + }); + instance.define('User', { + firstName: { type: sequelize.DataTypes.STRING }, + }); + + it('create is instrumented', async () => { + await instance.models.User.create({ firstName: 'OpenTelemetry' }).catch( + () => {} + ); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + const attributes = spans[0].attributes; + assert.strictEqual(attributes[SEMATTRS_DB_SYSTEM], 'sqlite'); + assert.strictEqual(attributes[SEMATTRS_NET_PEER_NAME], 'memory'); + assert.strictEqual(attributes[SEMATTRS_DB_OPERATION], 'INSERT'); + assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); + assert.strictEqual( + attributes[SEMATTRS_DB_STATEMENT], + 'INSERT INTO `Users` (`id`,`firstName`,`createdAt`,`updatedAt`) VALUES (NULL,$1,$2,$3);' + ); + }); + }); + + describe('config', () => { + describe('queryHook', () => { + it('able to collect query', async () => { + instrumentation.disable(); + const instance = new sequelize.Sequelize( + 'postgres://john@$localhost:1111/my-name', + { logging: false } + ); + instance.define('User', { + firstName: { type: sequelize.DataTypes.STRING }, + }); + + const response = { john: 'doe' }; + sequelize.Sequelize.prototype.query = (() => { + return new Promise(resolve => resolve(response)); + }) as QueryFunction; + instrumentation.setConfig({ + queryHook: (span, { sql, option }: { sql: any; option: any }) => { + span.setAttribute('test-sql', 'any'); + span.setAttribute('test-option', 'any'); + }, + }); + instrumentation.enable(); + + await instance.models.User.findAll(); + const spans = getSequelizeSpans(); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes['test-sql'], 'any'); + assert.strictEqual(attributes['test-option'], 'any'); + }); + + it('query hook which throws does not affect span', async () => { + instrumentation.disable(); + const instance = new sequelize.Sequelize( + 'postgres://john@$localhost:1111/my-name', + { logging: false } + ); + instance.define('User', { + firstName: { type: sequelize.DataTypes.STRING }, + }); + + const response = { john: 'doe' }; + sequelize.Sequelize.prototype.query = (() => { + return new Promise(resolve => resolve(response)); + }) as QueryFunction; + const mockedLogger = (() => { + let message: string; + let error: Error; + return { + error: (_message: string, _err: Error) => { + message = _message; + error = _err; + }, + debug: () => {}, + getMessage: () => message, + getError: () => error, + }; + })(); + + instrumentation.setConfig({ + queryHook: () => { + throw new Error('Throwing'); + }, + }); + instrumentation.enable(); + diag.setLogger(mockedLogger as any); + await instance.models.User.findAll(); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual( + mockedLogger.getMessage(), + 'sequelize instrumentation: queryHook error' + ); + assert.strictEqual(mockedLogger.getError().message, 'Throwing'); + diag.setLogger(new DiagConsoleLogger()); + }); + }); + + describe('responseHook', () => { + it('able to collect response', async () => { + instrumentation.disable(); + const instance = new sequelize.Sequelize( + 'postgres://john@$localhost:1111/my-name', + { logging: false } + ); + instance.define('User', { + firstName: { type: sequelize.DataTypes.STRING }, + }); + + const response = { john: 'doe' }; + sequelize.Sequelize.prototype.query = (() => { + return new Promise(resolve => resolve(response)); + }) as QueryFunction; + instrumentation.setConfig({ + responseHook: (span, response) => { + span.setAttribute('test', JSON.stringify(response)); + }, + }); + instrumentation.enable(); + + await instance.models.User.findAll(); + const spans = getSequelizeSpans(); + const attributes = spans[0].attributes; + + assert.strictEqual(attributes['test'], JSON.stringify(response)); + }); + + it('response hook which throws does not affect span', async () => { + instrumentation.disable(); + const instance = new sequelize.Sequelize( + 'postgres://john@$localhost:1111/my-name', + { logging: false } + ); + instance.define('User', { + firstName: { type: sequelize.DataTypes.STRING }, + }); + + const response = { john: 'doe' }; + sequelize.Sequelize.prototype.query = (() => { + return new Promise(resolve => resolve(response)); + }) as QueryFunction; + const mockedLogger = (() => { + let message: string; + let error: Error; + return { + error: (_message: string, _err: Error) => { + message = _message; + error = _err; + }, + debug: () => {}, + getMessage: () => message, + getError: () => error, + }; + })(); + + instrumentation.setConfig({ + responseHook: () => { + throw new Error('Throwing'); + }, + }); + instrumentation.enable(); + diag.setLogger(mockedLogger as any); + await instance.models.User.findAll(); + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 1); + assert.strictEqual( + mockedLogger.getMessage(), + 'sequelize instrumentation: responseHook error' + ); + assert.strictEqual(mockedLogger.getError().message, 'Throwing'); + diag.setLogger(new DiagConsoleLogger()); + }); + }); + + describe('ignoreOrphanedSpans', () => { + it('skips when ignoreOrphanedSpans option is true', async () => { + instrumentation.disable(); + const instance = new sequelize.Sequelize( + 'postgres://john@$localhost:1111/my-name', + { logging: false } + ); + instance.define('User', { + firstName: { type: sequelize.DataTypes.STRING }, + }); + instrumentation.setConfig({ + ignoreOrphanedSpans: true, + }); + instrumentation.enable(); + + try { + await context.with(ROOT_CONTEXT, async () => { + await instance.models.User.create({ firstName: 'OpenTelemetry' }); + }); + } catch {} + + const spans = getSequelizeSpans(); + assert.strictEqual(spans.length, 0); + }); + }); + }); + + describe('misc', () => { + it('extractTableFromQuery', async () => { + assert.strictEqual( + extractTableFromQuery('FROM Users JOIN Dogs Where 1243'), + 'Dogs,Users' + ); + assert.strictEqual(extractTableFromQuery('FROM "Users"'), 'Users'); + assert.strictEqual( + extractTableFromQuery( + 'SELECT count(*) AS "count" FROM "Users" AS "User";' + ), + 'Users' + ); + assert.strictEqual( + extractTableFromQuery( + 'SELECT `id`, `firstName`, `createdAt`, `updatedAt` FROM `Users` AS `User`;' + ), + 'Users' + ); + assert.strictEqual(extractTableFromQuery(null), undefined); + assert.strictEqual(extractTableFromQuery(undefined), undefined); + }); + }); +}); diff --git a/plugins/node/instrumentation-sequelize/tsconfig.json b/plugins/node/instrumentation-sequelize/tsconfig.json new file mode 100644 index 0000000000..28be80d266 --- /dev/null +++ b/plugins/node/instrumentation-sequelize/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} From 7700a12df90e825ccc5e2c0236e25de7288972a3 Mon Sep 17 00:00:00 2001 From: Siim Kallas Date: Thu, 22 Aug 2024 00:07:39 +0300 Subject: [PATCH 2/5] chore: update release please --- .release-please-manifest.json | 1 + release-please-config.json | 1 + 2 files changed, 2 insertions(+) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d17d841f61..c105341067 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -25,6 +25,7 @@ "plugins/node/instrumentation-lru-memoizer": "0.39.0", "plugins/node/instrumentation-mongoose": "0.40.0", "plugins/node/instrumentation-runtime-node": "0.6.0", + "plugins/node/instrumentation-sequelize": "0.1.0", "plugins/node/instrumentation-socket.io": "0.41.0", "plugins/node/instrumentation-tedious": "0.12.0", "plugins/node/instrumentation-undici": "0.4.0", diff --git a/release-please-config.json b/release-please-config.json index b8fb45cd90..75fa1724f3 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -32,6 +32,7 @@ "plugins/node/instrumentation-lru-memoizer": {}, "plugins/node/instrumentation-mongoose": {}, "plugins/node/instrumentation-runtime-node": {}, + "plugins/node/instrumentation-sequelize": {}, "plugins/node/instrumentation-socket.io": {}, "plugins/node/instrumentation-tedious": {}, "plugins/node/instrumentation-undici": {}, From 1e17f8aefe71bbef7e3cc03943ed3d4e2136c7aa Mon Sep 17 00:00:00 2001 From: Siim Kallas Date: Thu, 22 Aug 2024 00:12:37 +0300 Subject: [PATCH 3/5] lint --- plugins/node/instrumentation-sequelize/test/sequelize.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/instrumentation-sequelize/test/sequelize.test.ts b/plugins/node/instrumentation-sequelize/test/sequelize.test.ts index 72062feb7e..498e77fd83 100644 --- a/plugins/node/instrumentation-sequelize/test/sequelize.test.ts +++ b/plugins/node/instrumentation-sequelize/test/sequelize.test.ts @@ -102,7 +102,7 @@ describe('instrumentation-sequelize', () => { assert.strictEqual(attributes[SEMATTRS_DB_SQL_TABLE], 'Users'); assert.match( attributes[SEMATTRS_DB_STATEMENT] as string, - /INSERT INTO "Users" \("id","firstName","createdAt","updatedAt"\) VALUES \(DEFAULT,\$1,\$2,\$3\) RETURNING (\*|"id","firstName","createdAt","updatedAt");/, + /INSERT INTO "Users" \("id","firstName","createdAt","updatedAt"\) VALUES \(DEFAULT,\$1,\$2,\$3\) RETURNING (\*|"id","firstName","createdAt","updatedAt");/ ); }); From 22173ed18afb5a7c7520ff33db91619d059b2442 Mon Sep 17 00:00:00 2001 From: Siim Kallas Date: Thu, 22 Aug 2024 12:10:28 +0300 Subject: [PATCH 4/5] add readme --- .../node/instrumentation-sequelize/README.md | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 plugins/node/instrumentation-sequelize/README.md diff --git a/plugins/node/instrumentation-sequelize/README.md b/plugins/node/instrumentation-sequelize/README.md new file mode 100644 index 0000000000..9e2be6af63 --- /dev/null +++ b/plugins/node/instrumentation-sequelize/README.md @@ -0,0 +1,98 @@ +# OpenTelemetry `sequelize` Instrumentation for Node.js + +[![NPM Published Version][npm-img]][npm-url] +[![Apache License][license-image]][license-image] + +This module provides automatic instrumentation for the [`sequelize`](https://www.npmjs.com/package/sequelize) package, 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. + +## Installation + +```bash +npm install --save @opentelemetry/instrumentation-sequelize +``` + +### Supported versions + +- [`sequelize`](https://www.npmjs.com/package/sequelize) versions `>=6 <7` + +## Usage + +```js +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const { SequelizeInstrumentation } = require('@opentelemetry/instrumentation-sequelize'); +const { registerInstrumentations } = require('@opentelemetry/instrumentation'); + +const provider = new NodeTracerProvider(); +provider.register(); + +registerInstrumentations({ + instrumentations: [ + new SequelizeInstrumentation({ + // see below for available configuration + }), + ], +}); +export interface SequelizeInstrumentationConfig extends InstrumentationConfig { + /** Hook for adding custom attributes using the query */ + queryHook?: SequelizeQueryHook; + /** Hook for adding custom attributes using the response payload */ + responseHook?: SequelizeResponseCustomAttributesFunction; + /** Set to true if you only want to trace operation which has parent spans */ + ignoreOrphanedSpans?: boolean; + /** + * Sequelize operation use postgres/mysql/mariadb/etc. under the hood. + * If, for example, postgres instrumentation is enabled, a postgres operation will also create + * a postgres span describing the communication. + * Setting the `suppressInternalInstrumentation` config value to `true` will + * cause the instrumentation to suppress instrumentation of underlying operations. + */ + suppressInternalInstrumentation?: boolean; + * An identifier for the database management system (DBMS) product being used. See below for a list of well-known identifiers. +``` + +### Instrumentation Options + +You can set the following: + +| Options | Type | Description | +| --------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `queryHook` | `SequelizeQueryHook` | Function called before running the query. Allows for adding custom attributes to the span. | +| `responseHook` | `SequelizeResponseCustomAttributesFunction` | Function called after a response is received. Allows for adding custom attributes to the span. | +| `ignoreOrphanedSpans` | `boolean` | Can be set to only produce spans which have parent spans. Default: `false` | +| `suppressInternalInstrumentation` | `boolean` | Set to ignore the underlying database library instrumentation. Default: `false` | + +## Semantic Conventions + +This package uses `@opentelemetry/semantic-conventions` version `1.25+`, which implements Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md) + +Attributes collected: + +| Attribute | Short Description | +| ----------------| --------------------------------------------------------------------------- | +| `db.name` | The name of the database being accessed. | +| `db.operation` | The name of the operation being executed. | +| `db.statement` | The database statement being executed. | +| `db.sql.table` | The name of the table being used. | +| `db.system` | An identifier for the database management system (DBMS) product being used. | +| `db.user` | Username for accessing the database. | +| `net.peer.name` | Remote hostname of the database. | +| `net.peer.port` | Port of the database. | +| `net.transport` | The transport protocol being used. | + +## 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-sequelize +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Finstrumentation-sequelize.svg From bbca381a807cb6cdb49b175cf20412cc2c2b886c Mon Sep 17 00:00:00 2001 From: Siim Kallas Date: Tue, 3 Sep 2024 10:28:15 +0300 Subject: [PATCH 5/5] update dependencies --- package-lock.json | 16 ++++++++-------- .../node/instrumentation-sequelize/package.json | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 848e5f6a47..13f585114a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38256,13 +38256,13 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.1", - "@opentelemetry/semantic-conventions": "^1.25.1" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "devDependencies": { "@opentelemetry/api": "^1.3.0", - "@opentelemetry/contrib-test-utils": "^0.40.0", - "@opentelemetry/sdk-trace-base": "^1.25.1", + "@opentelemetry/contrib-test-utils": "^0.41.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", "@types/mocha": "7.0.2", "@types/node": "18.6.5", "@types/sinon": "^10.0.11", @@ -53015,10 +53015,10 @@ "version": "file:plugins/node/instrumentation-sequelize", "requires": { "@opentelemetry/api": "^1.3.0", - "@opentelemetry/contrib-test-utils": "^0.40.0", - "@opentelemetry/instrumentation": "^0.52.1", - "@opentelemetry/sdk-trace-base": "^1.25.1", - "@opentelemetry/semantic-conventions": "^1.25.1", + "@opentelemetry/contrib-test-utils": "^0.41.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@types/mocha": "7.0.2", "@types/node": "18.6.5", "@types/sinon": "^10.0.11", diff --git a/plugins/node/instrumentation-sequelize/package.json b/plugins/node/instrumentation-sequelize/package.json index 48c85ed58d..00c88da936 100644 --- a/plugins/node/instrumentation-sequelize/package.json +++ b/plugins/node/instrumentation-sequelize/package.json @@ -44,8 +44,8 @@ }, "devDependencies": { "@opentelemetry/api": "^1.3.0", - "@opentelemetry/contrib-test-utils": "^0.40.0", - "@opentelemetry/sdk-trace-base": "^1.25.1", + "@opentelemetry/contrib-test-utils": "^0.41.0", + "@opentelemetry/sdk-trace-base": "^1.26.0", "@types/mocha": "7.0.2", "@types/node": "18.6.5", "@types/sinon": "^10.0.11", @@ -58,8 +58,8 @@ "typescript": "4.4.4" }, "dependencies": { - "@opentelemetry/instrumentation": "^0.52.1", - "@opentelemetry/semantic-conventions": "^1.25.1" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/instrumentation-sequelize#readme" }