Skip to content

Commit f8ab559

Browse files
dyladanpichlermarcrenovate-bot
authored
Update HTTP client span semconv to 1.27 (#4940)
Co-authored-by: Marc Pichler <marcpi@edu.aau.at> Co-authored-by: Mend Renovate <bot@renovateapp.com> Co-authored-by: Marc Pichler <marc.pichler@dynatrace.com>
1 parent 61c3744 commit f8ab559

File tree

9 files changed

+413
-77
lines changed

9 files changed

+413
-77
lines changed

experimental/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ All notable changes to experimental packages in this project will be documented
1010
### :rocket: (Enhancement)
1111

1212
* feat(api-logs): Add delegating no-op logger provider [#4861](https://github.com/open-telemetry/opentelemetry-js/pull/4861) @hectorhdzg
13+
* feat(instrumentation-http): Add support for client span semantic conventions 1.27 [#4940](https://github.com/open-telemetry/opentelemetry-js/pull/4940) @dyladan
1314

1415
### :bug: (Bug Fix)
1516

experimental/packages/opentelemetry-instrumentation-http/README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,44 @@ The following options are deprecated:
7676

7777
## Semantic Conventions
7878

79-
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)
79+
### Client Spans
80+
81+
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).
82+
83+
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).
84+
It is controlled using the environment variable `OTEL_SEMCONV_STABILITY_OPT_IN`, which is a comma separated list of values.
85+
The values `http` and `http/dup` control this instrumentation.
86+
See details for the behavior of each of these values below.
87+
If neither `http` or `http/dup` is included in `OTEL_SEMCONV_STABILITY_OPT_IN`, the old experimental semantic conventions will be used by default.
88+
89+
#### Stable Semantic Conventions 1.27
90+
91+
Enabled when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http` OR `http/dup`.
92+
This is the recommended configuration, and will soon become the default behavior.
93+
94+
Follow all requirements and recommendations of HTTP Client 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.
95+
96+
#### Legacy Behavior (default)
97+
98+
Enabled when `OTEL_SEMCONV_STABILITY_OPT_IN` contains `http/dup` or DOES NOT CONTAIN `http`.
99+
This is the current default behavior.
100+
101+
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).
102+
103+
#### Upgrading Semantic Conventions
104+
105+
When upgrading to the new semantic conventions, it is recommended to do so in the following order:
106+
107+
1. Upgrade `@opentelemetry/instrumentation-http` to the latest version
108+
2. Set `OTEL_SEMCONV_STABILITY_OPT_IN=http/dup` to emit both old and new semantic conventions
109+
3. Modify alerts, dashboards, metrics, and other processes to expect the new semantic conventions
110+
4. Set `OTEL_SEMCONV_STABILITY_OPT_IN=http` to emit only the new semantic conventions
111+
112+
This will cause both the old and new semantic conventions to be emitted during the transition period.
113+
114+
### Server Spans
115+
116+
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).
80117

81118
Attributes collected:
82119

experimental/packages/opentelemetry-instrumentation-http/src/http.ts

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,39 @@ import {
4747
HttpInstrumentationConfig,
4848
HttpRequestArgs,
4949
Https,
50+
SemconvStability,
5051
} from './types';
51-
import * as utils from './utils';
5252
import { VERSION } from './version';
5353
import {
5454
InstrumentationBase,
5555
InstrumentationNodeModuleDefinition,
5656
safeExecuteInTheMiddle,
5757
} from '@opentelemetry/instrumentation';
58-
import { RPCMetadata, RPCType, setRPCMetadata } from '@opentelemetry/core';
58+
import {
59+
RPCMetadata,
60+
RPCType,
61+
setRPCMetadata,
62+
getEnv,
63+
} from '@opentelemetry/core';
5964
import { errorMonitor } from 'events';
6065
import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
66+
import {
67+
extractHostnameAndPort,
68+
getIncomingRequestAttributes,
69+
getIncomingRequestAttributesOnResponse,
70+
getIncomingRequestMetricAttributes,
71+
getIncomingRequestMetricAttributesOnResponse,
72+
getOutgoingRequestAttributes,
73+
getOutgoingRequestAttributesOnResponse,
74+
getOutgoingRequestMetricAttributes,
75+
getOutgoingRequestMetricAttributesOnResponse,
76+
getRequestInfo,
77+
headerCapture,
78+
isIgnored,
79+
isValidOptionsType,
80+
parseResponseStatus,
81+
setSpanWithError,
82+
} from './utils';
6183

6284
/**
6385
* Http instrumentation instrumentation for Opentelemetry
@@ -69,9 +91,21 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
6991
private _httpServerDurationHistogram!: Histogram;
7092
private _httpClientDurationHistogram!: Histogram;
7193

94+
private _semconvStability = SemconvStability.OLD;
95+
7296
constructor(config: HttpInstrumentationConfig = {}) {
7397
super('@opentelemetry/instrumentation-http', VERSION, config);
7498
this._headerCapture = this._createHeaderCapture();
99+
100+
for (const entry in getEnv().OTEL_SEMCONV_STABILITY_OPT_IN) {
101+
if (entry.toLowerCase() === 'http/dup') {
102+
// http/dup takes highest precedence. If it is found, there is no need to read the rest of the list
103+
this._semconvStability = SemconvStability.DUPLICATE;
104+
break;
105+
} else if (entry.toLowerCase() === 'http') {
106+
this._semconvStability = SemconvStability.STABLE;
107+
}
108+
}
75109
}
76110

77111
protected override _updateMetricInstruments() {
@@ -320,12 +354,14 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
320354
if (request.listenerCount('response') <= 1) {
321355
response.resume();
322356
}
323-
const responseAttributes =
324-
utils.getOutgoingRequestAttributesOnResponse(response);
357+
const responseAttributes = getOutgoingRequestAttributesOnResponse(
358+
response,
359+
this._semconvStability
360+
);
325361
span.setAttributes(responseAttributes);
326362
metricAttributes = Object.assign(
327363
metricAttributes,
328-
utils.getOutgoingRequestMetricAttributesOnResponse(responseAttributes)
364+
getOutgoingRequestMetricAttributesOnResponse(responseAttributes)
329365
);
330366

331367
if (this.getConfig().responseHook) {
@@ -353,11 +389,9 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
353389
if (response.aborted && !response.complete) {
354390
status = { code: SpanStatusCode.ERROR };
355391
} else {
392+
// behaves same for new and old semconv
356393
status = {
357-
code: utils.parseResponseStatus(
358-
SpanKind.CLIENT,
359-
response.statusCode
360-
),
394+
code: parseResponseStatus(SpanKind.CLIENT, response.statusCode),
361395
};
362396
}
363397

@@ -395,7 +429,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
395429
return;
396430
}
397431
responseFinished = true;
398-
utils.setSpanWithError(span, error);
432+
setSpanWithError(span, error, this._semconvStability);
399433
span.setStatus({
400434
code: SpanStatusCode.ERROR,
401435
message: error.message,
@@ -423,7 +457,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
423457
return;
424458
}
425459
responseFinished = true;
426-
utils.setSpanWithError(span, error);
460+
setSpanWithError(span, error, this._semconvStability);
427461
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
428462
});
429463

@@ -458,7 +492,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
458492
);
459493

460494
if (
461-
utils.isIgnored(
495+
isIgnored(
462496
pathname,
463497
instrumentation.getConfig().ignoreIncomingPaths,
464498
(e: unknown) =>
@@ -487,7 +521,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
487521

488522
const headers = request.headers;
489523

490-
const spanAttributes = utils.getIncomingRequestAttributes(request, {
524+
const spanAttributes = getIncomingRequestAttributes(request, {
491525
component: component,
492526
serverName: instrumentation.getConfig().serverName,
493527
hookAttributes: instrumentation._callStartSpanHook(
@@ -503,7 +537,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
503537

504538
const startTime = hrTime();
505539
const metricAttributes =
506-
utils.getIncomingRequestMetricAttributes(spanAttributes);
540+
getIncomingRequestMetricAttributes(spanAttributes);
507541

508542
const ctx = propagation.extract(ROOT_CONTEXT, headers);
509543
const span = instrumentation._startHttpSpan(method, spanOptions, ctx);
@@ -558,7 +592,11 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
558592
() => original.apply(this, [event, ...args]),
559593
error => {
560594
if (error) {
561-
utils.setSpanWithError(span, error);
595+
setSpanWithError(
596+
span,
597+
error,
598+
instrumentation._semconvStability
599+
);
562600
instrumentation._closeHttpSpan(
563601
span,
564602
SpanKind.SERVER,
@@ -584,15 +622,15 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
584622
options: url.URL | http.RequestOptions | string,
585623
...args: unknown[]
586624
): http.ClientRequest {
587-
if (!utils.isValidOptionsType(options)) {
625+
if (!isValidOptionsType(options)) {
588626
return original.apply(this, [options, ...args]);
589627
}
590628
const extraOptions =
591629
typeof args[0] === 'object' &&
592630
(typeof options === 'string' || options instanceof url.URL)
593631
? (args.shift() as http.RequestOptions)
594632
: undefined;
595-
const { origin, pathname, method, optionsParsed } = utils.getRequestInfo(
633+
const { origin, pathname, method, optionsParsed } = getRequestInfo(
596634
options,
597635
extraOptions
598636
);
@@ -610,7 +648,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
610648
}
611649

612650
if (
613-
utils.isIgnored(
651+
isIgnored(
614652
origin + pathname,
615653
instrumentation.getConfig().ignoreOutgoingUrls,
616654
(e: unknown) =>
@@ -635,21 +673,25 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
635673
return original.apply(this, [optionsParsed, ...args]);
636674
}
637675

638-
const { hostname, port } = utils.extractHostnameAndPort(optionsParsed);
639-
640-
const attributes = utils.getOutgoingRequestAttributes(optionsParsed, {
641-
component,
642-
port,
643-
hostname,
644-
hookAttributes: instrumentation._callStartSpanHook(
645-
optionsParsed,
646-
instrumentation.getConfig().startOutgoingSpanHook
647-
),
648-
});
676+
const { hostname, port } = extractHostnameAndPort(optionsParsed);
677+
678+
const attributes = getOutgoingRequestAttributes(
679+
optionsParsed,
680+
{
681+
component,
682+
port,
683+
hostname,
684+
hookAttributes: instrumentation._callStartSpanHook(
685+
optionsParsed,
686+
instrumentation.getConfig().startOutgoingSpanHook
687+
),
688+
},
689+
instrumentation._semconvStability
690+
);
649691

650692
const startTime = hrTime();
651693
const metricAttributes: MetricAttributes =
652-
utils.getOutgoingRequestMetricAttributes(attributes);
694+
getOutgoingRequestMetricAttributes(attributes);
653695

654696
const spanOptions: SpanOptions = {
655697
kind: SpanKind.CLIENT,
@@ -683,7 +725,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
683725
() => original.apply(this, [optionsParsed, ...args]),
684726
error => {
685727
if (error) {
686-
utils.setSpanWithError(span, error);
728+
setSpanWithError(span, error, instrumentation._semconvStability);
687729
instrumentation._closeHttpSpan(
688730
span,
689731
SpanKind.CLIENT,
@@ -716,21 +758,21 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
716758
metricAttributes: MetricAttributes,
717759
startTime: HrTime
718760
) {
719-
const attributes = utils.getIncomingRequestAttributesOnResponse(
761+
const attributes = getIncomingRequestAttributesOnResponse(
720762
request,
721763
response
722764
);
723765
metricAttributes = Object.assign(
724766
metricAttributes,
725-
utils.getIncomingRequestMetricAttributesOnResponse(attributes)
767+
getIncomingRequestMetricAttributesOnResponse(attributes)
726768
);
727769

728770
this._headerCapture.server.captureResponseHeaders(span, header =>
729771
response.getHeader(header)
730772
);
731773

732774
span.setAttributes(attributes).setStatus({
733-
code: utils.parseResponseStatus(SpanKind.SERVER, response.statusCode),
775+
code: parseResponseStatus(SpanKind.SERVER, response.statusCode),
734776
});
735777

736778
const route = attributes[SEMATTRS_HTTP_ROUTE];
@@ -760,7 +802,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
760802
startTime: HrTime,
761803
error: Err
762804
) {
763-
utils.setSpanWithError(span, error);
805+
setSpanWithError(span, error, this._semconvStability);
764806
this._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
765807
}
766808

@@ -854,21 +896,21 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
854896

855897
return {
856898
client: {
857-
captureRequestHeaders: utils.headerCapture(
899+
captureRequestHeaders: headerCapture(
858900
'request',
859901
config.headersToSpanAttributes?.client?.requestHeaders ?? []
860902
),
861-
captureResponseHeaders: utils.headerCapture(
903+
captureResponseHeaders: headerCapture(
862904
'response',
863905
config.headersToSpanAttributes?.client?.responseHeaders ?? []
864906
),
865907
},
866908
server: {
867-
captureRequestHeaders: utils.headerCapture(
909+
captureRequestHeaders: headerCapture(
868910
'request',
869911
config.headersToSpanAttributes?.server?.requestHeaders ?? []
870912
),
871-
captureResponseHeaders: utils.headerCapture(
913+
captureResponseHeaders: headerCapture(
872914
'response',
873915
config.headersToSpanAttributes?.server?.responseHeaders ?? []
874916
),

experimental/packages/opentelemetry-instrumentation-http/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,18 @@ export interface Err extends Error {
132132
syscall?: string;
133133
stack?: string;
134134
}
135+
136+
/**
137+
* Tracks whether this instrumentation emits old experimental,
138+
* new stable, or both semantic conventions.
139+
*
140+
* Enum values chosen such that the enum may be used as a bitmask.
141+
*/
142+
export const enum SemconvStability {
143+
/** Emit only stable semantic conventions */
144+
STABLE = 0x1,
145+
/** Emit only old semantic convetions */
146+
OLD = 0x2,
147+
/** Emit both stable and old semantic convetions */
148+
DUPLICATE = 0x1 | 0x2,
149+
}

0 commit comments

Comments
 (0)