refactor(instrumentation-http): Add back support for http semconv (#5665)

Co-authored-by: Trent Mick <trentm@gmail.com>
This commit is contained in:
Jamie Danielson 2025-05-12 16:18:25 -04:00 committed by GitHub
parent 373edd9bc1
commit 697e1d31bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1529 additions and 155 deletions

View File

@ -8,8 +8,6 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
### :boom: Breaking Changes
* feat(instrumentation-http)!: Remove legacy http span attributes and metrics [#5552](https://github.com/open-telemetry/opentelemetry-js/pull/5552) @svetlanabrennan
### :rocket: Features
* feat(instrumentation-xml-http-request): support migration to stable HTTP semconv, v1.23.1 [#5662](https://github.com/open-telemetry/opentelemetry-js/pull/5662) @trentm
@ -31,6 +29,9 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2
### :house: Internal
* refactor(instrumentation-http): Remove legacy http span attributes and metrics [#5552](https://github.com/open-telemetry/opentelemetry-js/pull/5552) @svetlanabrennan
* refactor(instrumentation-http): Add back support for http semconv [#5665](https://github.com/open-telemetry/opentelemetry-js/pull/5665) @JamieDanielson
* Note: We initially removed support for legacy http attributes and metrics, but then added back for an additional 6 months to ensure all instrumentations could be updated and kept consistent. There should be no net new change in this instrumentation related to these semantic conventions. See [#5646](https://github.com/open-telemetry/opentelemetry-js/issues/5646) for details.
* refactor(sdk-node): update semconv usage to `ATTR_` exports [#5668](https://github.com/open-telemetry/opentelemetry-js/pull/5668) @trentm
* chore(sdk-node): Refactored using `get*FromEnv` utility function instead of `process.env` for NodeSDK's resource detector setup. [#5582](https://github.com/open-telemetry/opentelemetry-js/pull/5582) @beeme1mr
* chore(sdk-node): Refactored using `get*FromEnv` utility function instead of `process.env` for NodeSDK's logging setup. [#5563](https://github.com/open-telemetry/opentelemetry-js/issues/5563) @weyert

View File

@ -51,34 +51,83 @@ See [examples/http](https://github.com/open-telemetry/opentelemetry-js/tree/main
### Http instrumentation Options
Http instrumentation has few options available to choose from. You can set the following:
Http instrumentation has a few [configuration options](https://github.com/open-telemetry/opentelemetry-js/blob/e1ec4026edae53a2dea3a9a604d6d21bb5e8d99f/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L60-L93) available to choose from.
You can set the following:
| Options | Type | Description |
| ------- | ---- | ----------- |
| [`applyCustomAttributesOnSpan`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L91) | `HttpCustomAttributeFunction` | Function for adding custom attributes |
| [`requestHook`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#93) | `HttpRequestCustomAttributeFunction` | Function for adding custom attributes before request is handled |
| [`responseHook`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L95) | `HttpResponseCustomAttributeFunction` | Function for adding custom attributes before response is handled |
| [`startIncomingSpanHook`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L97) | `StartIncomingSpanCustomAttributeFunction` | Function for adding custom attributes before a span is started in incomingRequest |
| [`startOutgoingSpanHook`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L99) | `StartOutgoingSpanCustomAttributeFunction` | Function for adding custom attributes before a span is started in outgoingRequest |
| `ignoreIncomingRequestHook` | `IgnoreIncomingRequestFunction` | Http instrumentation will not trace all incoming requests that matched with custom function |
| `ignoreOutgoingRequestHook` | `IgnoreOutgoingRequestFunction` | Http instrumentation will not trace all outgoing requests that matched with custom function |
| `disableOutgoingRequestInstrumentation` | `boolean` | Set to true to avoid instrumenting outgoing requests at all. This can be helpful when another instrumentation handles outgoing requests. |
| `disableIncomingRequestInstrumentation` | `boolean` | Set to true to avoid instrumenting incoming requests at all. This can be helpful when another instrumentation handles incoming requests. |
| [`requireParentforOutgoingSpans`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L103) | Boolean | Require that is a parent span to create new span for outgoing requests. |
| [`requireParentforIncomingSpans`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L105) | Boolean | Require that is a parent span to create new span for incoming requests. |
| [`headersToSpanAttributes`](https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-instrumentation-http/src/types.ts#L107) | `object` | List of case insensitive HTTP headers to convert to span attributes. Client (outgoing requests, incoming responses) and server (incoming requests, outgoing responses) headers will be converted to span attributes in the form of `http.{request\|response}.header.header_name`, e.g. `http.response.header.content_length` |
| Options | Type | Description |
| --------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `applyCustomAttributesOnSpan` | `HttpCustomAttributeFunction` | Function for adding custom attributes |
| `requestHook` | `HttpRequestCustomAttributeFunction` | Function for adding custom attributes before request is handled |
| `responseHook` | `HttpResponseCustomAttributeFunction` | Function for adding custom attributes before response is handled |
| `startIncomingSpanHook` | `StartIncomingSpanCustomAttributeFunction` | Function for adding custom attributes before a span is started in incomingRequest |
| `startOutgoingSpanHook` | `StartOutgoingSpanCustomAttributeFunction` | Function for adding custom attributes before a span is started in outgoingRequest |
| `ignoreIncomingRequestHook` | `IgnoreIncomingRequestFunction` | Http instrumentation will not trace all incoming requests that matched with custom function |
| `ignoreOutgoingRequestHook` | `IgnoreOutgoingRequestFunction` | Http instrumentation will not trace all outgoing requests that matched with custom function |
| `disableOutgoingRequestInstrumentation` | `boolean` | Set to true to avoid instrumenting outgoing requests at all. This can be helpful when another instrumentation handles outgoing requests. |
| `disableIncomingRequestInstrumentation` | `boolean` | Set to true to avoid instrumenting incoming requests at all. This can be helpful when another instrumentation handles incoming requests. |
| `serverName` | `string` | The primary server name of the matched virtual host. |
| `requireParentforOutgoingSpans` | Boolean | Require that is a parent span to create new span for outgoing requests. |
| `requireParentforIncomingSpans` | Boolean | Require that is a parent span to create new span for incoming requests. |
| `headersToSpanAttributes` | `object` | List of case insensitive HTTP headers to convert to span attributes. Client (outgoing requests, incoming responses) and server (incoming requests, outgoing responses) headers will be converted to span attributes in the form of `http.{request\|response}.header.header_name`, e.g. `http.response.header.content_length` |
## Semantic Conventions
This package emits Semantic Convention [Version 1.27.0](https://github.com/open-telemetry/semantic-conventions/blob/v1.27.0/docs/http/http-spans.md).
Prior to version `0.54.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).
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.
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-http` versions 0.54.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).
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
| v1.7.0 semconv | v1.23.0 semconv | Short Description |
| ------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `http.client_ip` | `client.address` | The IP address of the original client behind all proxies, if known |
| `http.flavor` | `network.protocol.version` | Kind of HTTP protocol used |
| `http.host` | `server.address` | The value of the HTTP host header |
| `http.method` | `http.request.method` | HTTP request method |
| `http.request_content_length` | (opt-in, `headersToSpanAttributes`) | The size of the request payload body in bytes. For newer semconv, use the `headersToSpanAttributes:` option to capture this as `http.request.header.content_length`. |
| `http.request_content_length_uncompressed` | (not included) | The size of the uncompressed request payload body after transport decoding. (In semconv v1.23.0 this is defined by `http.request.body.size`, which is experimental and opt-in.) |
| `http.response_content_length` | (opt-in, `headersToSpanAttributes`) | The size of the response payload body in bytes. For newer semconv, use the `headersToSpanAttributes:` option to capture this as `http.response.header.content_length`. |
| `http.response_content_length_uncompressed` | (not included) | The size of the uncompressed response payload body after transport decoding. (In semconv v1.23.0 this is defined by `http.response.body.size`, which is experimental and opt-in.) |
| `http.route` | no change | The matched route (path template). |
| `http.scheme` | `url.scheme` | The URI scheme identifying the used protocol |
| `http.server_name` | `server.address` | The primary server name of the matched virtual host |
| `http.status_code` | `http.response.status_code` | HTTP response status code |
| `http.target` | `url.path` and `url.query` | The URI path and query component |
| `http.url` | `url.full` | Full HTTP request URL in the form `scheme://host[:port]/path?query[#fragment]` |
| `http.user_agent` | `user_agent.original` | Value of the HTTP User-Agent header sent by the client |
| `net.host.ip` | `network.local.address` | Like net.peer.ip but for the host IP. Useful in case of a multi-IP host |
| `net.host.name` | `server.address` | Local hostname or similar |
| `net.host.port` | `server.port` | Like net.peer.port but for the host port |
| `net.peer.ip.` | `network.peer.address` | Remote address of the peer (dotted decimal for IPv4 or RFC5952 for IPv6) |
| `net.peer.name` | `server.address` | Server domain name if available without reverse DNS lookup |
| `net.peer.port` | `server.port` | Server port number |
| `net.transport` | `network.transport` | Transport protocol used |
Metrics Exported:
- [`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
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.
## Useful links
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>

View File

@ -0,0 +1,24 @@
/*
* 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.
*/
/**
* https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md
*/
export enum AttributeNames {
HTTP_ERROR_NAME = 'http.error_name',
HTTP_ERROR_MESSAGE = 'http.error_message',
HTTP_STATUS_TEXT = 'http.status_text',
}

View File

@ -47,14 +47,16 @@ import { VERSION } from './version';
import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
SemconvStability,
httpSemconvStabilityFromStr,
safeExecuteInTheMiddle,
} from '@opentelemetry/instrumentation';
import { errorMonitor } from 'events';
import {
ATTR_HTTP_REQUEST_METHOD,
ATTR_HTTP_RESPONSE_STATUS_CODE,
ATTR_HTTP_ROUTE,
ATTR_NETWORK_PROTOCOL_VERSION,
ATTR_HTTP_ROUTE,
ATTR_SERVER_ADDRESS,
ATTR_SERVER_PORT,
ATTR_URL_SCHEME,
@ -65,10 +67,14 @@ import {
extractHostnameAndPort,
getIncomingRequestAttributes,
getIncomingRequestAttributesOnResponse,
getIncomingRequestMetricAttributes,
getIncomingRequestMetricAttributesOnResponse,
getIncomingStableRequestMetricAttributesOnResponse,
getOutgoingRequestAttributes,
getOutgoingRequestAttributesOnResponse,
getOutgoingRequestMetricAttributes,
getOutgoingRequestMetricAttributesOnResponse,
getOutgoingStableRequestMetricAttributesOnResponse,
getRequestInfo,
headerCapture,
isValidOptionsType,
@ -84,15 +90,38 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
/** keep track on spans not ended */
private readonly _spanNotEnded: WeakSet<Span> = new WeakSet<Span>();
private _headerCapture;
declare private _oldHttpServerDurationHistogram: Histogram;
declare private _stableHttpServerDurationHistogram: Histogram;
declare private _oldHttpClientDurationHistogram: Histogram;
declare private _stableHttpClientDurationHistogram: Histogram;
private _semconvStability: SemconvStability = SemconvStability.OLD;
constructor(config: HttpInstrumentationConfig = {}) {
super('@opentelemetry/instrumentation-http', VERSION, config);
this._headerCapture = this._createHeaderCapture();
this._semconvStability = httpSemconvStabilityFromStr(
process.env.OTEL_SEMCONV_STABILITY_OPT_IN
);
}
protected override _updateMetricInstruments() {
this._oldHttpServerDurationHistogram = this.meter.createHistogram(
'http.server.duration',
{
description: 'Measures the duration of inbound HTTP requests.',
unit: 'ms',
valueType: ValueType.DOUBLE,
}
);
this._oldHttpClientDurationHistogram = this.meter.createHistogram(
'http.client.duration',
{
description: 'Measures the duration of outbound HTTP requests.',
unit: 'ms',
valueType: ValueType.DOUBLE,
}
);
this._stableHttpServerDurationHistogram = this.meter.createHistogram(
METRIC_HTTP_SERVER_REQUEST_DURATION,
{
@ -125,22 +154,40 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
private _recordServerDuration(
durationMs: number,
oldAttributes: Attributes,
stableAttributes: Attributes
) {
this._stableHttpServerDurationHistogram.record(
durationMs / 1000,
stableAttributes
);
if (this._semconvStability & SemconvStability.OLD) {
// old histogram is counted in MS
this._oldHttpServerDurationHistogram.record(durationMs, oldAttributes);
}
if (this._semconvStability & SemconvStability.STABLE) {
// stable histogram is counted in S
this._stableHttpServerDurationHistogram.record(
durationMs / 1000,
stableAttributes
);
}
}
private _recordClientDuration(
durationMs: number,
oldAttributes: Attributes,
stableAttributes: Attributes
) {
this._stableHttpClientDurationHistogram.record(
durationMs / 1000,
stableAttributes
);
if (this._semconvStability & SemconvStability.OLD) {
// old histogram is counted in MS
this._oldHttpClientDurationHistogram.record(durationMs, oldAttributes);
}
if (this._semconvStability & SemconvStability.STABLE) {
// stable histogram is counted in S
this._stableHttpClientDurationHistogram.record(
durationMs / 1000,
stableAttributes
);
}
}
override setConfig(config: HttpInstrumentationConfig = {}): void {
@ -355,12 +402,14 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
* @param request The original request object.
* @param span representing the current operation
* @param startTime representing the start time of the request to calculate duration in Metric
* @param oldMetricAttributes metric attributes for old semantic conventions
* @param stableMetricAttributes metric attributes for new semantic conventions
*/
private _traceClientRequest(
request: http.ClientRequest,
span: Span,
startTime: HrTime,
oldMetricAttributes: Attributes,
stableMetricAttributes: Attributes
): http.ClientRequest {
if (this.getConfig().requestHook) {
@ -384,12 +433,18 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
if (request.listenerCount('response') <= 1) {
response.resume();
}
const responseAttributes =
getOutgoingRequestAttributesOnResponse(response);
const responseAttributes = getOutgoingRequestAttributesOnResponse(
response,
this._semconvStability
);
span.setAttributes(responseAttributes);
oldMetricAttributes = Object.assign(
oldMetricAttributes,
getOutgoingRequestMetricAttributesOnResponse(responseAttributes)
);
stableMetricAttributes = Object.assign(
stableMetricAttributes,
getOutgoingRequestMetricAttributesOnResponse(responseAttributes)
getOutgoingStableRequestMetricAttributesOnResponse(responseAttributes)
);
if (this.getConfig().responseHook) {
@ -442,6 +497,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
span,
SpanKind.CLIENT,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
};
@ -453,7 +509,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
return;
}
responseFinished = true;
setSpanWithError(span, error);
setSpanWithError(span, error, this._semconvStability);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
@ -462,6 +518,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
span,
SpanKind.CLIENT,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
});
@ -477,6 +534,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
span,
SpanKind.CLIENT,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
});
@ -486,12 +544,13 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
return;
}
responseFinished = true;
setSpanWithError(span, error);
setSpanWithError(span, error, this._semconvStability);
this._closeHttpSpan(
span,
SpanKind.CLIENT,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
});
@ -551,10 +610,12 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
request,
{
component: component,
serverName: instrumentation.getConfig().serverName,
hookAttributes: instrumentation._callStartSpanHook(
request,
instrumentation.getConfig().startIncomingSpanHook
),
semconvStability: instrumentation._semconvStability,
enableSyntheticSourceDetection:
instrumentation.getConfig().enableSyntheticSourceDetection || false,
},
@ -567,6 +628,8 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
};
const startTime = hrTime();
const oldMetricAttributes =
getIncomingRequestMetricAttributes(spanAttributes);
// request method and url.scheme are both required span attributes
const stableMetricAttributes: Attributes = {
@ -615,6 +678,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
request,
response,
span,
oldMetricAttributes,
stableMetricAttributes,
startTime
);
@ -623,6 +687,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
hasError = true;
instrumentation._onServerResponseError(
span,
oldMetricAttributes,
stableMetricAttributes,
startTime,
err
@ -633,11 +698,16 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
() => original.apply(this, [event, ...args]),
error => {
if (error) {
setSpanWithError(span, error);
setSpanWithError(
span,
error,
instrumentation._semconvStability
);
instrumentation._closeHttpSpan(
span,
SpanKind.SERVER,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
throw error;
@ -705,10 +775,13 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
instrumentation.getConfig().startOutgoingSpanHook
),
},
instrumentation._semconvStability,
instrumentation.getConfig().enableSyntheticSourceDetection || false
);
const startTime = hrTime();
const oldMetricAttributes: Attributes =
getOutgoingRequestMetricAttributes(attributes);
// request method, server address, and server port are both required span attributes
const stableMetricAttributes: Attributes = {
@ -770,12 +843,13 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
},
error => {
if (error) {
setSpanWithError(span, error);
setSpanWithError(span, error, instrumentation._semconvStability);
instrumentation._closeHttpSpan(
span,
SpanKind.CLIENT,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
throw error;
@ -791,6 +865,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
request,
span,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
});
@ -801,12 +876,18 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
request: http.IncomingMessage,
response: http.ServerResponse,
span: Span,
oldMetricAttributes: Attributes,
stableMetricAttributes: Attributes,
startTime: HrTime
) {
const attributes = getIncomingRequestAttributesOnResponse(
request,
response
response,
this._semconvStability
);
oldMetricAttributes = Object.assign(
oldMetricAttributes,
getIncomingRequestMetricAttributesOnResponse(attributes)
);
stableMetricAttributes = Object.assign(
stableMetricAttributes,
@ -843,22 +924,25 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
span,
SpanKind.SERVER,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
}
private _onServerResponseError(
span: Span,
oldMetricAttributes: Attributes,
stableMetricAttributes: Attributes,
startTime: HrTime,
error: Err
) {
setSpanWithError(span, error);
setSpanWithError(span, error, this._semconvStability);
// TODO get error attributes for metrics
this._closeHttpSpan(
span,
SpanKind.SERVER,
startTime,
oldMetricAttributes,
stableMetricAttributes
);
}
@ -895,6 +979,7 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
span: Span,
spanKind: SpanKind,
startTime: HrTime,
oldMetricAttributes: Attributes,
stableMetricAttributes: Attributes
) {
if (!this._spanNotEnded.has(span)) {
@ -907,9 +992,17 @@ export class HttpInstrumentation extends InstrumentationBase<HttpInstrumentation
// Record metrics
const duration = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime()));
if (spanKind === SpanKind.SERVER) {
this._recordServerDuration(duration, stableMetricAttributes);
this._recordServerDuration(
duration,
oldMetricAttributes,
stableMetricAttributes
);
} else if (spanKind === SpanKind.CLIENT) {
this._recordClientDuration(duration, stableMetricAttributes);
this._recordClientDuration(
duration,
oldMetricAttributes,
stableMetricAttributes
);
}
}

View File

@ -39,3 +39,253 @@ export const USER_AGENT_SYNTHETIC_TYPE_VALUE_BOT = 'bot' as const;
* Enum value "test" for attribute {@link ATTR_USER_AGENT_SYNTHETIC_TYPE}.
*/
export const USER_AGENT_SYNTHETIC_TYPE_VALUE_TEST = 'test' as const;
/**
* Deprecated, use `client.address` instead.
*
* @example "83.164.160.102"
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `client.address`.
*/
export const ATTR_HTTP_CLIENT_IP = 'http.client_ip' as const;
/**
* Deprecated, use `network.protocol.name` instead.
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `network.protocol.name`.
*/
export const ATTR_HTTP_FLAVOR = 'http.flavor' as const;
/**
* Deprecated, use one of `server.address`, `client.address` or `http.request.header.host` instead, depending on the usage.
*
* @example www.example.org
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by one of `server.address`, `client.address` or `http.request.header.host`, depending on the usage.
*/
export const ATTR_HTTP_HOST = 'http.host' as const;
/**
* 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;
/**
* Deprecated, use `http.request.header.<key>` instead.
*
* @example 3495
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `http.request.header.<key>`.
*/
export const ATTR_HTTP_REQUEST_CONTENT_LENGTH =
'http.request_content_length' as const;
/**
* Deprecated, use `http.request.body.size` instead.
*
* @example 5493
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `http.request.body.size`.
*/
export const ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED =
'http.request_content_length_uncompressed' as const;
/**
* Deprecated, use `http.response.header.<key>` instead.
*
* @example 3495
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `http.response.header.<key>`.
*/
export const ATTR_HTTP_RESPONSE_CONTENT_LENGTH =
'http.response_content_length' as const;
/**
* Deprecated, use `http.response.body.size` instead.
*
* @example 5493
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replace by `http.response.body.size`.
*/
export const ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED =
'http.response_content_length_uncompressed' as const;
/**
* Deprecated, use `url.scheme` instead.
*
* @example http
* @example https
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `url.scheme` instead.
*/
export const ATTR_HTTP_SCHEME = 'http.scheme' as const;
/**
* Deprecated, use `server.address` instead.
*
* @example example.com
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `server.address`.
*/
export const ATTR_HTTP_SERVER_NAME = 'http.server_name' as const;
/**
* Deprecated, use `http.response.status_code` instead.
*
* @example 200
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `http.response.status_code`.
*/
export const ATTR_HTTP_STATUS_CODE = 'http.status_code' as const;
/**
* Deprecated, use `url.path` and `url.query` instead.
*
* @example /search?q=OpenTelemetry#SemConv
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Split to `url.path` and `url.query.
*/
export const ATTR_HTTP_TARGET = 'http.target' as const;
/**
* Deprecated, use `url.full` instead.
*
* @example https://www.foo.bar/search?q=OpenTelemetry#SemConv
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `url.full`.
*/
export const ATTR_HTTP_URL = 'http.url' as const;
/**
* Deprecated, use `user_agent.original` instead.
*
* @example CERN-LineMode/2.15 libwww/2.17b3
* @example Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `user_agent.original`.
*/
export const ATTR_HTTP_USER_AGENT = 'http.user_agent' as const;
/**
* Deprecated, use `network.local.address`.
*
* @example "192.168.0.1"
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `network.local.address`.
*/
export const ATTR_NET_HOST_IP = 'net.host.ip' as const;
/**
* Deprecated, use `server.address`.
*
* @example example.com
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `server.address`.
*/
export const ATTR_NET_HOST_NAME = 'net.host.name' as const;
/**
* Deprecated, use `server.port`.
*
* @example 8080
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `server.port`.
*/
export const ATTR_NET_HOST_PORT = 'net.host.port' as const;
/**
* Deprecated, use `network.peer.address`.
*
* @example "127.0.0.1"
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `network.peer.address`.
*/
export const ATTR_NET_PEER_IP = 'net.peer.ip' as const;
/**
* Deprecated, use `server.address` on client spans and `client.address` on server spans.
*
* @example example.com
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `server.address` on client spans and `client.address` on server spans.
*/
export const ATTR_NET_PEER_NAME = 'net.peer.name' as const;
/**
* Deprecated, use `server.port` on client spans and `client.port` on server spans.
*
* @example 8080
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `server.port` on client spans and `client.port` on server spans.
*/
export const ATTR_NET_PEER_PORT = 'net.peer.port' as const;
/**
* Deprecated, use `network.transport`.
*
* @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`.
*
* @deprecated Replaced by `network.transport`.
*/
export const ATTR_NET_TRANSPORT = 'net.transport' as const;
/**
* Enum value "ip_tcp" for attribute {@link ATTR_NET_TRANSPORT}.
*/
export const NET_TRANSPORT_VALUE_IP_TCP = 'ip_tcp' as const;
/**
* Enum value "ip_udp" for attribute {@link ATTR_NET_TRANSPORT}.
*/
export const NET_TRANSPORT_VALUE_IP_UDP = 'ip_udp' as const;
/**
* Enum value "1.1" for attribute {@link ATTR_HTTP_FLAVOR}.
*/
export const HTTP_FLAVOR_VALUE_HTTP_1_1 = '1.1' as const;

View File

@ -76,6 +76,8 @@ export interface HttpInstrumentationConfig extends InstrumentationConfig {
startIncomingSpanHook?: StartIncomingSpanCustomAttributeFunction;
/** Function for adding custom attributes before a span is started in outgoingRequest */
startOutgoingSpanHook?: StartOutgoingSpanCustomAttributeFunction;
/** The primary server name of the matched virtual host. */
serverName?: string;
/** Require parent to create span for outgoing requests */
requireParentforOutgoingSpans?: boolean;
/** Require parent to create span for incoming requests */

View File

@ -40,6 +40,34 @@ import {
ATTR_URL_SCHEME,
ATTR_USER_AGENT_ORIGINAL,
} from '@opentelemetry/semantic-conventions';
import {
ATTR_HTTP_CLIENT_IP,
ATTR_HTTP_FLAVOR,
ATTR_HTTP_HOST,
ATTR_HTTP_METHOD,
ATTR_HTTP_REQUEST_CONTENT_LENGTH,
ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_SCHEME,
ATTR_HTTP_SERVER_NAME,
ATTR_HTTP_STATUS_CODE,
ATTR_HTTP_TARGET,
ATTR_HTTP_URL,
ATTR_HTTP_USER_AGENT,
ATTR_NET_HOST_IP,
ATTR_NET_HOST_NAME,
ATTR_NET_HOST_PORT,
ATTR_NET_PEER_IP,
ATTR_NET_PEER_NAME,
ATTR_NET_PEER_PORT,
ATTR_NET_TRANSPORT,
NET_TRANSPORT_VALUE_IP_TCP,
NET_TRANSPORT_VALUE_IP_UDP,
ATTR_USER_AGENT_SYNTHETIC_TYPE,
USER_AGENT_SYNTHETIC_TYPE_VALUE_BOT,
USER_AGENT_SYNTHETIC_TYPE_VALUE_TEST,
} from './semconv';
import {
IncomingHttpHeaders,
IncomingMessage,
@ -49,14 +77,11 @@ import {
ServerResponse,
} from 'http';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import { SemconvStability } from '@opentelemetry/instrumentation';
import * as url from 'url';
import { AttributeNames } from './enums/AttributeNames';
import { Err, IgnoreMatcher, ParsedRequestOptions } from './internal-types';
import { SYNTHETIC_BOT_NAMES, SYNTHETIC_TEST_NAMES } from './internal-types';
import {
ATTR_USER_AGENT_SYNTHETIC_TYPE,
USER_AGENT_SYNTHETIC_TYPE_VALUE_BOT,
USER_AGENT_SYNTHETIC_TYPE_VALUE_TEST,
} from './semconv';
import forwardedParse = require('forwarded-parse');
/**
@ -130,14 +155,78 @@ export const satisfiesPattern = (
* Sets the span with the error passed in params
* @param {Span} span the span that need to be set
* @param {Error} error error that will be set to span
* @param {SemconvStability} semconvStability determines which semconv version to use
*/
export const setSpanWithError = (span: Span, error: Err): void => {
export const setSpanWithError = (
span: Span,
error: Err,
semconvStability: SemconvStability
): void => {
const message = error.message;
span.setAttribute(ATTR_ERROR_TYPE, error.name);
if (semconvStability & SemconvStability.OLD) {
span.setAttribute(AttributeNames.HTTP_ERROR_NAME, error.name);
span.setAttribute(AttributeNames.HTTP_ERROR_MESSAGE, message);
}
if (semconvStability & SemconvStability.STABLE) {
span.setAttribute(ATTR_ERROR_TYPE, error.name);
}
span.setStatus({ code: SpanStatusCode.ERROR, message });
span.recordException(error);
};
/**
* Adds attributes for request content-length and content-encoding HTTP headers
* @param { IncomingMessage } Request object whose headers will be analyzed
* @param { Attributes } Attributes object to be modified
*/
export const setRequestContentLengthAttribute = (
request: IncomingMessage,
attributes: Attributes
): void => {
const length = getContentLength(request.headers);
if (length === null) return;
if (isCompressed(request.headers)) {
attributes[ATTR_HTTP_REQUEST_CONTENT_LENGTH] = length;
} else {
attributes[ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED] = length;
}
};
/**
* Adds attributes for response content-length and content-encoding HTTP headers
* @param { IncomingMessage } Response object whose headers will be analyzed
* @param { Attributes } Attributes object to be modified
*
* @deprecated this is for an older version of semconv. It is retained for compatibility using OTEL_SEMCONV_STABILITY_OPT_IN
*/
export const setResponseContentLengthAttribute = (
response: IncomingMessage,
attributes: Attributes
): void => {
const length = getContentLength(response.headers);
if (length === null) return;
if (isCompressed(response.headers)) {
attributes[ATTR_HTTP_RESPONSE_CONTENT_LENGTH] = length;
} else {
attributes[ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED] = length;
}
};
function getContentLength(
headers: OutgoingHttpHeaders | IncomingHttpHeaders
): number | null {
const contentLengthHeader = headers['content-length'];
if (contentLengthHeader === undefined) return null;
const contentLength = parseInt(contentLengthHeader as string, 10);
if (isNaN(contentLength)) return null;
return contentLength;
}
export const isCompressed = (
headers: OutgoingHttpHeaders | IncomingHttpHeaders
@ -343,6 +432,7 @@ export const extractHostnameAndPort = (
* Returns outgoing request attributes scoped to the options passed to the request
* @param {ParsedRequestOptions} requestOptions the same options used to make the request
* @param {{ component: string, hostname: string, hookAttributes?: Attributes }} options used to pass data needed to create attributes
* @param {SemconvStability} semconvStability determines which semconv version to use
*/
export const getOutgoingRequestAttributes = (
requestOptions: ParsedRequestOptions,
@ -352,6 +442,7 @@ export const getOutgoingRequestAttributes = (
port: string | number;
hookAttributes?: Attributes;
},
semconvStability: SemconvStability,
enableSyntheticSourceDetection: boolean
): Attributes => {
const hostname = options.hostname;
@ -365,6 +456,15 @@ export const getOutgoingRequestAttributes = (
headers,
`${options.component}:`
);
const oldAttributes: Attributes = {
[ATTR_HTTP_URL]: urlFull,
[ATTR_HTTP_METHOD]: method,
[ATTR_HTTP_TARGET]: requestOptions.path || '/',
[ATTR_NET_PEER_NAME]: hostname,
[ATTR_HTTP_HOST]: headers.host ?? `${hostname}:${port}`,
};
const newAttributes: Attributes = {
// Required attributes
[ATTR_HTTP_REQUEST_METHOD]: normalizedMethod,
@ -387,7 +487,50 @@ export const getOutgoingRequestAttributes = (
if (enableSyntheticSourceDetection && userAgent) {
newAttributes[ATTR_USER_AGENT_SYNTHETIC_TYPE] = getSyntheticType(userAgent);
}
return Object.assign(newAttributes, options.hookAttributes);
if (userAgent !== undefined) {
oldAttributes[ATTR_HTTP_USER_AGENT] = userAgent;
}
switch (semconvStability) {
case SemconvStability.STABLE:
return Object.assign(newAttributes, options.hookAttributes);
case SemconvStability.OLD:
return Object.assign(oldAttributes, options.hookAttributes);
}
return Object.assign(oldAttributes, newAttributes, options.hookAttributes);
};
/**
* Returns outgoing request Metric attributes scoped to the request data
* @param {Attributes} spanAttributes the span attributes
*/
export const getOutgoingRequestMetricAttributes = (
spanAttributes: Attributes
): Attributes => {
const metricAttributes: Attributes = {};
metricAttributes[ATTR_HTTP_METHOD] = spanAttributes[ATTR_HTTP_METHOD];
metricAttributes[ATTR_NET_PEER_NAME] = spanAttributes[ATTR_NET_PEER_NAME];
//TODO: http.url attribute, it should substitute any parameters to avoid high cardinality.
return metricAttributes;
};
/**
* Returns attributes related to the kind of HTTP protocol used
* @param {string} [kind] Kind of HTTP protocol used: "1.0", "1.1", "2", "SPDY" or "QUIC".
*/
export const setAttributesFromHttpKind = (
kind: string | undefined,
attributes: Attributes
): void => {
if (kind) {
attributes[ATTR_HTTP_FLAVOR] = kind;
if (kind.toUpperCase() !== 'QUIC') {
attributes[ATTR_NET_TRANSPORT] = NET_TRANSPORT_VALUE_IP_TCP;
} else {
attributes[ATTR_NET_TRANSPORT] = NET_TRANSPORT_VALUE_IP_UDP;
}
}
};
/**
@ -414,11 +557,14 @@ const getSyntheticType = (
/**
* Returns outgoing request attributes scoped to the response data
* @param {IncomingMessage} response the response object
* @param {SemconvStability} semconvStability determines which semconv version to use
*/
export const getOutgoingRequestAttributesOnResponse = (
response: IncomingMessage
response: IncomingMessage,
semconvStability: SemconvStability
): Attributes => {
const { statusCode, socket } = response;
const { statusCode, statusMessage, httpVersion, socket } = response;
const oldAttributes: Attributes = {};
const stableAttributes: Attributes = {};
if (statusCode != null) {
@ -427,14 +573,33 @@ export const getOutgoingRequestAttributesOnResponse = (
if (socket) {
const { remoteAddress, remotePort } = socket;
oldAttributes[ATTR_NET_PEER_IP] = remoteAddress;
oldAttributes[ATTR_NET_PEER_PORT] = remotePort;
// Recommended
stableAttributes[ATTR_NETWORK_PEER_ADDRESS] = remoteAddress;
stableAttributes[ATTR_NETWORK_PEER_PORT] = remotePort;
stableAttributes[ATTR_NETWORK_PROTOCOL_VERSION] = response.httpVersion;
}
setResponseContentLengthAttribute(response, oldAttributes);
return stableAttributes;
if (statusCode) {
oldAttributes[ATTR_HTTP_STATUS_CODE] = statusCode;
oldAttributes[AttributeNames.HTTP_STATUS_TEXT] = (
statusMessage || ''
).toUpperCase();
}
setAttributesFromHttpKind(httpVersion, oldAttributes);
switch (semconvStability) {
case SemconvStability.STABLE:
return stableAttributes;
case SemconvStability.OLD:
return oldAttributes;
}
return Object.assign(oldAttributes, stableAttributes);
};
/**
@ -444,19 +609,30 @@ export const getOutgoingRequestAttributesOnResponse = (
export const getOutgoingRequestMetricAttributesOnResponse = (
spanAttributes: Attributes
): Attributes => {
const stableAttributes: Attributes = {};
const metricAttributes: Attributes = {};
metricAttributes[ATTR_NET_PEER_PORT] = spanAttributes[ATTR_NET_PEER_PORT];
metricAttributes[ATTR_HTTP_STATUS_CODE] =
spanAttributes[ATTR_HTTP_STATUS_CODE];
metricAttributes[ATTR_HTTP_FLAVOR] = spanAttributes[ATTR_HTTP_FLAVOR];
return metricAttributes;
};
export const getOutgoingStableRequestMetricAttributesOnResponse = (
spanAttributes: Attributes
): Attributes => {
const metricAttributes: Attributes = {};
if (spanAttributes[ATTR_NETWORK_PROTOCOL_VERSION]) {
stableAttributes[ATTR_NETWORK_PROTOCOL_VERSION] =
metricAttributes[ATTR_NETWORK_PROTOCOL_VERSION] =
spanAttributes[ATTR_NETWORK_PROTOCOL_VERSION];
}
if (spanAttributes[ATTR_HTTP_RESPONSE_STATUS_CODE]) {
stableAttributes[ATTR_HTTP_RESPONSE_STATUS_CODE] =
metricAttributes[ATTR_HTTP_RESPONSE_STATUS_CODE] =
spanAttributes[ATTR_HTTP_RESPONSE_STATUS_CODE];
}
return stableAttributes;
return metricAttributes;
};
function parseHostHeader(
@ -648,23 +824,32 @@ function getInfoFromIncomingMessage(
/**
* Returns incoming request attributes scoped to the request data
* @param {IncomingMessage} request the request object
* @param {{ component: string, hookAttributes?: Attributes }} options used to pass data needed to create attributes
* @param {{ component: string, serverName?: string, hookAttributes?: Attributes }} options used to pass data needed to create attributes
* @param {SemconvStability} semconvStability determines which semconv version to use
*/
export const getIncomingRequestAttributes = (
request: IncomingMessage,
options: {
component: 'http' | 'https';
serverName?: string;
hookAttributes?: Attributes;
semconvStability: SemconvStability;
enableSyntheticSourceDetection: boolean;
},
logger: DiagLogger
): Attributes => {
const headers = request.headers;
const userAgent = headers['user-agent'];
const ips = headers['x-forwarded-for'];
const httpVersion = request.httpVersion;
const host = headers.host;
const hostname = host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') || 'localhost';
const method = request.method;
const normalizedMethod = normalizeMethod(method);
const serverAddress = getServerAddress(request, options.component);
const serverName = options.serverName;
const remoteClientAddress = getRemoteClientAddress(request);
const newAttributes: Attributes = {
@ -708,7 +893,58 @@ export const getIncomingRequestAttributes = (
if (options.enableSyntheticSourceDetection && userAgent) {
newAttributes[ATTR_USER_AGENT_SYNTHETIC_TYPE] = getSyntheticType(userAgent);
}
return Object.assign(newAttributes, options.hookAttributes);
const oldAttributes: Attributes = {
[ATTR_HTTP_URL]: parsedUrl.toString(),
[ATTR_HTTP_HOST]: host,
[ATTR_NET_HOST_NAME]: hostname,
[ATTR_HTTP_METHOD]: method,
[ATTR_HTTP_SCHEME]: options.component,
};
if (typeof ips === 'string') {
oldAttributes[ATTR_HTTP_CLIENT_IP] = ips.split(',')[0];
}
if (typeof serverName === 'string') {
oldAttributes[ATTR_HTTP_SERVER_NAME] = serverName;
}
if (parsedUrl?.pathname) {
oldAttributes[ATTR_HTTP_TARGET] =
parsedUrl?.pathname + parsedUrl?.search || '/';
}
if (userAgent !== undefined) {
oldAttributes[ATTR_HTTP_USER_AGENT] = userAgent;
}
setRequestContentLengthAttribute(request, oldAttributes);
setAttributesFromHttpKind(httpVersion, oldAttributes);
switch (options.semconvStability) {
case SemconvStability.STABLE:
return Object.assign(newAttributes, options.hookAttributes);
case SemconvStability.OLD:
return Object.assign(oldAttributes, options.hookAttributes);
}
return Object.assign(oldAttributes, newAttributes, options.hookAttributes);
};
/**
* Returns incoming request Metric attributes scoped to the request data
* @param {Attributes} spanAttributes the span attributes
* @param {{ component: string }} options used to pass data needed to create attributes
*/
export const getIncomingRequestMetricAttributes = (
spanAttributes: Attributes
): Attributes => {
const metricAttributes: Attributes = {};
metricAttributes[ATTR_HTTP_SCHEME] = spanAttributes[ATTR_HTTP_SCHEME];
metricAttributes[ATTR_HTTP_METHOD] = spanAttributes[ATTR_HTTP_METHOD];
metricAttributes[ATTR_NET_HOST_NAME] = spanAttributes[ATTR_NET_HOST_NAME];
metricAttributes[ATTR_HTTP_FLAVOR] = spanAttributes[ATTR_HTTP_FLAVOR];
//TODO: http.target attribute, it should substitute any parameters to avoid high cardinality.
return metricAttributes;
};
/**
@ -717,27 +953,68 @@ export const getIncomingRequestAttributes = (
*/
export const getIncomingRequestAttributesOnResponse = (
request: IncomingMessage,
response: ServerResponse
response: ServerResponse,
semconvStability: SemconvStability
): Attributes => {
const { statusCode } = response;
// take socket from the request,
// since it may be detached from the response object in keep-alive mode
const { socket } = request;
const { statusCode, statusMessage } = response;
const newAttributes: Attributes = {
[ATTR_HTTP_RESPONSE_STATUS_CODE]: statusCode,
};
const rpcMetadata = getRPCMetadata(context.active());
const oldAttributes: Attributes = {};
if (socket) {
const { localAddress, localPort, remoteAddress, remotePort } = socket;
oldAttributes[ATTR_NET_HOST_IP] = localAddress;
oldAttributes[ATTR_NET_HOST_PORT] = localPort;
oldAttributes[ATTR_NET_PEER_IP] = remoteAddress;
oldAttributes[ATTR_NET_PEER_PORT] = remotePort;
}
oldAttributes[ATTR_HTTP_STATUS_CODE] = statusCode;
oldAttributes[AttributeNames.HTTP_STATUS_TEXT] = (
statusMessage || ''
).toUpperCase();
if (rpcMetadata?.type === RPCType.HTTP && rpcMetadata.route !== undefined) {
oldAttributes[ATTR_HTTP_ROUTE] = rpcMetadata.route;
newAttributes[ATTR_HTTP_ROUTE] = rpcMetadata.route;
}
return newAttributes;
switch (semconvStability) {
case SemconvStability.STABLE:
return newAttributes;
case SemconvStability.OLD:
return oldAttributes;
}
return Object.assign(oldAttributes, newAttributes);
};
/**
* Returns incoming request Metric attributes scoped to the request data
* @param {Attributes} spanAttributes the span attributes
*/
export const getIncomingRequestMetricAttributesOnResponse = (
spanAttributes: Attributes
): Attributes => {
const metricAttributes: Attributes = {};
metricAttributes[ATTR_HTTP_STATUS_CODE] =
spanAttributes[ATTR_HTTP_STATUS_CODE];
metricAttributes[ATTR_NET_HOST_PORT] = spanAttributes[ATTR_NET_HOST_PORT];
if (spanAttributes[ATTR_HTTP_ROUTE] !== undefined) {
metricAttributes[ATTR_HTTP_ROUTE] = spanAttributes[ATTR_HTTP_ROUTE];
}
return metricAttributes;
};
/**
* Returns incoming stable request Metric attributes scoped to the request data
* @param {Attributes} spanAttributes the span attributes
*/
export const getIncomingStableRequestMetricAttributesOnResponse = (
spanAttributes: Attributes
): Attributes => {

View File

@ -36,7 +36,6 @@ import {
ATTR_HTTP_ROUTE,
ATTR_NETWORK_PEER_ADDRESS,
ATTR_NETWORK_PEER_PORT,
ATTR_NETWORK_PROTOCOL_NAME,
ATTR_NETWORK_PROTOCOL_VERSION,
ATTR_SERVER_ADDRESS,
ATTR_SERVER_PORT,
@ -45,6 +44,25 @@ import {
ATTR_URL_SCHEME,
HTTP_REQUEST_METHOD_VALUE_GET,
} from '@opentelemetry/semantic-conventions';
import {
ATTR_HTTP_CLIENT_IP,
ATTR_HTTP_FLAVOR,
ATTR_HTTP_HOST,
ATTR_HTTP_METHOD,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_SCHEME,
ATTR_HTTP_STATUS_CODE,
ATTR_HTTP_TARGET,
ATTR_HTTP_URL,
ATTR_NET_HOST_IP,
ATTR_NET_HOST_NAME,
ATTR_NET_HOST_PORT,
ATTR_NET_PEER_IP,
ATTR_NET_PEER_NAME,
ATTR_NET_PEER_PORT,
ATTR_NET_TRANSPORT,
NET_TRANSPORT_VALUE_IP_TCP,
} from '../../src/semconv';
import * as assert from 'assert';
import * as nock from 'nock';
import * as path from 'path';
@ -61,7 +79,7 @@ import type {
ServerResponse,
RequestOptions,
} from 'http';
import { isWrapped } from '@opentelemetry/instrumentation';
import { isWrapped, SemconvStability } from '@opentelemetry/instrumentation';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
const instrumentation = new HttpInstrumentation();
@ -69,6 +87,7 @@ instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
import { AttributeNames } from '../../src/enums/AttributeNames';
import { getRemoteClientAddress } from '../../src/utils';
const applyCustomAttributesOnSpanErrorMessage =
@ -79,6 +98,7 @@ const serverPort = 22346;
const protocol = 'http';
const hostname = 'localhost';
const pathname = '/test';
const serverName = 'my.server.name';
const memoryExporter = new InMemorySpanExporter();
const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(memoryExporter)],
@ -214,14 +234,33 @@ describe('HttpInstrumentation', () => {
assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
assert.strictEqual(
incomingSpan.attributes[ATTR_SERVER_PORT],
incomingSpan.attributes[ATTR_NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[ATTR_SERVER_PORT],
outgoingSpan.attributes[ATTR_NET_PEER_PORT],
serverPort
);
});
it('should remove auth from the `http.url` attribute (client side and server side)', async () => {
await httpRequest.get(
`${protocol}://user:pass@${hostname}:${serverPort}${pathname}`
);
const spans = memoryExporter.getFinishedSpans();
const [incomingSpan, outgoingSpan] = spans;
assert.strictEqual(spans.length, 2);
assert.strictEqual(incomingSpan.kind, SpanKind.SERVER);
assert.strictEqual(outgoingSpan.kind, SpanKind.CLIENT);
assert.strictEqual(
incomingSpan.attributes[ATTR_HTTP_URL],
`${protocol}://${hostname}:${serverPort}${pathname}`
);
assert.strictEqual(
outgoingSpan.attributes[ATTR_HTTP_URL],
`${protocol}://${hostname}:${serverPort}${pathname}`
);
});
});
describe('partially disable instrumentation', () => {
@ -299,6 +338,7 @@ describe('HttpInstrumentation', () => {
responseHook: responseHookFunction,
startIncomingSpanHook: startIncomingSpanHookFunction,
startOutgoingSpanHook: startOutgoingSpanHookFunction,
serverName,
});
instrumentation.enable();
server = http.createServer((request, response) => {
@ -374,28 +414,30 @@ describe('HttpInstrumentation', () => {
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'http',
serverName,
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(
incomingSpan.attributes[ATTR_CLIENT_ADDRESS],
incomingSpan.attributes[ATTR_HTTP_CLIENT_IP],
'<client>'
);
assert.strictEqual(
incomingSpan.attributes[ATTR_SERVER_PORT],
incomingSpan.attributes[ATTR_NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[ATTR_SERVER_PORT],
outgoingSpan.attributes[ATTR_NET_PEER_PORT],
serverPort
);
[
{ span: incomingSpan, kind: SpanKind.SERVER },
{ span: outgoingSpan, kind: SpanKind.CLIENT },
].forEach(({ span, kind }) => {
assert.ok(
!span.attributes[ATTR_NETWORK_PROTOCOL_NAME],
'should not be added for HTTP kind'
assert.strictEqual(span.attributes[ATTR_HTTP_FLAVOR], '1.1');
assert.strictEqual(
span.attributes[ATTR_NET_TRANSPORT],
NET_TRANSPORT_VALUE_IP_TCP
);
assertSpan(span, kind, validations);
});
@ -826,10 +868,7 @@ describe('HttpInstrumentation', () => {
const [span] = spans;
assert.strictEqual(spans.length, 1);
assert.ok(Object.keys(span.attributes).length > 6);
assert.strictEqual(
span.attributes[ATTR_HTTP_RESPONSE_STATUS_CODE],
404
);
assert.strictEqual(span.attributes[ATTR_HTTP_STATUS_CODE], 404);
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
done();
});
@ -1025,13 +1064,14 @@ describe('HttpInstrumentation', () => {
});
});
describe('with semconv stability', () => {
describe('with semconv stability set to http', () => {
beforeEach(() => {
memoryExporter.reset();
});
before(async () => {
instrumentation.setConfig({});
instrumentation['_semconvStability'] = SemconvStability.STABLE;
instrumentation.enable();
server = http.createServer((request, response) => {
if (request.url?.includes('/premature-close')) {
@ -1163,6 +1203,164 @@ describe('HttpInstrumentation', () => {
});
});
describe('with semconv stability set to http/dup', () => {
beforeEach(() => {
memoryExporter.reset();
instrumentation.setConfig({});
});
before(async () => {
instrumentation['_semconvStability'] = SemconvStability.DUPLICATE;
instrumentation.enable();
server = http.createServer((request, response) => {
if (request.url?.includes('/setroute')) {
const rpcData = getRPCMetadata(context.active());
assert.ok(rpcData != null);
assert.strictEqual(rpcData.type, RPCType.HTTP);
assert.strictEqual(rpcData.route, undefined);
rpcData.route = 'TheRoute';
}
response.setHeader('Content-Type', 'application/json');
response.end(
JSON.stringify({ address: getRemoteClientAddress(request) })
);
});
await new Promise<void>(resolve => server.listen(serverPort, resolve));
});
after(() => {
server.close();
instrumentation.disable();
});
it('should create client spans with semconv 1.27 and old 1.7', async () => {
const response = await httpRequest.get(
`${protocol}://${hostname}:${serverPort}${pathname}`
);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 2);
const outgoingSpan = spans[1];
// should have only required and recommended attributes for semconv 1.27
assert.deepStrictEqual(outgoingSpan.attributes, {
// 1.27 attributes
[ATTR_HTTP_REQUEST_METHOD]: HTTP_REQUEST_METHOD_VALUE_GET,
[ATTR_SERVER_ADDRESS]: hostname,
[ATTR_SERVER_PORT]: serverPort,
[ATTR_URL_FULL]: `http://${hostname}:${serverPort}${pathname}`,
[ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
[ATTR_NETWORK_PEER_ADDRESS]: response.address,
[ATTR_NETWORK_PEER_PORT]: serverPort,
[ATTR_NETWORK_PROTOCOL_VERSION]: '1.1',
// 1.7 attributes
[ATTR_HTTP_FLAVOR]: '1.1',
[ATTR_HTTP_HOST]: `${hostname}:${serverPort}`,
[ATTR_HTTP_METHOD]: 'GET',
[ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED]:
response.data.length,
[ATTR_HTTP_STATUS_CODE]: 200,
[ATTR_HTTP_TARGET]: '/test',
[ATTR_HTTP_URL]: `http://${hostname}:${serverPort}${pathname}`,
[ATTR_NET_PEER_IP]: response.address,
[ATTR_NET_PEER_NAME]: hostname,
[ATTR_NET_PEER_PORT]: serverPort,
[ATTR_NET_TRANSPORT]: 'ip_tcp',
// unspecified old names
[AttributeNames.HTTP_STATUS_TEXT]: 'OK',
});
});
it('should create server spans with semconv 1.27 and old 1.7', async () => {
const response = await httpRequest.get(
`${protocol}://${hostname}:${serverPort}${pathname}`
);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 2);
const incomingSpan = spans[0];
const body = JSON.parse(response.data);
// should have only required and recommended attributes for semconv 1.27
assert.deepStrictEqual(incomingSpan.attributes, {
// 1.27 attributes
[ATTR_CLIENT_ADDRESS]: body.address,
[ATTR_HTTP_REQUEST_METHOD]: HTTP_REQUEST_METHOD_VALUE_GET,
[ATTR_SERVER_ADDRESS]: hostname,
[ATTR_SERVER_PORT]: serverPort,
[ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
[ATTR_NETWORK_PEER_ADDRESS]: body.address,
[ATTR_NETWORK_PEER_PORT]: response.clientRemotePort,
[ATTR_NETWORK_PROTOCOL_VERSION]: '1.1',
[ATTR_URL_PATH]: pathname,
[ATTR_URL_SCHEME]: protocol,
// 1.7 attributes
[ATTR_HTTP_FLAVOR]: '1.1',
[ATTR_HTTP_HOST]: `${hostname}:${serverPort}`,
[ATTR_HTTP_METHOD]: 'GET',
[ATTR_HTTP_SCHEME]: protocol,
[ATTR_HTTP_STATUS_CODE]: 200,
[ATTR_HTTP_TARGET]: '/test',
[ATTR_HTTP_URL]: `http://${hostname}:${serverPort}${pathname}`,
[ATTR_NET_TRANSPORT]: 'ip_tcp',
[ATTR_NET_HOST_IP]: body.address,
[ATTR_NET_HOST_NAME]: hostname,
[ATTR_NET_HOST_PORT]: serverPort,
[ATTR_NET_PEER_IP]: body.address,
[ATTR_NET_PEER_PORT]: response.clientRemotePort,
// unspecified old names
[AttributeNames.HTTP_STATUS_TEXT]: 'OK',
});
});
it('should create server spans with semconv 1.27 and old 1.7 including http.route if RPC metadata is available', async () => {
const response = await httpRequest.get(
`${protocol}://${hostname}:${serverPort}${pathname}/setroute`
);
const spans = memoryExporter.getFinishedSpans();
assert.strictEqual(spans.length, 2);
const incomingSpan = spans[0];
const body = JSON.parse(response.data);
// should have only required and recommended attributes for semconv 1.27
assert.deepStrictEqual(incomingSpan.attributes, {
// 1.27 attributes
[ATTR_CLIENT_ADDRESS]: body.address,
[ATTR_HTTP_REQUEST_METHOD]: HTTP_REQUEST_METHOD_VALUE_GET,
[ATTR_SERVER_ADDRESS]: hostname,
[ATTR_SERVER_PORT]: serverPort,
[ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
[ATTR_NETWORK_PEER_ADDRESS]: body.address,
[ATTR_NETWORK_PEER_PORT]: response.clientRemotePort,
[ATTR_NETWORK_PROTOCOL_VERSION]: '1.1',
[ATTR_URL_PATH]: `${pathname}/setroute`,
[ATTR_URL_SCHEME]: protocol,
[ATTR_HTTP_ROUTE]: 'TheRoute',
// 1.7 attributes
[ATTR_HTTP_FLAVOR]: '1.1',
[ATTR_HTTP_HOST]: `${hostname}:${serverPort}`,
[ATTR_HTTP_METHOD]: 'GET',
[ATTR_HTTP_SCHEME]: protocol,
[ATTR_HTTP_STATUS_CODE]: 200,
[ATTR_HTTP_TARGET]: `${pathname}/setroute`,
[ATTR_HTTP_URL]: `http://${hostname}:${serverPort}${pathname}/setroute`,
[ATTR_NET_TRANSPORT]: 'ip_tcp',
[ATTR_NET_HOST_IP]: body.address,
[ATTR_NET_HOST_NAME]: hostname,
[ATTR_NET_HOST_PORT]: serverPort,
[ATTR_NET_PEER_IP]: body.address,
[ATTR_NET_PEER_PORT]: response.clientRemotePort,
// unspecified old names
[AttributeNames.HTTP_STATUS_TEXT]: 'OK',
});
});
});
describe('with require parent span', () => {
beforeEach(done => {
memoryExporter.reset();

View File

@ -29,11 +29,22 @@ import {
ATTR_SERVER_PORT,
ATTR_URL_SCHEME,
} from '@opentelemetry/semantic-conventions';
import {
ATTR_HTTP_FLAVOR,
ATTR_HTTP_METHOD,
ATTR_HTTP_SCHEME,
ATTR_HTTP_STATUS_CODE,
ATTR_NET_HOST_NAME,
ATTR_NET_HOST_PORT,
ATTR_NET_PEER_NAME,
ATTR_NET_PEER_PORT,
} from '../../src/semconv';
import * as assert from 'assert';
import { HttpInstrumentation } from '../../src/http';
import { httpRequest } from '../utils/httpRequest';
import { TestMetricReader } from '../utils/TestMetricReader';
import { context, ContextManager } from '@opentelemetry/api';
import { SemconvStability } from '@opentelemetry/instrumentation';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
@ -86,8 +97,97 @@ describe('metrics', () => {
server.close();
instrumentation.disable();
});
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[ATTR_HTTP_SCHEME],
'http'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_HTTP_METHOD],
'GET'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_HTTP_FLAVOR],
'1.1'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_NET_HOST_NAME],
'localhost'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_HTTP_STATUS_CODE],
200
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_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[ATTR_HTTP_METHOD],
'GET'
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[ATTR_NET_PEER_NAME],
'localhost'
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[ATTR_NET_PEER_PORT],
22346
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[ATTR_HTTP_STATUS_CODE],
200
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[ATTR_HTTP_FLAVOR],
'1.1'
);
});
});
describe('with semconv stability set to stable', () => {
before(() => {
instrumentation['_semconvStability'] = SemconvStability.STABLE;
});
describe('using semconv stability attributes', () => {
it('should add server/client duration metrics', async () => {
const requestCount = 3;
for (let i = 0; i < requestCount; i++) {
@ -149,4 +249,144 @@ describe('metrics', () => {
});
});
});
describe('with 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[ATTR_HTTP_SCHEME],
'http'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_HTTP_METHOD],
'GET'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_HTTP_FLAVOR],
'1.1'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_NET_HOST_NAME],
'localhost'
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_HTTP_STATUS_CODE],
200
);
assert.strictEqual(
metrics[0].dataPoints[0].attributes[ATTR_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[ATTR_HTTP_METHOD],
'GET'
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[ATTR_NET_PEER_NAME],
'localhost'
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[ATTR_NET_PEER_PORT],
22346
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[ATTR_HTTP_STATUS_CODE],
200
);
assert.strictEqual(
metrics[1].dataPoints[0].attributes[ATTR_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_HTTP_RESPONSE_STATUS_CODE]: 200,
[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,
[ATTR_NETWORK_PROTOCOL_VERSION]: '1.1',
[ATTR_HTTP_RESPONSE_STATUS_CODE]: 200,
});
});
});
});

View File

@ -30,11 +30,14 @@ import {
SimpleSpanProcessor,
} from '@opentelemetry/sdk-trace-base';
import {
ATTR_CLIENT_ADDRESS,
ATTR_HTTP_RESPONSE_STATUS_CODE,
ATTR_SERVER_PORT,
ATTR_NETWORK_PROTOCOL_NAME,
} from '@opentelemetry/semantic-conventions';
ATTR_HTTP_CLIENT_IP,
ATTR_HTTP_FLAVOR,
ATTR_HTTP_STATUS_CODE,
ATTR_NET_HOST_PORT,
ATTR_NET_PEER_PORT,
ATTR_NET_TRANSPORT,
NET_TRANSPORT_VALUE_IP_TCP,
} from '../../src/semconv';
import * as assert from 'assert';
import * as fs from 'fs';
import * as nock from 'nock';
@ -59,6 +62,7 @@ let server: https.Server;
const serverPort = 32345;
const protocol = 'https';
const hostname = 'localhost';
const serverName = 'my.server.name';
const pathname = '/test';
const memoryExporter = new InMemorySpanExporter();
const provider = new BasicTracerProvider({
@ -162,11 +166,11 @@ describe('HttpsInstrumentation', () => {
assertSpan(incomingSpan, SpanKind.SERVER, validations);
assertSpan(outgoingSpan, SpanKind.CLIENT, validations);
assert.strictEqual(
incomingSpan.attributes[ATTR_SERVER_PORT],
incomingSpan.attributes[ATTR_NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[ATTR_SERVER_PORT],
outgoingSpan.attributes[ATTR_NET_PEER_PORT],
serverPort
);
});
@ -194,6 +198,7 @@ describe('HttpsInstrumentation', () => {
return false;
},
applyCustomAttributesOnSpan: customAttributeFunction,
serverName,
});
instrumentation.enable();
server = https.createServer(
@ -241,19 +246,20 @@ describe('HttpsInstrumentation', () => {
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
serverName,
};
assert.strictEqual(spans.length, 2);
assert.strictEqual(
incomingSpan.attributes[ATTR_CLIENT_ADDRESS],
incomingSpan.attributes[ATTR_HTTP_CLIENT_IP],
'<client>'
);
assert.strictEqual(
incomingSpan.attributes[ATTR_SERVER_PORT],
incomingSpan.attributes[ATTR_NET_HOST_PORT],
serverPort
);
assert.strictEqual(
outgoingSpan.attributes[ATTR_SERVER_PORT],
outgoingSpan.attributes[ATTR_NET_PEER_PORT],
serverPort
);
@ -261,9 +267,10 @@ describe('HttpsInstrumentation', () => {
{ span: incomingSpan, kind: SpanKind.SERVER },
{ span: outgoingSpan, kind: SpanKind.CLIENT },
].forEach(({ span, kind }) => {
assert.ok(
!span.attributes[ATTR_NETWORK_PROTOCOL_NAME],
'should not be added for HTTP kind'
assert.strictEqual(span.attributes[ATTR_HTTP_FLAVOR], '1.1');
assert.strictEqual(
span.attributes[ATTR_NET_TRANSPORT],
NET_TRANSPORT_VALUE_IP_TCP
);
assertSpan(span, kind, validations);
});
@ -606,7 +613,7 @@ describe('HttpsInstrumentation', () => {
const [span] = spans;
assert.strictEqual(spans.length, 1);
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
assert.ok(Object.keys(span.attributes).length >= 5);
assert.ok(Object.keys(span.attributes).length >= 6);
});
it('should have 1 ended span when request is aborted after receiving response', async () => {
@ -659,10 +666,7 @@ describe('HttpsInstrumentation', () => {
const [span] = spans;
assert.strictEqual(spans.length, 1);
assert.ok(Object.keys(span.attributes).length > 6);
assert.strictEqual(
span.attributes[ATTR_HTTP_RESPONSE_STATUS_CODE],
404
);
assert.strictEqual(span.attributes[ATTR_HTTP_STATUS_CODE], 404);
assert.strictEqual(span.status.code, SpanStatusCode.ERROR);
done();
});

View File

@ -22,7 +22,6 @@ import {
diag,
} from '@opentelemetry/api';
import {
ATTR_ERROR_TYPE,
ATTR_HTTP_ROUTE,
ATTR_USER_AGENT_ORIGINAL,
} from '@opentelemetry/semantic-conventions';
@ -32,6 +31,11 @@ import { Socket } from 'net';
import * as sinon from 'sinon';
import * as url from 'url';
import {
ATTR_HTTP_REQUEST_CONTENT_LENGTH,
ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_TARGET,
ATTR_USER_AGENT_SYNTHETIC_TYPE,
USER_AGENT_SYNTHETIC_TYPE_VALUE_BOT,
} from '../../src/semconv';
@ -39,7 +43,9 @@ import { IgnoreMatcher, ParsedRequestOptions } from '../../src/internal-types';
import * as utils from '../../src/utils';
import { RPCType, setRPCMetadata } from '@opentelemetry/core';
import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks';
import { SemconvStability } from '@opentelemetry/instrumentation';
import { extractHostnameAndPort } from '../../src/utils';
import { AttributeNames } from '../../src/enums/AttributeNames';
describe('Utility', () => {
describe('parseResponseStatus()', () => {
@ -180,15 +186,19 @@ describe('Utility', () => {
recordException: () => undefined,
} as unknown as Span;
const mock = sinon.mock(span);
mock.expects('setAttribute').calledWithExactly(ATTR_ERROR_TYPE, 'Error');
mock
.expects('setAttribute')
.calledWithExactly(AttributeNames.HTTP_ERROR_NAME, 'error');
mock
.expects('setAttribute')
.calledWithExactly(AttributeNames.HTTP_ERROR_MESSAGE, errorMessage);
mock.expects('setStatus').calledWithExactly({
code: SpanStatusCode.ERROR,
message: errorMessage,
});
mock.expects('recordException').calledWithExactly(error);
utils.setSpanWithError(span, error);
utils.setSpanWithError(span, error, SemconvStability.OLD);
mock.verify();
});
});
@ -225,7 +235,8 @@ describe('Utility', () => {
() => {
const attributes = utils.getIncomingRequestAttributesOnResponse(
request,
{} as ServerResponse
{} as ServerResponse,
SemconvStability.OLD
);
assert.deepStrictEqual(attributes[ATTR_HTTP_ROUTE], '/user/:id');
context.disable();
@ -238,22 +249,24 @@ describe('Utility', () => {
const request = {
socket: {},
} as IncomingMessage;
const attributes = utils.getIncomingRequestAttributesOnResponse(request, {
socket: {},
} as ServerResponse & { socket: Socket });
const attributes = utils.getIncomingRequestAttributesOnResponse(
request,
{
socket: {},
} as ServerResponse & { socket: Socket },
SemconvStability.OLD
);
assert.deepEqual(attributes[ATTR_HTTP_ROUTE], undefined);
});
});
describe('getIncomingStableRequestMetricAttributesOnResponse()', () => {
describe('getIncomingRequestMetricAttributesOnResponse()', () => {
it('should correctly add http_route if span has it', () => {
const spanAttributes: Attributes = {
[ATTR_HTTP_ROUTE]: '/user/:id',
};
const metricAttributes =
utils.getIncomingStableRequestMetricAttributesOnResponse(
spanAttributes
);
utils.getIncomingRequestMetricAttributesOnResponse(spanAttributes);
assert.deepStrictEqual(metricAttributes[ATTR_HTTP_ROUTE], '/user/:id');
});
@ -261,13 +274,153 @@ describe('Utility', () => {
it('should skip http_route if span does not have it', () => {
const spanAttributes: Attributes = {};
const metricAttributes =
utils.getIncomingStableRequestMetricAttributesOnResponse(
spanAttributes
);
utils.getIncomingRequestMetricAttributesOnResponse(spanAttributes);
assert.deepEqual(metricAttributes[ATTR_HTTP_ROUTE], undefined);
});
});
// Verify the key in the given attributes is set to the given value,
// and that no other HTTP Content Length attributes are set.
function verifyValueInAttributes(
attributes: Attributes,
key: string | undefined,
value: number
) {
const SemanticAttributess = [
ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH,
ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_REQUEST_CONTENT_LENGTH,
];
for (const attr of SemanticAttributess) {
if (attr === key) {
assert.strictEqual(attributes[attr], value);
} else {
assert.strictEqual(attributes[attr], undefined);
}
}
}
describe('setRequestContentLengthAttributes()', () => {
it('should set request content-length uncompressed attribute with no content-encoding header', () => {
const attributes: Attributes = {};
const request = {} as IncomingMessage;
request.headers = {
'content-length': '1200',
};
utils.setRequestContentLengthAttribute(request, attributes);
verifyValueInAttributes(
attributes,
ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});
it('should set request content-length uncompressed attribute with "identity" content-encoding header', () => {
const attributes: Attributes = {};
const request = {} as IncomingMessage;
request.headers = {
'content-length': '1200',
'content-encoding': 'identity',
};
utils.setRequestContentLengthAttribute(request, attributes);
verifyValueInAttributes(
attributes,
ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});
it('should set request content-length compressed attribute with "gzip" content-encoding header', () => {
const attributes: Attributes = {};
const request = {} as IncomingMessage;
request.headers = {
'content-length': '1200',
'content-encoding': 'gzip',
};
utils.setRequestContentLengthAttribute(request, attributes);
verifyValueInAttributes(
attributes,
ATTR_HTTP_REQUEST_CONTENT_LENGTH,
1200
);
});
});
describe('setResponseContentLengthAttributes()', () => {
it('should set response content-length uncompressed attribute with no content-encoding header', () => {
const attributes: Attributes = {};
const response = {} as IncomingMessage;
response.headers = {
'content-length': '1200',
};
utils.setResponseContentLengthAttribute(response, attributes);
verifyValueInAttributes(
attributes,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});
it('should set response content-length uncompressed attribute with "identity" content-encoding header', () => {
const attributes: Attributes = {};
const response = {} as IncomingMessage;
response.headers = {
'content-length': '1200',
'content-encoding': 'identity',
};
utils.setResponseContentLengthAttribute(response, attributes);
verifyValueInAttributes(
attributes,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
1200
);
});
it('should set response content-length compressed attribute with "gzip" content-encoding header', () => {
const attributes: Attributes = {};
const response = {} as IncomingMessage;
response.headers = {
'content-length': '1200',
'content-encoding': 'gzip',
};
utils.setResponseContentLengthAttribute(response, attributes);
verifyValueInAttributes(
attributes,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH,
1200
);
});
it('should set no attributes with no content-length header', () => {
const attributes: Attributes = {};
const message = {} as IncomingMessage;
message.headers = {
'content-encoding': 'gzip',
};
utils.setResponseContentLengthAttribute(message, attributes);
verifyValueInAttributes(attributes, undefined, 1200);
});
});
describe('getIncomingRequestAttributes()', () => {
it('should not set http.route in http span attributes', () => {
const request = {
@ -283,6 +436,7 @@ describe('Utility', () => {
request,
{
component: 'http',
semconvStability: SemconvStability.OLD,
enableSyntheticSourceDetection: false,
},
diag
@ -303,10 +457,12 @@ describe('Utility', () => {
request,
{
component: 'http',
semconvStability: SemconvStability.OLD,
enableSyntheticSourceDetection: false,
},
diag
);
assert.strictEqual(attributes[ATTR_HTTP_TARGET], '/user/?q=val');
assert.strictEqual(attributes[ATTR_USER_AGENT_SYNTHETIC_TYPE], undefined);
});
@ -323,6 +479,7 @@ describe('Utility', () => {
request,
{
component: 'http',
semconvStability: SemconvStability.STABLE,
enableSyntheticSourceDetection: true,
},
diag

View File

@ -15,7 +15,13 @@
*/
import { SpanKind, Span, context, propagation } from '@opentelemetry/api';
import { ATTR_NETWORK_PROTOCOL_NAME } from '@opentelemetry/semantic-conventions';
import {
ATTR_HTTP_FLAVOR,
ATTR_HTTP_HOST,
ATTR_NET_TRANSPORT,
HTTP_FLAVOR_VALUE_HTTP_1_1,
NET_TRANSPORT_VALUE_IP_TCP,
} from '../../src/semconv';
import * as assert from 'assert';
import * as url from 'url';
import { HttpInstrumentation } from '../../src/http';
@ -224,9 +230,13 @@ describe('HttpInstrumentation Integration tests', () => {
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(result.reqHeaders['x-foo'], 'foo');
assert.ok(
!span.attributes[ATTR_NETWORK_PROTOCOL_NAME],
'should not be added for HTTP kind'
assert.strictEqual(
span.attributes[ATTR_HTTP_FLAVOR],
HTTP_FLAVOR_VALUE_HTTP_1_1
);
assert.strictEqual(
span.attributes[ATTR_NET_TRANSPORT],
NET_TRANSPORT_VALUE_IP_TCP
);
assertSpan(span, SpanKind.CLIENT, validations);
});
@ -395,6 +405,10 @@ describe('HttpInstrumentation Integration tests', () => {
const span = spans.find(s => s.kind === SpanKind.CLIENT);
assert.ok(span);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(
span.attributes[ATTR_HTTP_HOST],
`localhost:${mockServerPort}`
);
});
});
});

View File

@ -15,7 +15,12 @@
*/
import { SpanKind, Span, context, propagation } from '@opentelemetry/api';
import { ATTR_NETWORK_PROTOCOL_NAME } from '@opentelemetry/semantic-conventions';
import {
HTTP_FLAVOR_VALUE_HTTP_1_1,
NET_TRANSPORT_VALUE_IP_TCP,
ATTR_HTTP_FLAVOR,
ATTR_NET_TRANSPORT,
} from '../../src/semconv';
import * as assert from 'assert';
import * as http from 'http';
import * as fs from 'fs';
@ -160,6 +165,8 @@ describe('HttpsInstrumentation Integration tests', () => {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
@ -185,6 +192,8 @@ describe('HttpsInstrumentation Integration tests', () => {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
@ -213,6 +222,8 @@ describe('HttpsInstrumentation Integration tests', () => {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
path: '/?query=test',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
@ -221,9 +232,13 @@ describe('HttpsInstrumentation Integration tests', () => {
assert.strictEqual(spans.length, 2);
assert.strictEqual(span.name, 'GET');
assert.strictEqual(result.reqHeaders['x-foo'], 'foo');
assert.ok(
!span.attributes[ATTR_NETWORK_PROTOCOL_NAME],
'should not be added for HTTP kind'
assert.strictEqual(
span.attributes[ATTR_HTTP_FLAVOR],
HTTP_FLAVOR_VALUE_HTTP_1_1
);
assert.strictEqual(
span.attributes[ATTR_NET_TRANSPORT],
NET_TRANSPORT_VALUE_IP_TCP
);
assertSpan(span, SpanKind.CLIENT, validations);
});
@ -239,6 +254,7 @@ describe('HttpsInstrumentation Integration tests', () => {
hostname: 'localhost',
httpStatusCode: result.statusCode!,
httpMethod: 'GET',
pathname: '/',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',
@ -266,6 +282,7 @@ describe('HttpsInstrumentation Integration tests', () => {
hostname: 'localhost',
httpStatusCode: 200,
httpMethod: 'GET',
pathname: '/',
resHeaders: result.resHeaders,
reqHeaders: result.reqHeaders,
component: 'https',

View File

@ -22,20 +22,28 @@ import {
import { hrTimeToNanoseconds } from '@opentelemetry/core';
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
import {
ATTR_HTTP_REQUEST_METHOD,
ATTR_HTTP_RESPONSE_STATUS_CODE,
ATTR_NETWORK_PEER_ADDRESS,
ATTR_SERVER_ADDRESS,
ATTR_SERVER_PORT,
ATTR_URL_FULL,
ATTR_URL_SCHEME,
ATTR_USER_AGENT_ORIGINAL,
ATTR_URL_PATH,
} from '@opentelemetry/semantic-conventions';
ATTR_HTTP_METHOD,
ATTR_HTTP_REQUEST_CONTENT_LENGTH,
ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH,
ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED,
ATTR_HTTP_SCHEME,
ATTR_HTTP_SERVER_NAME,
ATTR_HTTP_STATUS_CODE,
ATTR_HTTP_TARGET,
ATTR_HTTP_URL,
ATTR_HTTP_USER_AGENT,
ATTR_NET_HOST_IP,
ATTR_NET_HOST_PORT,
ATTR_NET_PEER_IP,
ATTR_NET_PEER_NAME,
ATTR_NET_PEER_PORT,
} from '../../src/semconv';
import * as assert from 'assert';
import * as http from 'http';
import * as utils from '../../src/utils';
import { DummyPropagation } from './DummyPropagation';
import { AttributeNames } from '../../src/enums/AttributeNames';
export const assertSpan = (
span: ReadableSpan,
@ -45,10 +53,11 @@ export const assertSpan = (
httpMethod: string;
resHeaders: http.IncomingHttpHeaders;
hostname: string;
pathname?: string;
pathname: string;
reqHeaders?: http.OutgoingHttpHeaders;
path?: string | null;
forceStatus?: SpanStatus;
serverName?: string;
component: string;
noNetPeer?: boolean; // we don't expect net peer info when request throw before being sent
error?: Exception;
@ -60,12 +69,17 @@ export const assertSpan = (
assert.strictEqual(span.name, validations.httpMethod);
assert.strictEqual(
span.attributes[ATTR_HTTP_REQUEST_METHOD],
validations.httpMethod
span.attributes[AttributeNames.HTTP_ERROR_MESSAGE],
span.status.message
);
assert.strictEqual(span.attributes[ATTR_HTTP_METHOD], validations.httpMethod);
assert.strictEqual(
span.attributes[ATTR_HTTP_RESPONSE_STATUS_CODE],
span.attributes[ATTR_HTTP_TARGET],
validations.path || validations.pathname
);
assert.strictEqual(
span.attributes[ATTR_HTTP_STATUS_CODE],
validations.httpStatusCode
);
@ -96,49 +110,83 @@ export const assertSpan = (
assert.ok(span.endTime, 'must be finished');
assert.ok(hrTimeToNanoseconds(span.duration), 'must have positive duration');
if (validations.reqHeaders) {
const userAgent = validations.reqHeaders['user-agent'];
if (userAgent) {
assert.strictEqual(span.attributes[ATTR_HTTP_USER_AGENT], userAgent);
}
}
if (span.kind === SpanKind.CLIENT) {
if (validations.resHeaders['content-length']) {
const contentLength = Number(validations.resHeaders['content-length']);
if (
validations.resHeaders['content-encoding'] &&
validations.resHeaders['content-encoding'] !== 'identity'
) {
assert.strictEqual(
span.attributes[ATTR_HTTP_RESPONSE_CONTENT_LENGTH],
contentLength
);
} else {
assert.strictEqual(
span.attributes[ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED],
contentLength
);
}
}
assert.strictEqual(
span.attributes[ATTR_SERVER_ADDRESS],
span.attributes[ATTR_NET_PEER_NAME],
validations.hostname,
'must be consistent (PEER_NAME and hostname)'
);
if (!validations.noNetPeer) {
assert.ok(
span.attributes[ATTR_NETWORK_PEER_ADDRESS],
'must have PEER_IP'
);
assert.ok(span.attributes[ATTR_SERVER_PORT], 'must have PEER_PORT');
assert.ok(span.attributes[ATTR_NET_PEER_IP], 'must have PEER_IP');
assert.ok(span.attributes[ATTR_NET_PEER_PORT], 'must have PEER_PORT');
}
assert.ok(
(span.attributes[ATTR_URL_FULL] as string).indexOf(
span.attributes[ATTR_SERVER_ADDRESS] as string
(span.attributes[ATTR_HTTP_URL] as string).indexOf(
span.attributes[ATTR_NET_PEER_NAME] as string
) > -1,
'must be consistent'
);
}
if (span.kind === SpanKind.SERVER) {
if (validations.reqHeaders && validations.reqHeaders['content-length']) {
const contentLength = validations.reqHeaders['content-length'];
if (
validations.reqHeaders['content-encoding'] &&
validations.reqHeaders['content-encoding'] !== 'identity'
) {
assert.strictEqual(
span.attributes[ATTR_HTTP_REQUEST_CONTENT_LENGTH],
contentLength
);
} else {
assert.strictEqual(
span.attributes[ATTR_HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED],
contentLength
);
}
}
if (validations.serverName) {
assert.strictEqual(
span.attributes[ATTR_HTTP_SERVER_NAME],
validations.serverName,
' must have serverName attribute'
);
assert.ok(span.attributes[ATTR_NET_HOST_PORT], 'must have HOST_PORT');
assert.ok(span.attributes[ATTR_NET_HOST_IP], 'must have HOST_IP');
}
assert.strictEqual(
span.attributes[ATTR_URL_SCHEME],
span.attributes[ATTR_HTTP_SCHEME],
validations.component,
' must have http.scheme attribute'
);
assert.ok(typeof span.parentSpanContext?.spanId === 'string');
assert.ok(isValidSpanId(span.parentSpanContext.spanId));
assert.strictEqual(
span.attributes[ATTR_URL_PATH],
validations.path || validations.pathname
);
if (validations.reqHeaders) {
const userAgent = validations.reqHeaders['user-agent'];
if (userAgent) {
assert.strictEqual(
span.attributes[ATTR_USER_AGENT_ORIGINAL],
userAgent
);
}
}
} else if (validations.reqHeaders) {
assert.ok(validations.reqHeaders[DummyPropagation.TRACE_CONTEXT_KEY]);
assert.ok(validations.reqHeaders[DummyPropagation.SPAN_CONTEXT_KEY]);