From 67d7102ce4326ebcd29b9e93fba76d396aaf9d45 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Wed, 28 May 2025 12:33:24 -0700 Subject: [PATCH 1/3] feat(instrumentation-hapi): support migration to stable HTTP semconv, v1.23.1 `instrumentation-hapi`, in addition to providing the HTTP route for the *HTTP* instrumentation, also generates hapi spans for each plugin/route. Attributes on these hapi spans include some covered by HTTP semantic conventions. This change adds support for controlled migration from the old to the stable HTTP semconv via the `OTEL_SEMCONV_STABILITY_OPT_IN` envvar. See https://opentelemetry.io/docs/specs/semconv/non-normative/http-migration/ This also updates from the deprecated `SpanAttributes` api type to `Attributes`. Refs: https://github.com/open-telemetry/opentelemetry-js/issues/5663 (HTTP semconv) Refs: https://github.com/open-telemetry/opentelemetry-js/issues/4175 (Attributes) --- .../README.md | 24 +++-- .../src/instrumentation.ts | 10 +- .../src/semconv.ts | 34 ++++++ .../src/utils.ts | 54 ++++++---- .../test/fixtures/use-hapi.js | 65 ++++++++++++ .../test/fixtures/use-hapi.mjs | 2 +- .../test/hapi.test.ts | 100 ++++++++++++++++++ 7 files changed, 258 insertions(+), 31 deletions(-) create mode 100644 plugins/node/opentelemetry-instrumentation-hapi/src/semconv.ts create mode 100644 plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.js diff --git a/plugins/node/opentelemetry-instrumentation-hapi/README.md b/plugins/node/opentelemetry-instrumentation-hapi/README.md index 7a5e2c3ac7..5b7f8e629a 100644 --- a/plugins/node/opentelemetry-instrumentation-hapi/README.md +++ b/plugins/node/opentelemetry-instrumentation-hapi/README.md @@ -60,7 +60,7 @@ hapiInstrumentation.setTracerProvider(provider); See [examples/hapi](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/examples/hapi) for a short example using Hapi - @@ -70,14 +70,24 @@ This package provides automatic tracing for hapi server routes and [request life ## 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) +Prior to version `0.48.0`, this instrumentation created spans targeting an experimental semantic convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md). -Attributes collected: +HTTP semantic conventions (semconv) were stabilized in v1.23.0, and a [migration process](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/non-normative/http-migration.md#http-semantic-convention-stability-migration) was defined. `instrumentation-hapi` versions 0.48.0 and later include support for migrating to stable HTTP semantic conventions, as described below. The intent is to provide an approximate 6 month time window for users of this instrumentation to migrate to the new HTTP semconv, after which a new minor version will use the *new* semconv by default and drop support for the old semconv. See the [HTTP semconv migration plan for OpenTelemetry JS instrumentations](https://github.com/open-telemetry/opentelemetry-js/issues/5646). -| Attribute | Short Description | -|---------------------|----------------------------------------------------| -| `http.method` | HTTP method | -| `http.route` | Route assigned to handler. Ex: `/users/:id` | +To select which semconv version(s) is emitted from this instrumentation, use the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. + +- `http`: emit the new (stable) v1.23.0+ semantics +- `http/dep`: emit **both** the old v1.7.0 and the new (stable) v1.23.0+ semantics +- By default, if `OTEL_SEMCONV_STABILITY_OPT_IN` includes neither of the above tokens, the old v1.7.0 semconv is used. + +### Attributes collected + +The following semconv attributes are collected on hapi route spans: + +| v1.7.0 semconv | v1.23.0 semconv | Notes | +| -------------- | --------------------- | ----- | +| `http.method` | `http.request.method` | HTTP request method | +| `http.route` | `http.route` (same) | Route assigned to handler. Ex: `/users/:id` | ## Useful links diff --git a/plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts index b3e619b3b8..300eda7ce4 100644 --- a/plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts @@ -21,6 +21,8 @@ import { InstrumentationConfig, InstrumentationNodeModuleDefinition, isWrapped, + SemconvStability, + semconvStabilityFromStr, } from '@opentelemetry/instrumentation'; import type * as Hapi from '@hapi/hapi'; @@ -50,8 +52,14 @@ import { /** Hapi instrumentation for OpenTelemetry */ export class HapiInstrumentation extends InstrumentationBase { + private _semconvStability: SemconvStability; + constructor(config: InstrumentationConfig = {}) { super(PACKAGE_NAME, PACKAGE_VERSION, config); + this._semconvStability = semconvStabilityFromStr( + 'http', + process.env.OTEL_SEMCONV_STABILITY_OPT_IN + ); } protected init() { @@ -387,7 +395,7 @@ export class HapiInstrumentation extends InstrumentationBase { if (rpcMetadata?.type === RPCType.HTTP) { rpcMetadata.route = route.path; } - const metadata = getRouteMetadata(route, pluginName); + const metadata = getRouteMetadata(route, instrumentation._semconvStability, pluginName); const span = instrumentation.tracer.startSpan(metadata.name, { attributes: metadata.attributes, }); diff --git a/plugins/node/opentelemetry-instrumentation-hapi/src/semconv.ts b/plugins/node/opentelemetry-instrumentation-hapi/src/semconv.ts new file mode 100644 index 0000000000..e146b0a1f3 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-hapi/src/semconv.ts @@ -0,0 +1,34 @@ +/* + * 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. + */ + +/* + * This file contains a copy of unstable semantic convention definitions + * used by this package. + * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv + */ + +/** + * Deprecated, use `http.request.method` instead. + * + * @example GET + * @example POST + * @example HEAD + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Replaced by `http.request.method`. + */ +export const ATTR_HTTP_METHOD = 'http.method' as const; diff --git a/plugins/node/opentelemetry-instrumentation-hapi/src/utils.ts b/plugins/node/opentelemetry-instrumentation-hapi/src/utils.ts index c8b829e25a..f54d275a1a 100644 --- a/plugins/node/opentelemetry-instrumentation-hapi/src/utils.ts +++ b/plugins/node/opentelemetry-instrumentation-hapi/src/utils.ts @@ -14,11 +14,12 @@ * limitations under the License. */ -import { SpanAttributes } from '@opentelemetry/api'; +import { Attributes } from '@opentelemetry/api'; import { - SEMATTRS_HTTP_METHOD, - SEMATTRS_HTTP_ROUTE, + ATTR_HTTP_ROUTE, + ATTR_HTTP_REQUEST_METHOD, } from '@opentelemetry/semantic-conventions'; +import { ATTR_HTTP_METHOD } from './semconv'; import type * as Hapi from '@hapi/hapi'; import { HapiLayerType, @@ -28,6 +29,7 @@ import { ServerExtDirectInput, } from './internal-types'; import { AttributeNames } from './enums/AttributeNames'; +import { SemconvStability } from '@opentelemetry/instrumentation'; export function getPluginName(plugin: Hapi.Plugin): string { if ((plugin as Hapi.PluginNameVersion).name) { @@ -72,37 +74,45 @@ export const isPatchableExtMethod = ( export const getRouteMetadata = ( route: Hapi.ServerRoute, + semconvStability: SemconvStability, pluginName?: string ): { - attributes: SpanAttributes; + attributes: Attributes; name: string; } => { + const attributes: Attributes = { + [ATTR_HTTP_ROUTE]: route.path, + }; + if (semconvStability & SemconvStability.OLD) { + attributes[ATTR_HTTP_METHOD] = route.method; + } + if (semconvStability & SemconvStability.STABLE) { + // Note: This currently does *not* normalize the method name to uppercase + // and conditionally include `http.request.method.original` as described + // at https://opentelemetry.io/docs/specs/semconv/http/http-spans/ + // These attributes are for a *hapi* span, and not the parent HTTP span, + // so the HTTP span guidance doesn't strictly apply. + attributes[ATTR_HTTP_REQUEST_METHOD] = route.method; + } + + let name; if (pluginName) { - return { - attributes: { - [SEMATTRS_HTTP_ROUTE]: route.path, - [SEMATTRS_HTTP_METHOD]: route.method, - [AttributeNames.HAPI_TYPE]: HapiLayerType.PLUGIN, - [AttributeNames.PLUGIN_NAME]: pluginName, - }, - name: `${pluginName}: route - ${route.path}`, - }; + attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.PLUGIN; + attributes[AttributeNames.PLUGIN_NAME] = pluginName; + name = `${pluginName}: route - ${route.path}`; + } else { + attributes[AttributeNames.HAPI_TYPE] = HapiLayerType.ROUTER; + name = `route - ${route.path}`; } - return { - attributes: { - [SEMATTRS_HTTP_ROUTE]: route.path, - [SEMATTRS_HTTP_METHOD]: route.method, - [AttributeNames.HAPI_TYPE]: HapiLayerType.ROUTER, - }, - name: `route - ${route.path}`, - }; + + return { attributes, name }; }; export const getExtMetadata = ( extPoint: Hapi.ServerRequestExtType, pluginName?: string ): { - attributes: SpanAttributes; + attributes: Attributes; name: string; } => { if (pluginName) { diff --git a/plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.js b/plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.js new file mode 100644 index 0000000000..a898bbd181 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.js @@ -0,0 +1,65 @@ +/* + * 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. + */ + +const { createTestNodeSdk } = require('@opentelemetry/contrib-test-utils'); + +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); +const { HapiInstrumentation } = require('../../build/src/index.js'); + +const sdk = createTestNodeSdk({ + serviceName: 'use-hapi', + instrumentations: [ + new HttpInstrumentation(), + new HapiInstrumentation() + ] +}) +sdk.start(); + +const Hapi = require('@hapi/hapi'); +const http = require('http'); + +async function main() { + // Start a Hapi server. + const server = new Hapi.Server({ + port: 0, + host: 'localhost' + }); + + server.route({ + method: 'GET', + path: '/route/{param}', + handler: function() { + return { hello: 'world' }; + } + }); + + await server.start(); + + // Make a single request to it. + await new Promise(resolve => { + http.get(`http://${server.info.host}:${server.info.port}/route/test`, (res) => { + res.resume(); + res.on('end', () => { + resolve(); + }); + }); + }); + + await server.stop(); + await sdk.shutdown(); +} + +main(); diff --git a/plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.mjs b/plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.mjs index f79b48f8e3..665998fdfe 100644 --- a/plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.mjs +++ b/plugins/node/opentelemetry-instrumentation-hapi/test/fixtures/use-hapi.mjs @@ -57,7 +57,7 @@ await new Promise(resolve => { res.on('end', () => { resolve(); }); - }) + }); }); await server.stop(); diff --git a/plugins/node/opentelemetry-instrumentation-hapi/test/hapi.test.ts b/plugins/node/opentelemetry-instrumentation-hapi/test/hapi.test.ts index d79199cac9..f0aeabdb5e 100644 --- a/plugins/node/opentelemetry-instrumentation-hapi/test/hapi.test.ts +++ b/plugins/node/opentelemetry-instrumentation-hapi/test/hapi.test.ts @@ -25,6 +25,7 @@ import { import { runTestFixture, TestCollector, + TestSpan, } from '@opentelemetry/contrib-test-utils'; import { getPlugin } from './plugin'; const plugin = getPlugin(); @@ -33,6 +34,16 @@ import * as assert from 'assert'; import * as hapi from '@hapi/hapi'; import { HapiLayerType } from '../src/internal-types'; import { AttributeNames } from '../src/enums/AttributeNames'; +import { ATTR_HTTP_REQUEST_METHOD } from '@opentelemetry/semantic-conventions'; +import { ATTR_HTTP_METHOD } from '../src/semconv'; + +function getTestSpanAttr(span: TestSpan, name: string) { + const kv = span.attributes.filter(a => a.key === name)[0]; + if (kv) { + return kv.value; + } + return undefined; +} describe('Hapi Instrumentation - Core Tests', () => { const memoryExporter = new InMemorySpanExporter(); @@ -582,4 +593,93 @@ describe('Hapi Instrumentation - Core Tests', () => { }); }); }); + + describe('HTTP semconv migration', () => { + it('should emit only old HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN unset', async () => { + await runTestFixture({ + cwd: __dirname, + argv: ['fixtures/use-hapi.js'], + env: { + OTEL_SEMCONV_STABILITY_OPT_IN: '', + }, + checkResult: (err, stdout, stderr) => { + assert.ifError(err); + }, + checkCollector: (collector: TestCollector) => { + const spans = collector.sortedSpans; + assert.equal(spans.length, 3); + const hapiSpan = spans[2]; + assert.equal( + hapiSpan.instrumentationScope.name, + '@opentelemetry/instrumentation-hapi' + ); + assert.equal( + getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD)?.stringValue, + 'GET' + ); + assert.equal( + getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD), + undefined + ); + }, + }); + }); + + it('should emit only stable HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN=http', async () => { + await runTestFixture({ + cwd: __dirname, + argv: ['fixtures/use-hapi.js'], + env: { + OTEL_SEMCONV_STABILITY_OPT_IN: 'http', + }, + checkResult: (err, stdout, stderr) => { + assert.ifError(err); + }, + checkCollector: (collector: TestCollector) => { + const spans = collector.sortedSpans; + assert.equal(spans.length, 3); + const hapiSpan = spans[2]; + assert.equal( + hapiSpan.instrumentationScope.name, + '@opentelemetry/instrumentation-hapi' + ); + assert.equal(getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD), undefined); + assert.equal( + getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD)?.stringValue, + 'GET' + ); + }, + }); + }); + + it('should emit both old and stable HTTP semconv with OTEL_SEMCONV_STABILITY_OPT_IN=http/dup', async () => { + await runTestFixture({ + cwd: __dirname, + argv: ['fixtures/use-hapi.js'], + env: { + OTEL_SEMCONV_STABILITY_OPT_IN: 'http/dup', + }, + checkResult: (err, stdout, stderr) => { + assert.ifError(err); + }, + checkCollector: (collector: TestCollector) => { + const spans = collector.sortedSpans; + assert.equal(spans.length, 3); + const hapiSpan = spans[2]; + assert.equal( + hapiSpan.instrumentationScope.name, + '@opentelemetry/instrumentation-hapi' + ); + assert.equal( + getTestSpanAttr(hapiSpan, ATTR_HTTP_METHOD)?.stringValue, + 'GET' + ); + assert.equal( + getTestSpanAttr(hapiSpan, ATTR_HTTP_REQUEST_METHOD)?.stringValue, + 'GET' + ); + }, + }); + }); + }); }); From 3ac945d44bcbc09a629df36285f813db6749b987 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Wed, 28 May 2025 12:40:13 -0700 Subject: [PATCH 2/3] lint:fix --- .../src/instrumentation.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts index 300eda7ce4..3b4d16647b 100644 --- a/plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-hapi/src/instrumentation.ts @@ -395,7 +395,11 @@ export class HapiInstrumentation extends InstrumentationBase { if (rpcMetadata?.type === RPCType.HTTP) { rpcMetadata.route = route.path; } - const metadata = getRouteMetadata(route, instrumentation._semconvStability, pluginName); + const metadata = getRouteMetadata( + route, + instrumentation._semconvStability, + pluginName + ); const span = instrumentation.tracer.startSpan(metadata.name, { attributes: metadata.attributes, }); From 8d4865ed4450c391714779abe90c9d4b72255136 Mon Sep 17 00:00:00 2001 From: Trent Mick Date: Fri, 30 May 2025 10:23:14 -0700 Subject: [PATCH 3/3] fix typo: http/dup is the token to use --- plugins/node/opentelemetry-instrumentation-hapi/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/node/opentelemetry-instrumentation-hapi/README.md b/plugins/node/opentelemetry-instrumentation-hapi/README.md index 5b7f8e629a..967b9fb24d 100644 --- a/plugins/node/opentelemetry-instrumentation-hapi/README.md +++ b/plugins/node/opentelemetry-instrumentation-hapi/README.md @@ -77,7 +77,7 @@ HTTP semantic conventions (semconv) were stabilized in v1.23.0, and a [migration To select which semconv version(s) is emitted from this instrumentation, use the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. - `http`: emit the new (stable) v1.23.0+ semantics -- `http/dep`: emit **both** the old v1.7.0 and the new (stable) v1.23.0+ semantics +- `http/dup`: emit **both** the old v1.7.0 and the new (stable) v1.23.0+ semantics - By default, if `OTEL_SEMCONV_STABILITY_OPT_IN` includes neither of the above tokens, the old v1.7.0 semconv is used. ### Attributes collected