From 68d88579f66f85dac22cf3c3f73b2e94a5eb11c4 Mon Sep 17 00:00:00 2001 From: Jamie Danielson Date: Thu, 9 Oct 2025 18:21:40 -0400 Subject: [PATCH 1/3] feat(instrumentation-memcached): support net.* and db.* semconv migration --- packages/instrumentation-memcached/README.md | 29 +++-- .../instrumentation-memcached/package.json | 2 +- .../src/instrumentation.ts | 79 ++++++++++--- .../test/index.test.ts | 105 +++++++++++++++++- 4 files changed, 187 insertions(+), 28 deletions(-) diff --git a/packages/instrumentation-memcached/README.md b/packages/instrumentation-memcached/README.md index d429346390..852870d1ff 100644 --- a/packages/instrumentation-memcached/README.md +++ b/packages/instrumentation-memcached/README.md @@ -44,23 +44,32 @@ registerInstrumentations({ ### Configuration Options -| Option | Type | Example | Description | -| ------- | ---- | ------- | ----------- | +| Option | Type | Example | Description | +| --------------------------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------------------- | | `enhancedDatabaseReporting` | `boolean` | `false` | Include full command statement in the span - **leaks potentially sensitive information to your spans**. Defaults to `false`. | ## 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 instrumentation implements Semantic Conventions (semconv) v1.7.0. Since then, networking (in semconv v1.23.1) and database (in semconv v1.33.0) semantic conventions were stabilized. As of `@opentelemetry/instrumentation-memcached@0.51.0` support has been added for migrating to the stable semantic conventions using the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable as follows: + +1. Upgrade to the latest version of this instrumentation package. +2. Set `OTEL_SEMCONV_STABILITY_OPT_IN=http/dup,database/dup` to emit both old and stable semantic conventions. (The `http` token is used to control the `net.*` attributes, the `database` token to control to `db.*` attributes.) +3. Modify alerts, dashboards, metrics, and other processes in your Observability system to use the stable semantic conventions. +4. Set `OTEL_SEMCONV_STABILITY_OPT_IN=http,database` to emit only the stable semantic conventions. + +By default, if `OTEL_SEMCONV_STABILITY_OPT_IN` includes neither of the above tokens, the old v1.7.0 semconv is used. +The intent is to provide an approximate 6 month time window for users of this instrumentation to migrate to the new database and networking semconv, after which a new minor version will use the new semconv by default and drop support for the old semconv. +See [the HTTP migration guide](https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/) and the [database migration guide](https://opentelemetry.io/docs/specs/semconv/non-normative/db-migration/) for details. Attributes collected: -| Attribute | Short Description | -|-----------------|-----------------------------------------------------------------------------| -| `db.operation` | The name of the operation being executed. | -| `db.statement` | The database statement being executed. | -| `db.system` | An identifier for the database management system (DBMS) product being used. | -| `net.peer.name` | Remote hostname or similar. | -| `net.peer.port` | Remote port number. | +| Old semconv | Stable semconv | Description | +| --------------- | ------------------- | --------------------------------------------------------------------------------------- | +| `db.system` | `db.system.name` | 'memcached' | +| `db.operation` | `db.operation.name` | The name of the operation being executed. | +| `db.statement` | `db.query.text` | The database statement being executed (only if `enhancedDatabaseReporting` is enabled). | +| `net.peer.name` | `server.address` | Remote hostname or similar. | +| `net.peer.port` | `server.port` | Remote port number. | ## Useful links diff --git a/packages/instrumentation-memcached/package.json b/packages/instrumentation-memcached/package.json index 28e4ecd135..d9d60d240f 100644 --- a/packages/instrumentation-memcached/package.json +++ b/packages/instrumentation-memcached/package.json @@ -55,7 +55,6 @@ "@opentelemetry/contrib-test-utils": "^0.53.0", "@opentelemetry/sdk-trace-base": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", - "@opentelemetry/semantic-conventions": "^1.27.0", "@types/mocha": "10.0.10", "@types/node": "18.18.14", "cross-env": "7.0.3", @@ -66,6 +65,7 @@ }, "dependencies": { "@opentelemetry/instrumentation": "^0.206.0", + "@opentelemetry/semantic-conventions": "^1.33.0", "@types/memcached": "^2.2.6" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/packages/instrumentation-memcached#readme" diff --git a/packages/instrumentation-memcached/src/instrumentation.ts b/packages/instrumentation-memcached/src/instrumentation.ts index 6e70442e8e..a7b3cfe3a5 100644 --- a/packages/instrumentation-memcached/src/instrumentation.ts +++ b/packages/instrumentation-memcached/src/instrumentation.ts @@ -19,6 +19,8 @@ import { isWrapped, InstrumentationBase, InstrumentationNodeModuleDefinition, + SemconvStability, + semconvStabilityFromStr, } from '@opentelemetry/instrumentation'; import type * as Memcached from 'memcached'; import { @@ -27,6 +29,12 @@ import { ATTR_DB_STATEMENT, ATTR_DB_SYSTEM, } from './semconv'; +import { + ATTR_DB_OPERATION_NAME, + ATTR_DB_QUERY_TEXT, + ATTR_DB_SYSTEM_NAME, +} from '@opentelemetry/semantic-conventions'; + import * as utils from './utils'; import { InstrumentationConfig } from './types'; /** @knipignore */ @@ -34,18 +42,31 @@ import { PACKAGE_NAME, PACKAGE_VERSION } from './version'; export class MemcachedInstrumentation extends InstrumentationBase { static readonly COMPONENT = 'memcached'; - static readonly COMMON_ATTRIBUTES = { - [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_MEMCACHED, - }; static readonly DEFAULT_CONFIG: InstrumentationConfig = { enhancedDatabaseReporting: false, }; + private _netSemconvStability!: SemconvStability; + private _dbSemconvStability!: SemconvStability; + constructor(config: InstrumentationConfig = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, { ...MemcachedInstrumentation.DEFAULT_CONFIG, ...config, }); + this._setSemconvStabilityFromEnv(); + } + + // Used for testing. + private _setSemconvStabilityFromEnv() { + this._netSemconvStability = semconvStabilityFromStr( + 'http', + process.env.OTEL_SEMCONV_STABILITY_OPT_IN + ); + this._dbSemconvStability = semconvStabilityFromStr( + 'database', + process.env.OTEL_SEMCONV_STABILITY_OPT_IN + ); } override setConfig(config: InstrumentationConfig = {}) { @@ -90,15 +111,24 @@ export class MemcachedInstrumentation extends InstrumentationBase { ]); }); }); + + describe('various values of OTEL_SEMCONV_STABILITY_OPT_IN', () => { + // Restore OTEL_SEMCONV_STABILITY_OPT_IN after we are done. + const _origOptInEnv = process.env.OTEL_SEMCONV_STABILITY_OPT_IN; + after(() => { + process.env.OTEL_SEMCONV_STABILITY_OPT_IN = _origOptInEnv; + (instrumentation as any)._setSemconvStabilityFromEnv(); + }); + + it('OTEL_SEMCONV_STABILITY_OPT_IN=(empty)', async () => { + process.env.OTEL_SEMCONV_STABILITY_OPT_IN = ''; + (instrumentation as any)._setSemconvStabilityFromEnv(); + + const client = getClient(`${CONFIG.host}:${CONFIG.port}`, { retries: 0 }); + await client.setPromise(KEY, VALUE, 10); + const value = await client.getPromise(KEY); + + assert.strictEqual(value, VALUE); + const instrumentationSpans = memoryExporter.getFinishedSpans(); + assert.strictEqual(instrumentationSpans.length, 2); + + const span = instrumentationSpans[1]; // get operation + // old `db.*` + assert.strictEqual(span.attributes[ATTR_DB_SYSTEM], DB_SYSTEM_VALUE_MEMCACHED); + assert.strictEqual(span.attributes[ATTR_DB_OPERATION], 'get'); + // stable `db.*` + assert.strictEqual(span.attributes[ATTR_DB_SYSTEM_NAME], undefined); + assert.strictEqual(span.attributes[ATTR_DB_OPERATION_NAME], undefined); + + // old `net.*` + assert.strictEqual(span.attributes[ATTR_NET_PEER_NAME], CONFIG.host); + assert.strictEqual(span.attributes[ATTR_NET_PEER_PORT], CONFIG.port); + // stable `net.*` + assert.strictEqual(span.attributes[ATTR_SERVER_ADDRESS], undefined); + assert.strictEqual(span.attributes[ATTR_SERVER_PORT], undefined); + + client.end(); + }); + + it('OTEL_SEMCONV_STABILITY_OPT_IN=http,database', async () => { + process.env.OTEL_SEMCONV_STABILITY_OPT_IN = 'http,database'; + (instrumentation as any)._setSemconvStabilityFromEnv(); + + const client = getClient(`${CONFIG.host}:${CONFIG.port}`, { retries: 0 }); + await client.setPromise(KEY, VALUE, 10); + const value = await client.getPromise(KEY); + + assert.strictEqual(value, VALUE); + const instrumentationSpans = memoryExporter.getFinishedSpans(); + assert.strictEqual(instrumentationSpans.length, 2); + + const span = instrumentationSpans[1]; // get operation + // old `db.*` + assert.strictEqual(span.attributes[ATTR_DB_SYSTEM], undefined); + assert.strictEqual(span.attributes[ATTR_DB_OPERATION], undefined); + // stable `db.*` + assert.strictEqual(span.attributes[ATTR_DB_SYSTEM_NAME], DB_SYSTEM_VALUE_MEMCACHED); + assert.strictEqual(span.attributes[ATTR_DB_OPERATION_NAME], 'get'); + + // old `net.*` + assert.strictEqual(span.attributes[ATTR_NET_PEER_NAME], undefined); + assert.strictEqual(span.attributes[ATTR_NET_PEER_PORT], undefined); + // stable `net.*` + assert.strictEqual(span.attributes[ATTR_SERVER_ADDRESS], CONFIG.host); + assert.strictEqual(span.attributes[ATTR_SERVER_PORT], CONFIG.port); + + client.end(); + }); + }); }); const assertSpans = (actualSpans: any[], expectedSpans: any[]) => { @@ -282,10 +366,28 @@ const assertSpans = (actualSpans: any[], expectedSpans: any[]) => { assertMatch(span.name, new RegExp(expected.op)); assertMatch(span.name, new RegExp('memcached')); assert.strictEqual(span.kind, SpanKind.CLIENT); - assert.strictEqual(span.attributes['db.statement'], expected.statement); + + // Verify both old and stable semconv attributes for (const attr in DEFAULT_ATTRIBUTES) { assert.strictEqual(span.attributes[attr], DEFAULT_ATTRIBUTES[attr]); } + + // Verify db.operation (old) and db.operation.name (stable) + assert.strictEqual(span.attributes[ATTR_DB_OPERATION], expected.op); + assert.strictEqual(span.attributes[ATTR_DB_OPERATION_NAME], expected.op); + + // Verify db.statement (old) and db.query.text (stable) if statement is expected + if (expected.statement !== undefined) { + assert.strictEqual(span.attributes['db.statement'], expected.statement); + assert.strictEqual( + span.attributes[ATTR_DB_QUERY_TEXT], + expected.statement + ); + } else { + assert.strictEqual(span.attributes['db.statement'], undefined); + assert.strictEqual(span.attributes[ATTR_DB_QUERY_TEXT], undefined); + } + assert.strictEqual(span.attributes['db.memcached.key'], expected.key); assert.strictEqual( typeof span.attributes['memcached.version'], @@ -296,7 +398,6 @@ const assertSpans = (actualSpans: any[], expectedSpans: any[]) => { span.status, expected.status || { code: SpanStatusCode.UNSET } ); - assert.strictEqual(span.attributes['db.operation'], expected.op); assert.strictEqual( span.parentSpanContext?.spanId, expected.parentSpan?.spanContext().spanId From f9ce510bf1dbbad5db7199787ccd89718ff3614a Mon Sep 17 00:00:00 2001 From: Jamie Danielson Date: Tue, 14 Oct 2025 15:08:01 -0400 Subject: [PATCH 2/3] oops need to commit utils --- .../instrumentation-memcached/src/utils.ts | 49 ++++++++++++++----- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/instrumentation-memcached/src/utils.ts b/packages/instrumentation-memcached/src/utils.ts index 3a08d7bb7a..ec12c6e7b8 100644 --- a/packages/instrumentation-memcached/src/utils.ts +++ b/packages/instrumentation-memcached/src/utils.ts @@ -15,13 +15,23 @@ */ import type * as Memcached from 'memcached'; -import { ATTR_NET_PEER_NAME, ATTR_NET_PEER_PORT } from './semconv'; +import { + ATTR_NET_PEER_NAME, + ATTR_NET_PEER_PORT, +} from './semconv'; +import { + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, +} from '@opentelemetry/semantic-conventions'; +import { SemconvStability } from '@opentelemetry/instrumentation'; +import { Attributes } from '@opentelemetry/api'; export const getPeerAttributes = ( client: any /* Memcached, but the type definitions are lacking */, server: string | undefined, - query: Memcached.CommandData -) => { + query: Memcached.CommandData, + netSemconvStability: SemconvStability +): Attributes => { if (!server) { if (client.servers.length === 1) { server = client.servers[0]; @@ -47,15 +57,32 @@ export const getPeerAttributes = ( const [host, port] = server && server.split(':'); if (host && port) { const portNumber = parseInt(port, 10); - if (!isNaN(portNumber)) { - return { - [ATTR_NET_PEER_NAME]: host, - [ATTR_NET_PEER_PORT]: portNumber, - }; + const attrs: Attributes = {}; + + if (netSemconvStability & SemconvStability.OLD) { + attrs[ATTR_NET_PEER_NAME] = host; + if (!isNaN(portNumber)) { + attrs[ATTR_NET_PEER_PORT] = portNumber; + } + } + if (netSemconvStability & SemconvStability.STABLE) { + attrs[ATTR_SERVER_ADDRESS] = host; + if (!isNaN(portNumber)) { + attrs[ATTR_SERVER_PORT] = portNumber; + } + } + + return attrs; + } + if (host) { + const attrs: Attributes = {}; + if (netSemconvStability & SemconvStability.OLD) { + attrs[ATTR_NET_PEER_NAME] = host; + } + if (netSemconvStability & SemconvStability.STABLE) { + attrs[ATTR_SERVER_ADDRESS] = host; } - return { - [ATTR_NET_PEER_NAME]: host, - }; + return attrs; } } return {}; From 9743a7ac1f9ce360548150134aed764515cb340e Mon Sep 17 00:00:00 2001 From: Jamie Danielson Date: Tue, 14 Oct 2025 15:10:44 -0400 Subject: [PATCH 3/3] rename misleading var in test --- packages/instrumentation-memcached/test/index.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/instrumentation-memcached/test/index.test.ts b/packages/instrumentation-memcached/test/index.test.ts index 869f2a97fd..496bd8cd8e 100644 --- a/packages/instrumentation-memcached/test/index.test.ts +++ b/packages/instrumentation-memcached/test/index.test.ts @@ -59,7 +59,7 @@ const CONFIG = { : 27017, }; -const DEFAULT_ATTRIBUTES: Attributes = { +const ATTRIBUTES: Attributes = { // Old semconv [ATTR_DB_SYSTEM]: DB_SYSTEM_VALUE_MEMCACHED, [ATTR_NET_PEER_NAME]: CONFIG.host, @@ -368,8 +368,8 @@ const assertSpans = (actualSpans: any[], expectedSpans: any[]) => { assert.strictEqual(span.kind, SpanKind.CLIENT); // Verify both old and stable semconv attributes - for (const attr in DEFAULT_ATTRIBUTES) { - assert.strictEqual(span.attributes[attr], DEFAULT_ATTRIBUTES[attr]); + for (const attr in ATTRIBUTES) { + assert.strictEqual(span.attributes[attr], ATTRIBUTES[attr]); } // Verify db.operation (old) and db.operation.name (stable)