diff --git a/package-lock.json b/package-lock.json index 4a35f3134f..4464c66ed1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9543,6 +9543,8 @@ "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", "dev": true, + "optional": true, + "peer": true, "peerDependencies": { "@redis/client": "^1.0.0" } @@ -9552,6 +9554,8 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -9566,6 +9570,8 @@ "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">= 4" } @@ -9574,13 +9580,17 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@redis/graph": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", "dev": true, + "optional": true, + "peer": true, "peerDependencies": { "@redis/client": "^1.0.0" } @@ -9590,6 +9600,8 @@ "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", "dev": true, + "optional": true, + "peer": true, "peerDependencies": { "@redis/client": "^1.0.0" } @@ -9599,6 +9611,8 @@ "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", "dev": true, + "optional": true, + "peer": true, "peerDependencies": { "@redis/client": "^1.0.0" } @@ -9608,6 +9622,8 @@ "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", "dev": true, + "optional": true, + "peer": true, "peerDependencies": { "@redis/client": "^1.0.0" } @@ -27419,6 +27435,8 @@ "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.1", @@ -34841,7 +34859,7 @@ "@types/node": "18.18.14", "cross-env": "7.0.3", "nyc": "17.1.0", - "redis": "^4.7.1", + "redis": "^5.6.0", "rimraf": "5.0.10", "test-all-versions": "6.1.0", "typescript": "5.0.4" @@ -34853,6 +34871,71 @@ "@opentelemetry/api": "^1.3.0" } }, + "packages/instrumentation-redis/node_modules/@redis/bloom": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.0.tgz", + "integrity": "sha512-l13/d6BaZDJzogzZJEphIeZ8J0hpQpjkMiozomTm6nJiMNYkoPsNOBOOQua4QsG0fFjyPmLMDJFPAp5FBQtTXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.0" + } + }, + "packages/instrumentation-redis/node_modules/@redis/client": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.0.tgz", + "integrity": "sha512-wmP9kCFElCSr4MM4+1E4VckDuN4wLtiXSM/J0rKVQppajxQhowci89RGZr2OdLualowb8SRJ/R6OjsXrn9ZNFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/instrumentation-redis/node_modules/@redis/json": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.0.tgz", + "integrity": "sha512-YQN9ZqaSDpdLfJqwzcF4WeuJMGru/h4WsV7GeeNtXsSeyQjHTyDxrd48xXfRRJGv7HitA7zGnzdHplNeKOgrZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.0" + } + }, + "packages/instrumentation-redis/node_modules/@redis/search": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.0.tgz", + "integrity": "sha512-sLgQl92EyMVNHtri5K8Q0j2xt9c0cO9HYurXz667Un4xeUYR+B/Dw5lLG35yqO7VvVxb9amHJo9sAWumkKZYwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.0" + } + }, + "packages/instrumentation-redis/node_modules/@redis/time-series": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.0.tgz", + "integrity": "sha512-tXABmN1vu4aTNL3WI4Iolpvx/5jgil2Bs31ozvKblT+jkUoRkk8ykmYo9Pv/Mp7Gk6/Qkr/2rMgVminrt/4BBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.6.0" + } + }, "packages/instrumentation-redis/node_modules/@types/node": { "version": "18.18.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.14.tgz", @@ -34862,6 +34945,36 @@ "undici-types": "~5.26.4" } }, + "packages/instrumentation-redis/node_modules/redis": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.6.0.tgz", + "integrity": "sha512-0x3pM3SlYA5azdNwO8qgfMBzoOqSqr9M+sd1hojbcn0ZDM5zsmKeMM+zpTp6LIY+mbQomIc/RTTQKuBzr8QKzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redis/bloom": "5.6.0", + "@redis/client": "5.6.0", + "@redis/json": "5.6.0", + "@redis/search": "5.6.0", + "@redis/time-series": "5.6.0" + }, + "engines": { + "node": ">= 18" + } + }, + "packages/instrumentation-redis/node_modules/typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=12.20" + } + }, "packages/instrumentation-redis/node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -45266,12 +45379,49 @@ "@types/node": "18.18.14", "cross-env": "7.0.3", "nyc": "17.1.0", - "redis": "^4.7.1", + "redis": "^5.6.0", "rimraf": "5.0.10", "test-all-versions": "6.1.0", "typescript": "5.0.4" }, "dependencies": { + "@redis/bloom": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.6.0.tgz", + "integrity": "sha512-l13/d6BaZDJzogzZJEphIeZ8J0hpQpjkMiozomTm6nJiMNYkoPsNOBOOQua4QsG0fFjyPmLMDJFPAp5FBQtTXg==", + "dev": true, + "requires": {} + }, + "@redis/client": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.0.tgz", + "integrity": "sha512-wmP9kCFElCSr4MM4+1E4VckDuN4wLtiXSM/J0rKVQppajxQhowci89RGZr2OdLualowb8SRJ/R6OjsXrn9ZNFA==", + "dev": true, + "requires": { + "cluster-key-slot": "1.1.2" + } + }, + "@redis/json": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.6.0.tgz", + "integrity": "sha512-YQN9ZqaSDpdLfJqwzcF4WeuJMGru/h4WsV7GeeNtXsSeyQjHTyDxrd48xXfRRJGv7HitA7zGnzdHplNeKOgrZA==", + "dev": true, + "requires": {} + }, + "@redis/search": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.6.0.tgz", + "integrity": "sha512-sLgQl92EyMVNHtri5K8Q0j2xt9c0cO9HYurXz667Un4xeUYR+B/Dw5lLG35yqO7VvVxb9amHJo9sAWumkKZYwA==", + "dev": true, + "requires": {} + }, + "@redis/time-series": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.6.0.tgz", + "integrity": "sha512-tXABmN1vu4aTNL3WI4Iolpvx/5jgil2Bs31ozvKblT+jkUoRkk8ykmYo9Pv/Mp7Gk6/Qkr/2rMgVminrt/4BBQ==", + "dev": true, + "requires": {} + }, "@types/node": { "version": "18.18.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.14.tgz", @@ -45281,6 +45431,25 @@ "undici-types": "~5.26.4" } }, + "redis": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.6.0.tgz", + "integrity": "sha512-0x3pM3SlYA5azdNwO8qgfMBzoOqSqr9M+sd1hojbcn0ZDM5zsmKeMM+zpTp6LIY+mbQomIc/RTTQKuBzr8QKzQ==", + "dev": true, + "requires": { + "@redis/bloom": "5.6.0", + "@redis/client": "5.6.0", + "@redis/json": "5.6.0", + "@redis/search": "5.6.0", + "@redis/time-series": "5.6.0" + } + }, + "typescript": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "dev": true + }, "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -46947,6 +47116,8 @@ "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", "dev": true, + "optional": true, + "peer": true, "requires": {} }, "@redis/client": { @@ -46954,6 +47125,8 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", "dev": true, + "optional": true, + "peer": true, "requires": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -46964,13 +47137,17 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "optional": true, + "peer": true } } }, @@ -46979,6 +47156,8 @@ "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", "dev": true, + "optional": true, + "peer": true, "requires": {} }, "@redis/json": { @@ -46986,6 +47165,8 @@ "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", "dev": true, + "optional": true, + "peer": true, "requires": {} }, "@redis/search": { @@ -46993,6 +47174,8 @@ "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", "dev": true, + "optional": true, + "peer": true, "requires": {} }, "@redis/time-series": { @@ -47000,6 +47183,8 @@ "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", "dev": true, + "optional": true, + "peer": true, "requires": {} }, "@rollup/plugin-commonjs": { @@ -60688,6 +60873,8 @@ "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", "dev": true, + "optional": true, + "peer": true, "requires": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.1", diff --git a/packages/instrumentation-redis/.tav.yml b/packages/instrumentation-redis/.tav.yml index 12aa469538..47d23f9878 100644 --- a/packages/instrumentation-redis/.tav.yml +++ b/packages/instrumentation-redis/.tav.yml @@ -4,8 +4,8 @@ redis: mode: latest-minors commands: npm run test-v2-v3 - versions: - include: '>=4 <5' + include: '>=4 <6' # "4.6.9" was a bad release that accidentally broke node v14 support. exclude: "4.6.9" mode: latest-minors - commands: npm test + commands: npm run test \ No newline at end of file diff --git a/packages/instrumentation-redis/README.md b/packages/instrumentation-redis/README.md index b2827e72f4..b45de68231 100644 --- a/packages/instrumentation-redis/README.md +++ b/packages/instrumentation-redis/README.md @@ -3,7 +3,7 @@ [![NPM Published Version][npm-img]][npm-url] [![Apache License][license-image]][license-image] -This module provides automatic instrumentation for the [`redis`](https://github.com/NodeRedis/node_redis) module versions `>=2.6.0 <5`, 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. +This module provides automatic instrumentation for the [`redis`](https://github.com/NodeRedis/node_redis) module versions `>=2.6.0 <6`, 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. @@ -17,7 +17,7 @@ npm install --save @opentelemetry/instrumentation-redis ### Supported Versions -- [`redis`](https://www.npmjs.com/package/redis) versions `>=2.6.0 <5` +- [`redis`](https://www.npmjs.com/package/redis) versions `>=2.6.0 <6` ## Usage @@ -75,10 +75,16 @@ const redisInstrumentation = new RedisInstrumentation({ ## Semantic Conventions -This package uses `@opentelemetry/semantic-conventions` version `1.22+`, which implements Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md) + +This package uses `@opentelemetry/semantic-conventions` version `1.22+`, which implements Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md) ("old" conventions). + +It also supports the new stable semantic conventions introduced in [Version 1.33.0] +By default, old semantic conventions are used. Use the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable to control which version to emit. Attributes collected: +### Old Semantic Conventions (default) + | Attribute | Short Description | |------------------------|--------------------------------------------------------------| | `db.connection_string` | URL to Redis server address, of the form `redis://host:port` | @@ -87,6 +93,17 @@ Attributes collected: | `net.peer.name` | Hostname or IP of the connected Redis server | | `net.peer.port` | Port of the connected Redis server | +### Stable Semantic Conventions (v1.33.0) + +| Attribute | Short Description | +|------------------------|--------------------------------------------------------------| +| `db.operation.name` | Redis command name | +| `db.operation.batch.size` | Number of commands in a Redis `MULTI/EXEC` transaction | +| `db.query.text` | The database query being executed | +| `db.system.name` | Database identifier; always `redis` | +| `server.address` | Hostname or IP of the connected Redis server | +| `server.port` | Port of the connected Redis server | + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/packages/instrumentation-redis/package.json b/packages/instrumentation-redis/package.json index 63b02835e8..0ebfa3d4d5 100644 --- a/packages/instrumentation-redis/package.json +++ b/packages/instrumentation-redis/package.json @@ -19,7 +19,7 @@ "prepublishOnly": "npm run compile", "tdd": "npm run test -- --watch-extensions ts --watch", "test-v2-v3": "nyc mocha --require '@opentelemetry/contrib-test-utils' 'test/v2-v3/*.test.ts'", - "test": "nyc mocha --require '@opentelemetry/contrib-test-utils' 'test/v4/*.test.ts'", + "test": "nyc mocha --require '@opentelemetry/contrib-test-utils' 'test/v4-v5/*.test.ts'", "test:debug": "cross-env RUN_REDIS_TESTS=true mocha --inspect-brk --no-timeouts 'test/**/*.test.ts'", "test:with-services-env": "cross-env NODE_OPTIONS='-r dotenv/config' DOTENV_CONFIG_PATH=../../test/test-services.env npm test", "test-all-versions": "tav", @@ -62,7 +62,7 @@ "@types/node": "18.18.14", "cross-env": "7.0.3", "nyc": "17.1.0", - "redis": "^4.7.1", + "redis": "^5.6.0", "rimraf": "5.0.10", "test-all-versions": "6.1.0", "typescript": "5.0.4" diff --git a/packages/instrumentation-redis/src/redis.ts b/packages/instrumentation-redis/src/redis.ts index 14c2003dce..fb9bfe331d 100644 --- a/packages/instrumentation-redis/src/redis.ts +++ b/packages/instrumentation-redis/src/redis.ts @@ -19,7 +19,7 @@ import { RedisInstrumentationConfig } from './types'; import { PACKAGE_NAME, PACKAGE_VERSION } from './version'; import { RedisInstrumentationV2_V3 } from './v2-v3/instrumentation'; import { TracerProvider } from '@opentelemetry/api'; -import { RedisInstrumentationV4 } from './v4/instrumentation'; +import { RedisInstrumentationV4_V5 } from './v4-v5/instrumentation'; const DEFAULT_CONFIG: RedisInstrumentationConfig = { requireParentSpan: false, @@ -28,7 +28,7 @@ const DEFAULT_CONFIG: RedisInstrumentationConfig = { // Wrapper RedisInstrumentation that address all supported versions export class RedisInstrumentation extends InstrumentationBase { private instrumentationV2_V3: RedisInstrumentationV2_V3; - private instrumentationV4: RedisInstrumentationV4; + private instrumentationV4_V5: RedisInstrumentationV4_V5; // this is used to bypass a flaw in the base class constructor, which is calling // member functions before the constructor has a chance to fully initialize the member variables. @@ -39,7 +39,7 @@ export class RedisInstrumentation extends InstrumentationBase { static readonly COMPONENT = 'redis'; + private _semconvStability: SemconvStability; constructor(config: RedisInstrumentationConfig = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, config); + this._semconvStability = config.semconvStability + ? config.semconvStability + : semconvStabilityFromStr( + 'database', + process.env.OTEL_SEMCONV_STABILITY_OPT_IN + ); + } + + override setConfig(config: RedisInstrumentationConfig = {}) { + super.setConfig(config); + this._semconvStability = config.semconvStability + ? config.semconvStability + : semconvStabilityFromStr( + 'database', + process.env.OTEL_SEMCONV_STABILITY_OPT_IN + ); } protected init() { @@ -127,28 +151,60 @@ export class RedisInstrumentationV2_V3 extends InstrumentationBase; } -export class RedisInstrumentationV4 extends InstrumentationBase { +export class RedisInstrumentationV4_V5 extends InstrumentationBase { static readonly COMPONENT = 'redis'; + private _semconvStability: SemconvStability; constructor(config: RedisInstrumentationConfig = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, config); + this._semconvStability = config.semconvStability + ? config.semconvStability + : semconvStabilityFromStr( + 'database', + process.env.OTEL_SEMCONV_STABILITY_OPT_IN + ); + } + + override setConfig(config: RedisInstrumentationConfig = {}) { + super.setConfig(config); + this._semconvStability = config.semconvStability + ? config.semconvStability + : semconvStabilityFromStr( + 'database', + process.env.OTEL_SEMCONV_STABILITY_OPT_IN + ); } protected init() { @@ -111,7 +134,7 @@ export class RedisInstrumentationV4 extends InstrumentationBase { const redisClientMultiCommandPrototype = moduleExports?.default?.prototype; @@ -150,7 +173,7 @@ export class RedisInstrumentationV4 extends InstrumentationBase { const redisClientPrototype = moduleExports?.default?.prototype; @@ -213,7 +236,7 @@ export class RedisInstrumentationV4 extends InstrumentationBase { return moduleExports; }, @@ -327,11 +350,14 @@ export class RedisInstrumentationV4 extends InstrumentationBase { const options = this.options; - - const attributes = getClientAttributes(plugin._diag, options); + const attributes = getClientAttributes( + plugin._diag, + options, + plugin._semconvStability + ); const span = plugin.tracer.startSpan( - `${RedisInstrumentationV4.COMPONENT}-connect`, + `${RedisInstrumentationV4_V5.COMPONENT}-connect`, { kind: SpanKind.CLIENT, attributes, @@ -379,12 +405,23 @@ export class RedisInstrumentationV4 extends InstrumentationBase { description: string; command: string; args: string[]; - expectedDbStatement: string; + expectedDbStatementOld: string; + expectedDbStatementStable: string; method: (cb: Callback) => unknown; }> = [ { description: 'insert', command: 'hset', args: ['hash', 'random', 'random'], - expectedDbStatement: 'hash random [1 other arguments]', + expectedDbStatementOld: 'hash random [1 other arguments]', + expectedDbStatementStable: 'hset hash random [1 other arguments]', method: (cb: Callback) => client.hset('hash', 'random', 'random', cb), }, @@ -120,14 +132,16 @@ describe('redis v2-v3', () => { description: 'get', command: 'get', args: ['test'], - expectedDbStatement: 'test', + expectedDbStatementOld: 'test', + expectedDbStatementStable: 'get test', method: (cb: Callback) => client.get('test', cb), }, { description: 'delete', command: 'del', args: ['test'], - expectedDbStatement: 'test', + expectedDbStatementOld: 'test', + expectedDbStatementStable: 'del test', method: (cb: Callback) => client.del('test', cb), }, ]; @@ -157,13 +171,135 @@ describe('redis v2-v3', () => { done(); }); }); + describe('semconv stability config', () => { + function recordSpanForOperation(operation: any, cb: (span: any) => void) { + operation.method((err: unknown) => { + assert.ifError(err); + const [span] = testUtils.getTestSpans(); + cb(span); + }); + } + + describe('semconv stability with get operation', () => { + const operation = REDIS_OPERATIONS.find(op => op.command === 'get')!; + + it('should emit only old attributes when set to OLD', done => { + instrumentation.setConfig({ semconvStability: SemconvStability.OLD }); + + recordSpanForOperation(operation, span => { + assert.strictEqual(span.attributes[SEMATTRS_DB_SYSTEM], 'redis'); + assert.strictEqual( + span.attributes[SEMATTRS_DB_STATEMENT], + `${operation.command} ${operation.expectedDbStatementOld}` + ); + + assert.ok(!(ATTR_DB_SYSTEM_NAME in span.attributes)); + assert.ok(!(ATTR_DB_QUERY_TEXT in span.attributes)); + assert.ok(!(ATTR_DB_OPERATION_NAME in span.attributes)); + + assert.strictEqual( + span.attributes[SEMATTRS_NET_PEER_NAME], + CONFIG.host + ); + assert.strictEqual( + span.attributes[SEMATTRS_NET_PEER_PORT], + CONFIG.port + ); + assert.strictEqual( + span.attributes[SEMATTRS_DB_CONNECTION_STRING], + URL + ); + + assert.ok(!(ATTR_SERVER_ADDRESS in span.attributes)); + assert.ok(!(ATTR_SERVER_PORT in span.attributes)); + done(); + }); + }); + + it('should emit only new attributes when set to STABLE', done => { + instrumentation.setConfig({ + semconvStability: SemconvStability.STABLE, + }); + + recordSpanForOperation(operation, span => { + assert.strictEqual(span.attributes[ATTR_DB_SYSTEM_NAME], 'redis'); + assert.strictEqual( + span.attributes[ATTR_DB_QUERY_TEXT], + operation.expectedDbStatementStable + ); + assert.strictEqual( + span.attributes[ATTR_DB_OPERATION_NAME], + operation.command + ); + + assert.ok(!(SEMATTRS_DB_SYSTEM in span.attributes)); + assert.ok(!(SEMATTRS_DB_STATEMENT in span.attributes)); + + assert.strictEqual( + span.attributes[ATTR_SERVER_ADDRESS], + CONFIG.host + ); + assert.strictEqual(span.attributes[ATTR_SERVER_PORT], CONFIG.port); + + assert.ok(!(SEMATTRS_NET_PEER_NAME in span.attributes)); + assert.ok(!(SEMATTRS_NET_PEER_PORT in span.attributes)); + assert.ok(!(SEMATTRS_DB_CONNECTION_STRING in span.attributes)); + done(); + }); + }); + + it('should emit both old and new attributes when set to DUPLICATE', done => { + instrumentation.setConfig({ + semconvStability: SemconvStability.DUPLICATE, + }); + + recordSpanForOperation(operation, span => { + assert.strictEqual(span.attributes[SEMATTRS_DB_SYSTEM], 'redis'); + assert.strictEqual( + span.attributes[SEMATTRS_DB_STATEMENT], + `${operation.command} ${operation.expectedDbStatementOld}` + ); + assert.strictEqual(span.attributes[ATTR_DB_SYSTEM_NAME], 'redis'); + assert.strictEqual( + span.attributes[ATTR_DB_QUERY_TEXT], + operation.expectedDbStatementStable + ); + assert.strictEqual( + span.attributes[ATTR_DB_OPERATION_NAME], + operation.command + ); + + assert.strictEqual( + span.attributes[SEMATTRS_NET_PEER_NAME], + CONFIG.host + ); + assert.strictEqual( + span.attributes[SEMATTRS_NET_PEER_PORT], + CONFIG.port + ); + assert.strictEqual( + span.attributes[ATTR_SERVER_ADDRESS], + CONFIG.host + ); + assert.strictEqual(span.attributes[ATTR_SERVER_PORT], CONFIG.port); + assert.strictEqual( + span.attributes[SEMATTRS_DB_CONNECTION_STRING], + URL + ); + done(); + }); + }); + }); + }); describe('Instrumenting query operations', () => { REDIS_OPERATIONS.forEach(operation => { it(`should create a child span for ${operation.description}`, done => { const attributes = { ...DEFAULT_ATTRIBUTES, - [SEMATTRS_DB_STATEMENT]: `${operation.command} ${operation.expectedDbStatement}`, + [ATTR_DB_OPERATION_NAME]: operation.command, + [ATTR_DB_QUERY_TEXT]: operation.expectedDbStatementStable, + [SEMATTRS_DB_STATEMENT]: `${operation.command} ${operation.expectedDbStatementOld}`, }; const span = tracer.startSpan('test span'); context.with(trace.setSpan(context.active(), span), () => { @@ -177,6 +313,7 @@ describe('redis v2-v3', () => { endedSpans[0].name, `redis-${operation.command}` ); + testUtils.assertSpan( endedSpans[0], SpanKind.CLIENT, @@ -233,6 +370,11 @@ describe('redis v2-v3', () => { endedSpans[0].attributes[SEMATTRS_DB_STATEMENT], expectedStatement ); + + assert.strictEqual( + endedSpans[0].attributes[ATTR_DB_QUERY_TEXT], + expectedStatement + ); done(); }); }); diff --git a/packages/instrumentation-redis/test/v4/redis.test.ts b/packages/instrumentation-redis/test/v4-v5/redis.test.ts similarity index 73% rename from packages/instrumentation-redis/test/v4/redis.test.ts rename to packages/instrumentation-redis/test/v4-v5/redis.test.ts index 9b92d52f75..d749056c31 100644 --- a/packages/instrumentation-redis/test/v4/redis.test.ts +++ b/packages/instrumentation-redis/test/v4-v5/redis.test.ts @@ -20,11 +20,12 @@ import { registerInstrumentationTesting, } from '@opentelemetry/contrib-test-utils'; import { RedisInstrumentation } from '../../src'; -import type { MultiErrorReply } from '../../src/v4/internal-types'; +import type { MultiErrorReply } from '../../src/v4-v5/internal-types'; import * as assert from 'assert'; import { redisTestConfig, redisTestUrl, shouldTest } from './utils'; +process.env.OTEL_SEMCONV_STABILITY_OPT_IN = 'database/dup'; const instrumentation = registerInstrumentationTesting( new RedisInstrumentation() ); @@ -45,11 +46,18 @@ import { SEMATTRS_EXCEPTION_MESSAGE, SEMATTRS_NET_PEER_NAME, SEMATTRS_NET_PEER_PORT, + ATTR_DB_SYSTEM_NAME, + ATTR_DB_OPERATION_NAME, + ATTR_DB_QUERY_TEXT, + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, + ATTR_EXCEPTION_MESSAGE, } from '@opentelemetry/semantic-conventions'; import { RedisResponseCustomAttributeFunction } from '../../src/types'; import { hrTimeToMilliseconds, suppressTracing } from '@opentelemetry/core'; +import { SemconvStability } from '@opentelemetry/instrumentation'; -describe('redis v4', () => { +describe('redis v4-v5', () => { before(function () { // needs to be "function" to have MochaContext "this" context if (!shouldTest) { @@ -88,19 +96,33 @@ describe('redis v4', () => { assert.ok(setSpan); assert.strictEqual(setSpan?.kind, SpanKind.CLIENT); assert.strictEqual(setSpan?.name, 'redis-SET'); + assert.strictEqual(setSpan?.attributes[ATTR_DB_SYSTEM_NAME], 'redis'); assert.strictEqual(setSpan?.attributes[SEMATTRS_DB_SYSTEM], 'redis'); assert.strictEqual( setSpan?.attributes[SEMATTRS_DB_STATEMENT], 'SET key [1 other arguments]' ); + assert.strictEqual( + setSpan?.attributes[ATTR_DB_QUERY_TEXT], + 'SET key [1 other arguments]' + ); assert.strictEqual( setSpan?.attributes[SEMATTRS_NET_PEER_NAME], redisTestConfig.host ); + assert.strictEqual( + setSpan?.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); assert.strictEqual( setSpan?.attributes[SEMATTRS_NET_PEER_PORT], redisTestConfig.port ); + assert.strictEqual( + setSpan?.attributes[ATTR_SERVER_PORT], + redisTestConfig.port + ); + assert.strictEqual(setSpan?.attributes[ATTR_DB_OPERATION_NAME], 'SET'); assert.strictEqual( setSpan?.attributes[SEMATTRS_DB_CONNECTION_STRING], redisTestUrl @@ -110,16 +132,26 @@ describe('redis v4', () => { assert.ok(getSpan); assert.strictEqual(getSpan?.kind, SpanKind.CLIENT); assert.strictEqual(getSpan?.name, 'redis-GET'); + assert.strictEqual(getSpan?.attributes[ATTR_DB_SYSTEM_NAME], 'redis'); + assert.strictEqual(getSpan?.attributes[ATTR_DB_QUERY_TEXT], 'GET key'); assert.strictEqual(getSpan?.attributes[SEMATTRS_DB_SYSTEM], 'redis'); assert.strictEqual(getSpan?.attributes[SEMATTRS_DB_STATEMENT], 'GET key'); assert.strictEqual( getSpan?.attributes[SEMATTRS_NET_PEER_NAME], redisTestConfig.host ); + assert.strictEqual( + getSpan?.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); assert.strictEqual( getSpan?.attributes[SEMATTRS_NET_PEER_PORT], redisTestConfig.port ); + assert.strictEqual( + getSpan?.attributes[ATTR_SERVER_PORT], + redisTestConfig.port + ); assert.strictEqual( getSpan?.attributes[SEMATTRS_DB_CONNECTION_STRING], redisTestUrl @@ -137,14 +169,27 @@ describe('redis v4', () => { setSpan?.attributes[SEMATTRS_DB_STATEMENT], 'SET key [1 other arguments]' ); + assert.strictEqual( + setSpan?.attributes[ATTR_DB_QUERY_TEXT], + 'SET key [1 other arguments]' + ); assert.strictEqual( setSpan?.attributes[SEMATTRS_NET_PEER_NAME], redisTestConfig.host ); + assert.strictEqual( + setSpan?.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); assert.strictEqual( setSpan?.attributes[SEMATTRS_NET_PEER_PORT], redisTestConfig.port ); + assert.strictEqual( + setSpan?.attributes[ATTR_SERVER_PORT], + redisTestConfig.port + ); + assert.strictEqual(setSpan?.attributes[ATTR_DB_OPERATION_NAME], 'SET'); }); it('command with error', async () => { @@ -168,6 +213,10 @@ describe('redis v4', () => { exceptions?.[0].attributes?.[SEMATTRS_EXCEPTION_MESSAGE], 'ERR value is not an integer or out of range' ); + assert.strictEqual( + exceptions?.[0].attributes?.[ATTR_EXCEPTION_MESSAGE], + 'ERR value is not an integer or out of range' + ); }); }); @@ -188,14 +237,23 @@ describe('redis v4', () => { assert.strictEqual(span.name, 'redis-connect'); assert.strictEqual(span.attributes[SEMATTRS_DB_SYSTEM], 'redis'); + assert.strictEqual(span.attributes[ATTR_DB_SYSTEM_NAME], 'redis'); assert.strictEqual( span.attributes[SEMATTRS_NET_PEER_NAME], redisTestConfig.host ); + assert.strictEqual( + span.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); assert.strictEqual( span.attributes[SEMATTRS_NET_PEER_PORT], redisTestConfig.port ); + assert.strictEqual( + span.attributes[ATTR_SERVER_PORT], + redisTestConfig.port + ); assert.strictEqual( span.attributes[SEMATTRS_DB_CONNECTION_STRING], redisTestUrl @@ -312,6 +370,46 @@ describe('redis v4', () => { }); }); + describe('Redis client connect with malformed URL', () => { + it('malformed URL should trigger diag error and reject connection', async () => { + instrumentation.setConfig({ semconvStability: SemconvStability.OLD }); + + const diagErrors: any[] = []; + diag.setLogger( + { + verbose() {}, + debug() {}, + info() {}, + warn() {}, + error(...args) { + diagErrors.push(args); + }, + }, + DiagLogLevel.ALL + ); + + const client = createClient({ + socket: { host: 'localhost', port: 9999 }, + }); + + const opts = (client as any).options; + if (opts) opts.url = '://malformed-url-no-protocol'; + + await assert.rejects(() => client.connect()); + + try { + await client.disconnect(); + } catch {} + + assert.ok(diagErrors.length > 0, 'Expected at least one diag error'); + const found = diagErrors.some(args => + args.includes('failed to sanitize redis connection url') + ); + + assert.ok(found, 'Expected sanitize URL diag error'); + }); + }); + describe('multi (transactions) commands', () => { it('multi commands', async () => { await client.set('another-key', 'another-value'); @@ -334,18 +432,34 @@ describe('redis v4', () => { multiSetSpan.attributes[SEMATTRS_DB_STATEMENT], 'SET key [1 other arguments]' ); + assert.strictEqual( + multiSetSpan.attributes[ATTR_DB_QUERY_TEXT], + 'SET key [1 other arguments]' + ); assert.strictEqual( multiSetSpan?.attributes[SEMATTRS_NET_PEER_NAME], redisTestConfig.host ); + assert.strictEqual( + multiSetSpan?.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); assert.strictEqual( multiSetSpan?.attributes[SEMATTRS_NET_PEER_PORT], redisTestConfig.port ); + assert.strictEqual( + multiSetSpan?.attributes[ATTR_SERVER_PORT], + redisTestConfig.port + ); assert.strictEqual( multiSetSpan?.attributes[SEMATTRS_DB_CONNECTION_STRING], redisTestUrl ); + assert.strictEqual( + multiSetSpan?.attributes[ATTR_DB_OPERATION_NAME], + 'SET' + ); assert.ok(multiGetSpan); assert.strictEqual(multiGetSpan.name, 'redis-GET'); @@ -353,18 +467,34 @@ describe('redis v4', () => { multiGetSpan.attributes[SEMATTRS_DB_STATEMENT], 'GET another-key' ); + assert.strictEqual( + multiGetSpan.attributes[ATTR_DB_QUERY_TEXT], + 'GET another-key' + ); assert.strictEqual( multiGetSpan?.attributes[SEMATTRS_NET_PEER_NAME], redisTestConfig.host ); + assert.strictEqual( + multiGetSpan?.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); assert.strictEqual( multiGetSpan?.attributes[SEMATTRS_NET_PEER_PORT], redisTestConfig.port ); + assert.strictEqual( + multiGetSpan?.attributes[ATTR_SERVER_PORT], + redisTestConfig.port + ); assert.strictEqual( multiGetSpan?.attributes[SEMATTRS_DB_CONNECTION_STRING], redisTestUrl ); + assert.strictEqual( + multiGetSpan?.attributes[ATTR_DB_OPERATION_NAME], + 'GET' + ); }); it('multi command with generic command', async () => { @@ -380,18 +510,34 @@ describe('redis v4', () => { multiSetSpan.attributes[SEMATTRS_DB_STATEMENT], 'SET key [1 other arguments]' ); + assert.strictEqual( + multiSetSpan.attributes[ATTR_DB_QUERY_TEXT], + 'SET key [1 other arguments]' + ); assert.strictEqual( multiSetSpan?.attributes[SEMATTRS_NET_PEER_NAME], redisTestConfig.host ); + assert.strictEqual( + multiSetSpan?.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); assert.strictEqual( multiSetSpan?.attributes[SEMATTRS_NET_PEER_PORT], redisTestConfig.port ); + assert.strictEqual( + multiSetSpan?.attributes[ATTR_SERVER_PORT], + redisTestConfig.port + ); assert.strictEqual( multiSetSpan?.attributes[SEMATTRS_DB_CONNECTION_STRING], redisTestUrl ); + assert.strictEqual( + multiSetSpan?.attributes[ATTR_DB_OPERATION_NAME], + 'SET' + ); }); it('multi command with error', async () => { @@ -444,7 +590,7 @@ describe('redis v4', () => { it('duration covers create until server response', async () => { await client.set('another-key', 'another-value'); const multiClient = client.multi(); - let commands = multiClient.set('key', 'value'); + let commands: any = multiClient.set('key', 'value'); // wait 10 ms before adding next command // simulate long operation await new Promise(resolve => setTimeout(resolve, 10)); @@ -533,6 +679,10 @@ describe('redis v4', () => { span.attributes[SEMATTRS_DB_STATEMENT], 'SET key value' ); + assert.strictEqual( + span.attributes[ATTR_DB_QUERY_TEXT], + 'SET key value' + ); }); it('dbStatementSerializer throws', async () => { @@ -545,6 +695,7 @@ describe('redis v4', () => { const [span] = getTestSpans(); assert.ok(span); assert.ok(!(SEMATTRS_DB_STATEMENT in span.attributes)); + assert.ok(!(ATTR_DB_QUERY_TEXT in span.attributes)); }); }); @@ -606,4 +757,80 @@ describe('redis v4', () => { }); }); }); + + describe('semconv stability configuration', () => { + async function getSpan(client: RedisClientType) { + await client.set('key', 'value'); + const spans = getTestSpans(); + return spans.find(s => s.name.includes('SET')); + } + + it('should emit only old attributes when semconvStability is OLD', async () => { + instrumentation.setConfig({ semconvStability: SemconvStability.OLD }); + const setSpan = await getSpan(client); + assert.ok(setSpan); + + assert.strictEqual(setSpan.attributes[SEMATTRS_DB_SYSTEM], 'redis'); + assert.strictEqual( + setSpan.attributes[SEMATTRS_DB_STATEMENT], + 'SET key [1 other arguments]' + ); + assert.strictEqual( + setSpan.attributes[SEMATTRS_NET_PEER_NAME], + redisTestConfig.host + ); + + assert.ok(!(ATTR_DB_SYSTEM_NAME in setSpan.attributes)); + assert.ok(!(ATTR_DB_QUERY_TEXT in setSpan.attributes)); + assert.ok(!(ATTR_SERVER_ADDRESS in setSpan.attributes)); + }); + + it('should emit only new attributes when semconvStability is STABLE', async () => { + instrumentation.setConfig({ semconvStability: SemconvStability.STABLE }); + const setSpan = await getSpan(client); + assert.ok(setSpan); + + assert.strictEqual(setSpan.attributes[ATTR_DB_SYSTEM_NAME], 'redis'); + assert.strictEqual( + setSpan.attributes[ATTR_DB_QUERY_TEXT], + 'SET key [1 other arguments]' + ); + assert.strictEqual( + setSpan.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); + + assert.ok(!(SEMATTRS_DB_SYSTEM in setSpan.attributes)); + assert.ok(!(SEMATTRS_DB_STATEMENT in setSpan.attributes)); + assert.ok(!(SEMATTRS_NET_PEER_NAME in setSpan.attributes)); + }); + + it('should emit both old and new attributes when semconvStability is DUPLICATE', async () => { + instrumentation.setConfig({ + semconvStability: SemconvStability.DUPLICATE, + }); + const setSpan = await getSpan(client); + assert.ok(setSpan); + + assert.strictEqual(setSpan.attributes[SEMATTRS_DB_SYSTEM], 'redis'); + assert.strictEqual( + setSpan.attributes[SEMATTRS_DB_STATEMENT], + 'SET key [1 other arguments]' + ); + assert.strictEqual( + setSpan.attributes[SEMATTRS_NET_PEER_NAME], + redisTestConfig.host + ); + + assert.strictEqual(setSpan.attributes[ATTR_DB_SYSTEM_NAME], 'redis'); + assert.strictEqual( + setSpan.attributes[ATTR_DB_QUERY_TEXT], + 'SET key [1 other arguments]' + ); + assert.strictEqual( + setSpan.attributes[ATTR_SERVER_ADDRESS], + redisTestConfig.host + ); + }); + }); }); diff --git a/packages/instrumentation-redis/test/v4/utils.ts b/packages/instrumentation-redis/test/v4-v5/utils.ts similarity index 100% rename from packages/instrumentation-redis/test/v4/utils.ts rename to packages/instrumentation-redis/test/v4-v5/utils.ts diff --git a/packages/instrumentation-redis/tsconfig.json b/packages/instrumentation-redis/tsconfig.json index 4078877ce6..1d2135f1cc 100644 --- a/packages/instrumentation-redis/tsconfig.json +++ b/packages/instrumentation-redis/tsconfig.json @@ -2,7 +2,12 @@ "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": ".", - "outDir": "build" + "outDir": "build", + /* Work-around for the node-redis type-checking bug + (see https://github.com/redis/node-redis/issues/3009). */ + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true }, "include": [ "src/**/*.ts",