Skip to content

Commit

Permalink
Update HTTP client span semconv to 1.27 (open-telemetry#4940)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
4 people authored and Zirak committed Sep 14, 2024
1 parent 12147ca commit e555ed9
Show file tree
Hide file tree
Showing 9 changed files with 413 additions and 77 deletions.
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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 client span semantic conventions 1.27 [#4940](https://github.com/open-telemetry/opentelemetry-js/pull/4940) @dyladan

### :bug: (Bug Fix)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,44 @@ The following options are deprecated:

## 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)
### Client 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).
It is controlled using the environment variable `OTEL_SEMCONV_STABILITY_OPT_IN`, which is a comma separated list of values.
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

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 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)

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).

#### Upgrading Semantic Conventions

When upgrading to the new semantic conventions, it is recommended to do so in the following order:

1. Upgrade `@opentelemetry/instrumentation-http` to the latest version
2. Set `OTEL_SEMCONV_STABILITY_OPT_IN=http/dup` to emit both old and new semantic conventions
3. Modify alerts, dashboards, metrics, and other processes to expect the new semantic conventions
4. Set `OTEL_SEMCONV_STABILITY_OPT_IN=http` to emit only the new semantic conventions

This will cause both the old and new semantic conventions to be emitted during the transition period.

### Server Spans

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).

Attributes collected:

Expand Down
120 changes: 81 additions & 39 deletions experimental/packages/opentelemetry-instrumentation-http/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,39 @@ import {
HttpInstrumentationConfig,
HttpRequestArgs,
Https,
SemconvStability,
} from './types';
import * as utils from './utils';
import { VERSION } from './version';
import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';
import { RPCMetadata, RPCType, setRPCMetadata } from '@opentelemetry/core';
import {
RPCMetadata,
RPCType,
setRPCMetadata,
getEnv,
} from '@opentelemetry/core';
import { errorMonitor } from 'events';
import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import {
extractHostnameAndPort,
getIncomingRequestAttributes,
getIncomingRequestAttributesOnResponse,
getIncomingRequestMetricAttributes,
getIncomingRequestMetricAttributesOnResponse,
getOutgoingRequestAttributes,
getOutgoingRequestAttributesOnResponse,
getOutgoingRequestMetricAttributes,
getOutgoingRequestMetricAttributesOnResponse,
getRequestInfo,
headerCapture,
isIgnored,
isValidOptionsType,
parseResponseStatus,
setSpanWithError,
} from './utils';

