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 6ffd90c932..cbd6832c1c 100644 --- a/packages/instrumentation-memcached/package.json +++ b/packages/instrumentation-memcached/package.json @@ -53,7 +53,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", @@ -64,6 +63,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 { + 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 {}; diff --git a/packages/instrumentation-memcached/test/index.test.ts b/packages/instrumentation-memcached/test/index.test.ts index 41b51276c3..496bd8cd8e 100644 --- a/packages/instrumentation-memcached/test/index.test.ts +++ b/packages/instrumentation-memcached/test/index.test.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +process.env.OTEL_SEMCONV_STABILITY_OPT_IN = 'http/dup,database/dup'; + import { Attributes, context, @@ -34,9 +36,17 @@ import { ATTR_EXCEPTION_MESSAGE } from '@opentelemetry/semantic-conventions'; import { DB_SYSTEM_VALUE_MEMCACHED, ATTR_DB_SYSTEM, + ATTR_DB_OPERATION, ATTR_NET_PEER_NAME, ATTR_NET_PEER_PORT, } from '../src/semconv'; +import { + ATTR_DB_SYSTEM_NAME, + ATTR_DB_OPERATION_NAME, + ATTR_DB_QUERY_TEXT, + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, +} from '@opentelemetry/semantic-conventions'; import * as util from 'util'; const instrumentation = new MemcachedInstrumentation(); @@ -49,10 +59,15 @@ 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, [ATTR_NET_PEER_PORT]: CONFIG.port, + // Stable semconv + [ATTR_DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MEMCACHED, + [ATTR_SERVER_ADDRESS]: CONFIG.host, + [ATTR_SERVER_PORT]: CONFIG.port, }; interface ExtendedMemcached extends Memcached { @@ -260,6 +275,75 @@ describe('memcached@2.x', () => { ]); }); }); + + 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); - for (const attr in DEFAULT_ATTRIBUTES) { - assert.strictEqual(span.attributes[attr], DEFAULT_ATTRIBUTES[attr]); + + // Verify both old and stable semconv attributes + for (const attr in ATTRIBUTES) { + assert.strictEqual(span.attributes[attr], 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