diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 49eb60e237..37252e92cc 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -10,10 +10,10 @@ All notable changes to experimental packages in this project will be documented ### :rocket: (Enhancement) * feat(api-logs): Add delegating no-op logger provider [#4861](https://github.com/open-telemetry/opentelemetry-js/pull/4861) @hectorhdzg -* feat(instrumentation-http): Add support for [Semantic Conventions 1.27+](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.27.0) [#4940](https://github.com/open-telemetry/opentelemetry-js/pull/4940) [#4978](https://github.com/open-telemetry/opentelemetry-js/pull/4978) @dyladan - * Applies to both client and server spans - * Generate spans compliant with Semantic Conventions 1.27+ when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http` or `http/dup` - * Generate spans backwards compatible with previous attributes when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http/dup` or DOES NOT contain `http` +* feat(instrumentation-http): Add support for [Semantic Conventions 1.27+](https://github.com/open-telemetry/semantic-conventions/releases/tag/v1.27.0) [#4940](https://github.com/open-telemetry/opentelemetry-js/pull/4940) [#4978](https://github.com/open-telemetry/opentelemetry-js/pull/4978) [#5026](https://github.com/open-telemetry/opentelemetry-js/pull/5026) @dyladan + * Applies to client and server spans and metrics + * Generate spans and metrics compliant with Semantic Conventions 1.27+ when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http` or `http/dup` + * Generate spans and metrics backwards compatible with previous attributes when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http/dup` or DOES NOT contain `http` ### :bug: (Bug Fix) diff --git a/experimental/packages/opentelemetry-instrumentation-http/README.md b/experimental/packages/opentelemetry-instrumentation-http/README.md index 456d5b3473..030e8f327b 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/README.md +++ b/experimental/packages/opentelemetry-instrumentation-http/README.md @@ -76,8 +76,6 @@ The following options are deprecated: ## Semantic Conventions -### Client and Server Spans - Prior to version `0.54`, 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). This package is capable of emitting both Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md) and [Version 1.27.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-spans.md). @@ -86,21 +84,19 @@ The values `http` and `http/dup` control this instrumentation. See details for the behavior of each of these values below. If neither `http` or `http/dup` is included in `OTEL_SEMCONV_STABILITY_OPT_IN`, the old experimental semantic conventions will be used by default. -#### Stable Semantic Conventions 1.27 +### Stable Semantic Conventions 1.27 Enabled when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http` OR `http/dup`. This is the recommended configuration, and will soon become the default behavior. -Follow all requirements and recommendations of HTTP Client and Server Span Semantic Conventions [Version 1.27.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-spans.md), including all required and recommended attributes. - -#### Legacy Behavior (default) +Follow all requirements and recommendations of HTTP Client and Server Semantic Conventions Version 1.27.0 for [spans](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-spans.md) and [metrics](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-metrics.md), including all required and recommended attributes. -Enabled when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http/dup` or DOES NOT CONTAIN `http`. -This is the current default behavior. +Metrics Exported: -Create HTTP client spans which implement Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md). +- [`http.server.request.duration`](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-metrics.md#metric-httpserverrequestduration) +- [`http.client.request.duration`](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-metrics.md#metric-httpclientrequestduration) -#### Upgrading Semantic Conventions +### Upgrading Semantic Conventions When upgrading to the new semantic conventions, it is recommended to do so in the following order: @@ -111,9 +107,16 @@ When upgrading to the new semantic conventions, it is recommended to do so in th This will cause both the old and new semantic conventions to be emitted during the transition period. -### Server Spans +### Legacy Behavior (default) + +Enabled when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http/dup` or DOES NOT CONTAIN `http`. +This is the current default behavior. + +Create HTTP client spans which implement Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md). + +#### Server Spans (legacy) -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). +When `OTEL_SEMCONV_STABILITY_OPT_IN` is not set or includes `http/dup`, this module implements Semantic Convention [Version 1.7.0](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/semantic_conventions/README.md). Attributes collected: diff --git a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts index 456b8d08e0..0d0a63920b 100644 --- a/experimental/packages/opentelemetry-instrumentation-http/src/http.ts +++ b/experimental/packages/opentelemetry-instrumentation-http/src/http.ts @@ -62,7 +62,18 @@ import { getEnv, } from '@opentelemetry/core'; import { errorMonitor } from 'events'; -import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; +import { + ATTR_HTTP_REQUEST_METHOD, + ATTR_HTTP_RESPONSE_STATUS_CODE, + ATTR_HTTP_ROUTE, + ATTR_NETWORK_PROTOCOL_VERSION, + ATTR_SERVER_ADDRESS, + ATTR_SERVER_PORT, + ATTR_URL_SCHEME, + METRIC_HTTP_CLIENT_REQUEST_DURATION, + METRIC_HTTP_SERVER_REQUEST_DURATION, + SEMATTRS_HTTP_ROUTE, +} from '@opentelemetry/semantic-conventions'; import { extractHostnameAndPort, getIncomingRequestAttributes, @@ -88,8 +99,10 @@ export class HttpInstrumentation extends InstrumentationBase = new WeakSet(); private _headerCapture; - private _httpServerDurationHistogram!: Histogram; - private _httpClientDurationHistogram!: Histogram; + private _oldHttpServerDurationHistogram!: Histogram; + private _stableHttpServerDurationHistogram!: Histogram; + private _oldHttpClientDurationHistogram!: Histogram; + private _stableHttpClientDurationHistogram!: Histogram; private _semconvStability = SemconvStability.OLD; @@ -109,7 +122,7 @@ export class HttpInstrumentation extends InstrumentationBase { this._diag.debug('outgoingRequest on request error()', error); @@ -458,7 +558,13 @@ export class HttpInstrumentation extends InstrumentationBase { + let contextManager: ContextManager; + beforeEach(() => { + contextManager = new AsyncHooksContextManager().enable(); + context.setGlobalContextManager(contextManager); + instrumentation['_updateMetricInstruments'](); metricsMemoryExporter.reset(); }); before(() => { + instrumentation.setConfig({}); instrumentation.enable(); server = http.createServer((request, response) => { + const rpcData = getRPCMetadata(context.active()); + assert.ok(rpcData != null); + assert.strictEqual(rpcData.type, RPCType.HTTP); + assert.strictEqual(rpcData.route, undefined); + rpcData.route = 'TheRoute'; response.end('Test Server Response'); }); server.listen(serverPort); @@ -74,87 +95,289 @@ describe('metrics', () => { instrumentation.disable(); }); - it('should add server/client duration metrics', async () => { - const requestCount = 3; - for (let i = 0; i < requestCount; i++) { - await httpRequest.get( - `${protocol}://${hostname}:${serverPort}${pathname}` - ); - } - await metricReader.collectAndExport(); - const resourceMetrics = metricsMemoryExporter.getMetrics(); - const scopeMetrics = resourceMetrics[0].scopeMetrics; - assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); - const metrics = scopeMetrics[0].metrics; - assert.strictEqual(metrics.length, 2, 'metrics count'); - assert.strictEqual(metrics[0].dataPointType, DataPointType.HISTOGRAM); - assert.strictEqual( - metrics[0].descriptor.description, - 'Measures the duration of inbound HTTP requests.' - ); - assert.strictEqual(metrics[0].descriptor.name, 'http.server.duration'); - assert.strictEqual(metrics[0].descriptor.unit, 'ms'); - assert.strictEqual(metrics[0].dataPoints.length, 1); - assert.strictEqual( - (metrics[0].dataPoints[0].value as any).count, - requestCount - ); - assert.strictEqual( - metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_SCHEME], - 'http' - ); - assert.strictEqual( - metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_METHOD], - 'GET' - ); - assert.strictEqual( - metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_FLAVOR], - '1.1' - ); - assert.strictEqual( - metrics[0].dataPoints[0].attributes[SEMATTRS_NET_HOST_NAME], - 'localhost' - ); - assert.strictEqual( - metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_STATUS_CODE], - 200 - ); - assert.strictEqual( - metrics[0].dataPoints[0].attributes[SEMATTRS_NET_HOST_PORT], - 22346 - ); - - assert.strictEqual(metrics[1].dataPointType, DataPointType.HISTOGRAM); - assert.strictEqual( - metrics[1].descriptor.description, - 'Measures the duration of outbound HTTP requests.' - ); - assert.strictEqual(metrics[1].descriptor.name, 'http.client.duration'); - assert.strictEqual(metrics[1].descriptor.unit, 'ms'); - assert.strictEqual(metrics[1].dataPoints.length, 1); - assert.strictEqual( - (metrics[1].dataPoints[0].value as any).count, - requestCount - ); - assert.strictEqual( - metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_METHOD], - 'GET' - ); - assert.strictEqual( - metrics[1].dataPoints[0].attributes[SEMATTRS_NET_PEER_NAME], - 'localhost' - ); - assert.strictEqual( - metrics[1].dataPoints[0].attributes[SEMATTRS_NET_PEER_PORT], - 22346 - ); - assert.strictEqual( - metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_STATUS_CODE], - 200 - ); - assert.strictEqual( - metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_FLAVOR], - '1.1' - ); + describe('with no stability set', () => { + it('should add server/client duration metrics', async () => { + const requestCount = 3; + for (let i = 0; i < requestCount; i++) { + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + } + await metricReader.collectAndExport(); + const resourceMetrics = metricsMemoryExporter.getMetrics(); + const scopeMetrics = resourceMetrics[0].scopeMetrics; + assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); + const metrics = scopeMetrics[0].metrics; + assert.strictEqual(metrics.length, 2, 'metrics count'); + assert.strictEqual(metrics[0].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual( + metrics[0].descriptor.description, + 'Measures the duration of inbound HTTP requests.' + ); + assert.strictEqual(metrics[0].descriptor.name, 'http.server.duration'); + assert.strictEqual(metrics[0].descriptor.unit, 'ms'); + assert.strictEqual(metrics[0].dataPoints.length, 1); + assert.strictEqual( + (metrics[0].dataPoints[0].value as any).count, + requestCount + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_SCHEME], + 'http' + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_METHOD], + 'GET' + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_FLAVOR], + '1.1' + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_NET_HOST_NAME], + 'localhost' + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_STATUS_CODE], + 200 + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_NET_HOST_PORT], + 22346 + ); + + assert.strictEqual(metrics[1].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual( + metrics[1].descriptor.description, + 'Measures the duration of outbound HTTP requests.' + ); + assert.strictEqual(metrics[1].descriptor.name, 'http.client.duration'); + assert.strictEqual(metrics[1].descriptor.unit, 'ms'); + assert.strictEqual(metrics[1].dataPoints.length, 1); + assert.strictEqual( + (metrics[1].dataPoints[0].value as any).count, + requestCount + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_METHOD], + 'GET' + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_NET_PEER_NAME], + 'localhost' + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_NET_PEER_PORT], + 22346 + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_STATUS_CODE], + 200 + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_FLAVOR], + '1.1' + ); + }); + }); + + describe('with no semconv stability set to stable', () => { + before(() => { + instrumentation['_semconvStability'] = SemconvStability.STABLE; + }); + + it('should add server/client duration metrics', async () => { + const requestCount = 3; + for (let i = 0; i < requestCount; i++) { + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + } + await metricReader.collectAndExport(); + const resourceMetrics = metricsMemoryExporter.getMetrics(); + const scopeMetrics = resourceMetrics[0].scopeMetrics; + assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); + const metrics = scopeMetrics[0].metrics; + assert.strictEqual(metrics.length, 2, 'metrics count'); + assert.strictEqual(metrics[0].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual( + metrics[0].descriptor.description, + 'Duration of HTTP server requests.' + ); + assert.strictEqual( + metrics[0].descriptor.name, + 'http.server.request.duration' + ); + assert.strictEqual(metrics[0].descriptor.unit, 's'); + assert.strictEqual(metrics[0].dataPoints.length, 1); + assert.strictEqual( + (metrics[0].dataPoints[0].value as any).count, + requestCount + ); + assert.deepStrictEqual(metrics[0].dataPoints[0].attributes, { + [ATTR_HTTP_REQUEST_METHOD]: 'GET', + [ATTR_URL_SCHEME]: 'http', + [ATTR_NETWORK_PROTOCOL_VERSION]: '1.1', + }); + + assert.strictEqual(metrics[1].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual( + metrics[1].descriptor.description, + 'Duration of HTTP client requests.' + ); + assert.strictEqual( + metrics[1].descriptor.name, + 'http.client.request.duration' + ); + assert.strictEqual(metrics[1].descriptor.unit, 's'); + assert.strictEqual(metrics[1].dataPoints.length, 1); + assert.strictEqual( + (metrics[1].dataPoints[0].value as any).count, + requestCount + ); + + assert.deepStrictEqual(metrics[1].dataPoints[0].attributes, { + [ATTR_HTTP_REQUEST_METHOD]: 'GET', + [ATTR_SERVER_ADDRESS]: 'localhost', + [ATTR_SERVER_PORT]: 22346, + }); + }); + }); + + describe('with no semconv stability set to duplicate', () => { + before(() => { + instrumentation['_semconvStability'] = SemconvStability.DUPLICATE; + }); + + it('should add server/client duration metrics', async () => { + const requestCount = 3; + for (let i = 0; i < requestCount; i++) { + await httpRequest.get( + `${protocol}://${hostname}:${serverPort}${pathname}` + ); + } + await metricReader.collectAndExport(); + const resourceMetrics = metricsMemoryExporter.getMetrics(); + const scopeMetrics = resourceMetrics[0].scopeMetrics; + assert.strictEqual(scopeMetrics.length, 1, 'scopeMetrics count'); + const metrics = scopeMetrics[0].metrics; + assert.strictEqual(metrics.length, 4, 'metrics count'); + + // old metrics + assert.strictEqual(metrics[0].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual( + metrics[0].descriptor.description, + 'Measures the duration of inbound HTTP requests.' + ); + assert.strictEqual(metrics[0].descriptor.name, 'http.server.duration'); + assert.strictEqual(metrics[0].descriptor.unit, 'ms'); + assert.strictEqual(metrics[0].dataPoints.length, 1); + assert.strictEqual( + (metrics[0].dataPoints[0].value as any).count, + requestCount + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_SCHEME], + 'http' + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_METHOD], + 'GET' + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_FLAVOR], + '1.1' + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_NET_HOST_NAME], + 'localhost' + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_HTTP_STATUS_CODE], + 200 + ); + assert.strictEqual( + metrics[0].dataPoints[0].attributes[SEMATTRS_NET_HOST_PORT], + 22346 + ); + + assert.strictEqual(metrics[1].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual( + metrics[1].descriptor.description, + 'Measures the duration of outbound HTTP requests.' + ); + assert.strictEqual(metrics[1].descriptor.name, 'http.client.duration'); + assert.strictEqual(metrics[1].descriptor.unit, 'ms'); + assert.strictEqual(metrics[1].dataPoints.length, 1); + assert.strictEqual( + (metrics[1].dataPoints[0].value as any).count, + requestCount + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_METHOD], + 'GET' + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_NET_PEER_NAME], + 'localhost' + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_NET_PEER_PORT], + 22346 + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_STATUS_CODE], + 200 + ); + assert.strictEqual( + metrics[1].dataPoints[0].attributes[SEMATTRS_HTTP_FLAVOR], + '1.1' + ); + + // Stable metrics + assert.strictEqual(metrics[2].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual( + metrics[2].descriptor.description, + 'Duration of HTTP server requests.' + ); + assert.strictEqual( + metrics[2].descriptor.name, + 'http.server.request.duration' + ); + assert.strictEqual(metrics[2].descriptor.unit, 's'); + assert.strictEqual(metrics[2].dataPoints.length, 1); + assert.strictEqual( + (metrics[2].dataPoints[0].value as any).count, + requestCount + ); + assert.deepStrictEqual(metrics[2].dataPoints[0].attributes, { + [ATTR_HTTP_REQUEST_METHOD]: 'GET', + [ATTR_URL_SCHEME]: 'http', + [ATTR_NETWORK_PROTOCOL_VERSION]: '1.1', + [ATTR_HTTP_ROUTE]: 'TheRoute', + }); + + assert.strictEqual(metrics[3].dataPointType, DataPointType.HISTOGRAM); + assert.strictEqual( + metrics[3].descriptor.description, + 'Duration of HTTP client requests.' + ); + assert.strictEqual( + metrics[3].descriptor.name, + 'http.client.request.duration' + ); + assert.strictEqual(metrics[3].descriptor.unit, 's'); + assert.strictEqual(metrics[3].dataPoints.length, 1); + assert.strictEqual( + (metrics[3].dataPoints[0].value as any).count, + requestCount + ); + + assert.deepStrictEqual(metrics[3].dataPoints[0].attributes, { + [ATTR_HTTP_REQUEST_METHOD]: 'GET', + [ATTR_SERVER_ADDRESS]: 'localhost', + [ATTR_SERVER_PORT]: 22346, + }); + }); }); });