/**
* Http instrumentation instrumentation for Opentelemetry
Expand All @@ -69,9 +91,21 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
private _httpServerDurationHistogram!: Histogram;
private _httpClientDurationHistogram!: Histogram;

private _semconvStability = SemconvStability.OLD;

constructor(config: HttpInstrumentationConfig = {}) {
super('@opentelemetry/instrumentation-http', VERSION, config);
this._headerCapture = this._createHeaderCapture();

for (const entry in getEnv().OTEL_SEMCONV_STABILITY_OPT_IN) {
if (entry.toLowerCase() === 'http/dup') {
// http/dup takes highest precedence. If it is found, there is no need to read the rest of the list
this._semconvStability = SemconvStability.DUPLICATE;
break;
} else if (entry.toLowerCase() === 'http') {
this._semconvStability = SemconvStability.STABLE;
}
}
}

protected override _updateMetricInstruments() {
Expand Down Expand Up @@ -320,12 +354,14 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
if (request.listenerCount('response') <= 1) {
response.resume();
}
const responseAttributes =
utils.getOutgoingRequestAttributesOnResponse(response);
const responseAttributes = getOutgoingRequestAttributesOnResponse(
response,
this._semconvStability
);
span.setAttributes(responseAttributes);
metricAttributes = Object.assign(
metricAttributes,
utils.getOutgoingRequestMetricAttributesOnResponse(responseAttributes)
getOutgoingRequestMetricAttributesOnResponse(responseAttributes)
);

if (this.getConfig().responseHook) {
Expand Down Expand Up @@ -353,11 +389,9 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
if (response.aborted && !response.complete) {
status = { code: SpanStatusCode.ERROR };
} else {
// behaves same for new and old semconv
status = {
code: utils.parseResponseStatus(
SpanKind.CLIENT,
response.statusCode
),
code: parseResponseStatus(SpanKind.CLIENT, response.statusCode),
};
}

Expand Down Expand Up @@ -395,7 +429,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
return;
}
responseFinished = true;
utils.setSpanWithError(span, error);
setSpanWithError(span, error, this._semconvStability);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
Expand Down Expand Up @@ -423,7 +457,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
return;
}
responseFinished = true;
utils.setSpanWithError(span, error);
setSpanWithError(span, error, this._semconvStability);
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
});

Expand Down Expand Up @@ -458,7 +492,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
);

if (
utils.isIgnored(
isIgnored(
pathname,
instrumentation.getConfig().ignoreIncomingPaths,
(e: unknown) =>
Expand Down Expand Up @@ -487,7 +521,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation

const headers = request.headers;

const spanAttributes = utils.getIncomingRequestAttributes(request, {
const spanAttributes = getIncomingRequestAttributes(request, {
component: component,
serverName: instrumentation.getConfig().serverName,
hookAttributes: instrumentation._callStartSpanHook(
Expand All @@ -503,7 +537,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation

const startTime = hrTime();
const metricAttributes =
utils.getIncomingRequestMetricAttributes(spanAttributes);
getIncomingRequestMetricAttributes(spanAttributes);

const ctx = propagation.extract(ROOT_CONTEXT, headers);
const span = instrumentation._startHttpSpan(method, spanOptions, ctx);
Expand Down Expand Up @@ -558,7 +592,11 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
() => original.apply(this, [event, ...args]),
error => {
if (error) {
utils.setSpanWithError(span, error);
setSpanWithError(
span,
error,
instrumentation._semconvStability
);
instrumentation._closeHttpSpan(
span,
SpanKind.SERVER,
Expand All @@ -584,15 +622,15 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
options: url.URL | http.RequestOptions | string,
...args: unknown[]
): http.ClientRequest {
if (!utils.isValidOptionsType(options)) {
if (!isValidOptionsType(options)) {
return original.apply(this, [options, ...args]);
}
const extraOptions =
typeof args[0] === 'object' &&
(typeof options === 'string' || options instanceof url.URL)
? (args.shift() as http.RequestOptions)
: undefined;
const { origin, pathname, method, optionsParsed } = utils.getRequestInfo(
const { origin, pathname, method, optionsParsed } = getRequestInfo(
options,
extraOptions
);
Expand All @@ -610,7 +648,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
}

if (
utils.isIgnored(
isIgnored(
origin + pathname,
instrumentation.getConfig().ignoreOutgoingUrls,
(e: unknown) =>
Expand All @@ -635,21 +673,25 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
return original.apply(this, [optionsParsed, ...args]);
}

const { hostname, port } = utils.extractHostnameAndPort(optionsParsed);

const attributes = utils.getOutgoingRequestAttributes(optionsParsed, {
component,
port,
hostname,
hookAttributes: instrumentation._callStartSpanHook(
optionsParsed,
instrumentation.getConfig().startOutgoingSpanHook
),
});
const { hostname, port } = extractHostnameAndPort(optionsParsed);

const attributes = getOutgoingRequestAttributes(
optionsParsed,
{
component,
port,
hostname,
hookAttributes: instrumentation._callStartSpanHook(
optionsParsed,
instrumentation.getConfig().startOutgoingSpanHook
),
},
instrumentation._semconvStability
);

const startTime = hrTime();
const metricAttributes: MetricAttributes =
utils.getOutgoingRequestMetricAttributes(attributes);
getOutgoingRequestMetricAttributes(attributes);

const spanOptions: SpanOptions = {
kind: SpanKind.CLIENT,
Expand Down Expand Up @@ -683,7 +725,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
() => original.apply(this, [optionsParsed, ...args]),
error => {
if (error) {
utils.setSpanWithError(span, error);
setSpanWithError(span, error, instrumentation._semconvStability);
instrumentation._closeHttpSpan(
span,
SpanKind.CLIENT,
Expand Down Expand Up @@ -716,21 +758,21 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
metricAttributes: MetricAttributes,
startTime: HrTime
) {
const attributes = utils.getIncomingRequestAttributesOnResponse(
const attributes = getIncomingRequestAttributesOnResponse(
request,
response
);
metricAttributes = Object.assign(
metricAttributes,
utils.getIncomingRequestMetricAttributesOnResponse(attributes)
getIncomingRequestMetricAttributesOnResponse(attributes)
);

this._headerCapture.server.captureResponseHeaders(span, header =>
response.getHeader(header)
);

span.setAttributes(attributes).setStatus({
code: utils.parseResponseStatus(SpanKind.SERVER, response.statusCode),
code: parseResponseStatus(SpanKind.SERVER, response.statusCode),
});

const route = attributes[SEMATTRS_HTTP_ROUTE];
Expand Down Expand Up @@ -760,7 +802,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
startTime: HrTime,
error: Err
) {
utils.setSpanWithError(span, error);
setSpanWithError(span, error, this._semconvStability);
this._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
}

Expand Down Expand Up @@ -854,21 +896,21 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation

return {
client: {
captureRequestHeaders: utils.headerCapture(
captureRequestHeaders: headerCapture(
'request',
config.headersToSpanAttributes?.client?.requestHeaders ?? []
),
captureResponseHeaders: utils.headerCapture(
captureResponseHeaders: headerCapture(
'response',
config.headersToSpanAttributes?.client?.responseHeaders ?? []
),
},
server: {
captureRequestHeaders: utils.headerCapture(
captureRequestHeaders: headerCapture(
'request',
config.headersToSpanAttributes?.server?.requestHeaders ?? []
),
captureResponseHeaders: utils.headerCapture(
captureResponseHeaders: headerCapture(
'response',
config.headersToSpanAttributes?.server?.responseHeaders ?? []
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,18 @@ export interface Err extends Error {
syscall?: string;
stack?: string;
}

/**
* Tracks whether this instrumentation emits old experimental,
* new stable, or both semantic conventions.
*
* Enum values chosen such that the enum may be used as a bitmask.
*/
export const enum SemconvStability {
/** Emit only stable semantic conventions */
STABLE = 0x1,
/** Emit only old semantic convetions */
OLD = 0x2,
/** Emit both stable and old semantic convetions */
DUPLICATE = 0x1 | 0x2,
}
Loading

0 comments on commit e555ed9

Please sign in to comment.