Add HTTP Server and Client duration Metrics in HTTP Node.js Instrumentation (#3149)

This commit is contained in:
Hector Hernandez 2022-09-06 11:22:22 -07:00 committed by GitHub
parent a8047ba9cd
commit 597ea98e58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 254 additions and 30 deletions

View File

@ -57,6 +57,7 @@ All notable changes to experimental packages in this project will be documented
### :rocket: (Enhancement) ### :rocket: (Enhancement)
* feature(instrumentation-http): Add HTTP Server and Client duration Metrics in HTTP Node.js Instrumentation [#3149](https://github.com/open-telemetry/opentelemetry-js/pull/3149) @hectorhdzg
* fix(add-views-to-node-sdk): added the ability to define meter views in `NodeSDK` [#3066](https://github.com/open-telemetry/opentelemetry-js/pull/3124) @weyert * fix(add-views-to-node-sdk): added the ability to define meter views in `NodeSDK` [#3066](https://github.com/open-telemetry/opentelemetry-js/pull/3124) @weyert
* feature(add-console-metrics-exporter): add ConsoleMetricExporter [#3120](https://github.com/open-telemetry/opentelemetry-js/pull/3120) @weyert * feature(add-console-metrics-exporter): add ConsoleMetricExporter [#3120](https://github.com/open-telemetry/opentelemetry-js/pull/3120) @weyert
* feature(prometheus-serialiser): export the unit block when unit is set in metric descriptor [#3066](https://github.com/open-telemetry/opentelemetry-js/pull/3041) @weyert * feature(prometheus-serialiser): export the unit block when unit is set in metric descriptor [#3066](https://github.com/open-telemetry/opentelemetry-js/pull/3041) @weyert

View File

@ -74,8 +74,10 @@
"@opentelemetry/api": "^1.0.0" "@opentelemetry/api": "^1.0.0"
}, },
"dependencies": { "dependencies": {
"@opentelemetry/api-metrics": "0.32.0",
"@opentelemetry/core": "1.6.0", "@opentelemetry/core": "1.6.0",
"@opentelemetry/instrumentation": "0.32.0", "@opentelemetry/instrumentation": "0.32.0",
"@opentelemetry/sdk-metrics": "0.32.0",
"@opentelemetry/semantic-conventions": "1.6.0", "@opentelemetry/semantic-conventions": "1.6.0",
"semver": "^7.3.5" "semver": "^7.3.5"
}, },

View File

@ -15,6 +15,7 @@
*/ */
import { import {
context, context,
HrTime,
INVALID_SPAN_CONTEXT, INVALID_SPAN_CONTEXT,
propagation, propagation,
ROOT_CONTEXT, ROOT_CONTEXT,
@ -25,7 +26,8 @@ import {
SpanStatusCode, SpanStatusCode,
trace, trace,
} from '@opentelemetry/api'; } from '@opentelemetry/api';
import { suppressTracing } from '@opentelemetry/core'; import { Histogram, MeterProvider, MetricAttributes, ValueType } from '@opentelemetry/api-metrics';
import { hrTime, hrTimeDuration, hrTimeToMilliseconds, suppressTracing } from '@opentelemetry/core';
import type * as http from 'http'; import type * as http from 'http';
import type * as https from 'https'; import type * as https from 'https';
import { Socket } from 'net'; import { Socket } from 'net';
@ -58,6 +60,8 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
private readonly _spanNotEnded: WeakSet<Span> = new WeakSet<Span>(); private readonly _spanNotEnded: WeakSet<Span> = new WeakSet<Span>();
private readonly _version = process.versions.node; private readonly _version = process.versions.node;
private _headerCapture; private _headerCapture;
private _httpServerDurationHistogram!: Histogram;
private _httpClientDurationHistogram!: Histogram;
constructor(config?: HttpInstrumentationConfig) { constructor(config?: HttpInstrumentationConfig) {
super( super(
@ -65,8 +69,26 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
VERSION, VERSION,
config config
); );
this._headerCapture = this._createHeaderCapture(); this._headerCapture = this._createHeaderCapture();
this._updateMetricInstruments();
}
override setMeterProvider(meterProvider: MeterProvider) {
super.setMeterProvider(meterProvider);
this._updateMetricInstruments();
}
private _updateMetricInstruments() {
this._httpServerDurationHistogram = this.meter.createHistogram('http.server.duration', {
description: 'measures the duration of the inbound HTTP requests',
unit: 'ms',
valueType: ValueType.DOUBLE
});
this._httpClientDurationHistogram = this.meter.createHistogram('http.client.duration', {
description: 'measures the duration of the outbound HTTP requests',
unit: 'ms',
valueType: ValueType.DOUBLE
});
} }
private _getConfig(): HttpInstrumentationConfig { private _getConfig(): HttpInstrumentationConfig {
@ -272,11 +294,15 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
* @param request The original request object. * @param request The original request object.
* @param options The arguments to the original function. * @param options The arguments to the original function.
* @param span representing the current operation * @param span representing the current operation
* @param startTime representing the start time of the request to calculate duration in Metric
* @param metricAttributes metric attributes
*/ */
private _traceClientRequest( private _traceClientRequest(
request: http.ClientRequest, request: http.ClientRequest,
hostname: string, hostname: string,
span: Span span: Span,
startTime: HrTime,
metricAttributes: MetricAttributes
): http.ClientRequest { ): http.ClientRequest {
if (this._getConfig().requestHook) { if (this._getConfig().requestHook) {
this._callRequestHook(span, request); this._callRequestHook(span, request);
@ -294,6 +320,8 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
response, response,
); );
span.setAttributes(responseAttributes); span.setAttributes(responseAttributes);
metricAttributes = Object.assign(metricAttributes, utils.getOutgoingRequestMetricAttributesOnResponse(responseAttributes));
if (this._getConfig().responseHook) { if (this._getConfig().responseHook) {
this._callResponseHook(span, response); this._callResponseHook(span, response);
} }
@ -323,32 +351,32 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
request, request,
response response
), ),
() => {}, () => { },
true true
); );
} }
this._closeHttpSpan(span); this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
}); });
response.on('error', (error: Err) => { response.on('error', (error: Err) => {
this._diag.debug('outgoingRequest on error()', error); this._diag.debug('outgoingRequest on error()', error);
utils.setSpanWithError(span, error); utils.setSpanWithError(span, error);
const code = utils.parseResponseStatus(SpanKind.CLIENT, response.statusCode); const code = utils.parseResponseStatus(SpanKind.CLIENT, response.statusCode);
span.setStatus({ code, message: error.message }); span.setStatus({ code, message: error.message });
this._closeHttpSpan(span); this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
}); });
} }
); );
request.on('close', () => { request.on('close', () => {
this._diag.debug('outgoingRequest on request close()'); this._diag.debug('outgoingRequest on request close()');
if (!request.aborted) { if (!request.aborted) {
this._closeHttpSpan(span); this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
} }
}); });
request.on('error', (error: Err) => { request.on('error', (error: Err) => {
this._diag.debug('outgoingRequest on request error()', error); this._diag.debug('outgoingRequest on request error()', error);
utils.setSpanWithError(span, error); utils.setSpanWithError(span, error);
this._closeHttpSpan(span); this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
}); });
this._diag.debug('http.ClientRequest return request'); this._diag.debug('http.ClientRequest return request');
@ -404,18 +432,23 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
const headers = request.headers; const headers = request.headers;
const spanAttributes = utils.getIncomingRequestAttributes(request, {
component: component,
serverName: instrumentation._getConfig().serverName,
hookAttributes: instrumentation._callStartSpanHook(
request,
instrumentation._getConfig().startIncomingSpanHook
),
});
const spanOptions: SpanOptions = { const spanOptions: SpanOptions = {
kind: SpanKind.SERVER, kind: SpanKind.SERVER,
attributes: utils.getIncomingRequestAttributes(request, { attributes: spanAttributes,
component: component,
serverName: instrumentation._getConfig().serverName,
hookAttributes: instrumentation._callStartSpanHook(
request,
instrumentation._getConfig().startIncomingSpanHook
),
}),
}; };
const startTime = hrTime();
let metricAttributes: MetricAttributes = utils.getIncomingRequestMetricAttributes(spanAttributes);
const ctx = propagation.extract(ROOT_CONTEXT, headers); const ctx = propagation.extract(ROOT_CONTEXT, headers);
const span = instrumentation._startHttpSpan( const span = instrumentation._startHttpSpan(
`${component.toLocaleUpperCase()} ${method}`, `${component.toLocaleUpperCase()} ${method}`,
@ -456,7 +489,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
error => { error => {
if (error) { if (error) {
utils.setSpanWithError(span, error); utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(span); instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
throw error; throw error;
} }
} }
@ -466,6 +499,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
request, request,
response response
); );
metricAttributes = Object.assign(metricAttributes, utils.getIncomingRequestMetricAttributesOnResponse(attributes));
instrumentation._headerCapture.server.captureResponseHeaders(span, header => response.getHeader(header)); instrumentation._headerCapture.server.captureResponseHeaders(span, header => response.getHeader(header));
@ -481,12 +515,12 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
request, request,
response response
), ),
() => {}, () => { },
true true
); );
} }
instrumentation._closeHttpSpan(span); instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
return returned; return returned;
}; };
@ -495,7 +529,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
error => { error => {
if (error) { if (error) {
utils.setSpanWithError(span, error); utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(span); instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
throw error; throw error;
} }
} }
@ -520,7 +554,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
} }
const extraOptions = const extraOptions =
typeof args[0] === 'object' && typeof args[0] === 'object' &&
(typeof options === 'string' || options instanceof url.URL) (typeof options === 'string' || options instanceof url.URL)
? (args.shift() as http.RequestOptions) ? (args.shift() as http.RequestOptions)
: undefined; : undefined;
const { origin, pathname, method, optionsParsed } = utils.getRequestInfo( const { origin, pathname, method, optionsParsed } = utils.getRequestInfo(
@ -572,6 +606,9 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
), ),
}); });
const startTime = hrTime();
const metricAttributes: MetricAttributes = utils.getOutgoingRequestMetricAttributes(attributes);
const spanOptions: SpanOptions = { const spanOptions: SpanOptions = {
kind: SpanKind.CLIENT, kind: SpanKind.CLIENT,
attributes, attributes,
@ -601,7 +638,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
error => { error => {
if (error) { if (error) {
utils.setSpanWithError(span, error); utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(span); instrumentation._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
throw error; throw error;
} }
} }
@ -612,7 +649,9 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
return instrumentation._traceClientRequest( return instrumentation._traceClientRequest(
request, request,
hostname, hostname,
span span,
startTime,
metricAttributes
); );
}); });
}; };
@ -646,13 +685,21 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
return span; return span;
} }
private _closeHttpSpan(span: Span) { private _closeHttpSpan(span: Span, spanKind: SpanKind, startTime: HrTime, metricAttributes: MetricAttributes) {
if (!this._spanNotEnded.has(span)) { if (!this._spanNotEnded.has(span)) {
return; return;
} }
span.end(); span.end();
this._spanNotEnded.delete(span); this._spanNotEnded.delete(span);
// Record metrics
const duration = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime()));
if (spanKind === SpanKind.SERVER) {
this._httpServerDurationHistogram.record(duration, metricAttributes);
} else if (spanKind === SpanKind.CLIENT) {
this._httpClientDurationHistogram.record(duration, metricAttributes);
}
} }
private _callResponseHook( private _callResponseHook(
@ -661,7 +708,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
) { ) {
safeExecuteInTheMiddle( safeExecuteInTheMiddle(
() => this._getConfig().responseHook!(span, response), () => this._getConfig().responseHook!(span, response),
() => {}, () => { },
true true
); );
} }
@ -672,7 +719,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
) { ) {
safeExecuteInTheMiddle( safeExecuteInTheMiddle(
() => this._getConfig().requestHook!(span, request), () => this._getConfig().requestHook!(span, request),
() => {}, () => { },
true true
); );
} }
@ -681,7 +728,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
request: http.IncomingMessage | http.RequestOptions, request: http.IncomingMessage | http.RequestOptions,
hookFunc: Function | undefined, hookFunc: Function | undefined,
) { ) {
if(typeof hookFunc === 'function'){ if (typeof hookFunc === 'function') {
return safeExecuteInTheMiddle( return safeExecuteInTheMiddle(
() => hookFunc(request), () => hookFunc(request),
() => { }, () => { },

View File

@ -35,6 +35,7 @@ import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import * as url from 'url'; import * as url from 'url';
import { AttributeNames } from './enums/AttributeNames'; import { AttributeNames } from './enums/AttributeNames';
import { Err, IgnoreMatcher, ParsedRequestOptions } from './types'; import { Err, IgnoreMatcher, ParsedRequestOptions } from './types';
import { MetricAttributes } from '@opentelemetry/api-metrics';
/** /**
* Get an absolute url * Get an absolute url
@ -299,7 +300,7 @@ export const extractHostnameAndPort = (
requestOptions: Pick<ParsedRequestOptions, 'hostname' | 'host' | 'port' | 'protocol'> requestOptions: Pick<ParsedRequestOptions, 'hostname' | 'host' | 'port' | 'protocol'>
): { hostname: string, port: number | string } => { ): { hostname: string, port: number | string } => {
if (requestOptions.hostname && requestOptions.port) { if (requestOptions.hostname && requestOptions.port) {
return {hostname: requestOptions.hostname, port: requestOptions.port}; return { hostname: requestOptions.hostname, port: requestOptions.port };
} }
const matches = requestOptions.host?.match(/^([^:/ ]+)(:\d{1,5})?/) || null; const matches = requestOptions.host?.match(/^([^:/ ]+)(:\d{1,5})?/) || null;
const hostname = requestOptions.hostname || (matches === null ? 'localhost' : matches[1]); const hostname = requestOptions.hostname || (matches === null ? 'localhost' : matches[1]);
@ -312,7 +313,7 @@ export const extractHostnameAndPort = (
port = requestOptions.protocol === 'https:' ? '443' : '80'; port = requestOptions.protocol === 'https:' ? '443' : '80';
} }
} }
return {hostname, port}; return { hostname, port };
}; };
/** /**
@ -348,6 +349,20 @@ export const getOutgoingRequestAttributes = (
return Object.assign(attributes, options.hookAttributes); return Object.assign(attributes, options.hookAttributes);
}; };
/**
* Returns outgoing request Metric attributes scoped to the request data
* @param {SpanAttributes} spanAttributes the span attributes
*/
export const getOutgoingRequestMetricAttributes = (
spanAttributes: SpanAttributes
): MetricAttributes => {
const metricAttributes: MetricAttributes = {};
metricAttributes[SemanticAttributes.HTTP_METHOD] = spanAttributes[SemanticAttributes.HTTP_METHOD];
metricAttributes[SemanticAttributes.NET_PEER_NAME] = spanAttributes[SemanticAttributes.NET_PEER_NAME];
//TODO: http.url attribute, it should susbtitute any parameters to avoid high cardinality.
return metricAttributes;
};
/** /**
* Returns attributes related to the kind of HTTP protocol used * 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". * @param {string} [kind] Kind of HTTP protocol used: "1.0", "1.1", "2", "SPDY" or "QUIC".
@ -392,6 +407,20 @@ export const getOutgoingRequestAttributesOnResponse = (
return Object.assign(attributes, httpKindAttributes); return Object.assign(attributes, httpKindAttributes);
}; };
/**
* Returns outgoing request Metric attributes scoped to the response data
* @param {SpanAttributes} spanAttributes the span attributes
*/
export const getOutgoingRequestMetricAttributesOnResponse = (
spanAttributes: SpanAttributes
): MetricAttributes => {
const metricAttributes: MetricAttributes = {};
metricAttributes[SemanticAttributes.NET_PEER_PORT] = spanAttributes[SemanticAttributes.NET_PEER_PORT];
metricAttributes[SemanticAttributes.HTTP_STATUS_CODE] = spanAttributes[SemanticAttributes.HTTP_STATUS_CODE];
metricAttributes[SemanticAttributes.HTTP_FLAVOR] = spanAttributes[SemanticAttributes.HTTP_FLAVOR];
return metricAttributes;
};
/** /**
* Returns incoming request attributes scoped to the request data * Returns incoming request attributes scoped to the request data
* @param {IncomingMessage} request the request object * @param {IncomingMessage} request the request object
@ -422,6 +451,7 @@ export const getIncomingRequestAttributes = (
[SemanticAttributes.HTTP_HOST]: host, [SemanticAttributes.HTTP_HOST]: host,
[SemanticAttributes.NET_HOST_NAME]: hostname, [SemanticAttributes.NET_HOST_NAME]: hostname,
[SemanticAttributes.HTTP_METHOD]: method, [SemanticAttributes.HTTP_METHOD]: method,
[SemanticAttributes.HTTP_SCHEME]: options.component,
}; };
if (typeof ips === 'string') { if (typeof ips === 'string') {
@ -445,6 +475,23 @@ export const getIncomingRequestAttributes = (
return Object.assign(attributes, httpKindAttributes, options.hookAttributes); return Object.assign(attributes, httpKindAttributes, options.hookAttributes);
}; };
/**
* Returns incoming request Metric attributes scoped to the request data
* @param {SpanAttributes} spanAttributes the span attributes
* @param {{ component: string }} options used to pass data needed to create attributes
*/
export const getIncomingRequestMetricAttributes = (
spanAttributes: SpanAttributes
): MetricAttributes => {
const metricAttributes: MetricAttributes = {};
metricAttributes[SemanticAttributes.HTTP_SCHEME] = spanAttributes[SemanticAttributes.HTTP_SCHEME];
metricAttributes[SemanticAttributes.HTTP_METHOD] = spanAttributes[SemanticAttributes.HTTP_METHOD];
metricAttributes[SemanticAttributes.NET_HOST_NAME] = spanAttributes[SemanticAttributes.NET_HOST_NAME];
metricAttributes[SemanticAttributes.HTTP_FLAVOR] = spanAttributes[SemanticAttributes.HTTP_FLAVOR];
//TODO: http.target attribute, it should susbtitute any parameters to avoid high cardinality.
return metricAttributes;
};
/** /**
* Returns incoming request attributes scoped to the response data * Returns incoming request attributes scoped to the response data
* @param {(ServerResponse & { socket: Socket; })} response the response object * @param {(ServerResponse & { socket: Socket; })} response the response object
@ -475,6 +522,19 @@ export const getIncomingRequestAttributesOnResponse = (
return attributes; return attributes;
}; };
/**
* Returns incoming request Metric attributes scoped to the request data
* @param {SpanAttributes} spanAttributes the span attributes
*/
export const getIncomingRequestMetricAttributesOnResponse = (
spanAttributes: SpanAttributes
): MetricAttributes => {
const metricAttributes: MetricAttributes = {};
metricAttributes[SemanticAttributes.HTTP_STATUS_CODE] = spanAttributes[SemanticAttributes.HTTP_STATUS_CODE];
metricAttributes[SemanticAttributes.NET_HOST_PORT] = spanAttributes[SemanticAttributes.NET_HOST_PORT];
return metricAttributes;
};
export function headerCapture(type: 'request' | 'response', headers: string[]) { export function headerCapture(type: 'request' | 'response', headers: string[]) {
const normalizedHeaders = new Map(headers.map(header => [header.toLowerCase(), header.toLowerCase().replace(/-/g, '_')])); const normalizedHeaders = new Map(headers.map(header => [header.toLowerCase(), header.toLowerCase().replace(/-/g, '_')]));

View File

@ -0,0 +1,103 @@
/*
* 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.
*/
import {
AggregationTemporality,
DataPointType,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import * as assert from 'assert';
import { HttpInstrumentation } from '../../src/http';
import { httpRequest } from '../utils/httpRequest';
const instrumentation = new HttpInstrumentation();
instrumentation.enable();
instrumentation.disable();
import * as http from 'http';
let server: http.Server;
const serverPort = 22346;
const protocol = 'http';
const hostname = 'localhost';
const pathname = '/test';
const tracerProvider = new NodeTracerProvider();
const meterProvider = new MeterProvider();
const metricsMemoryExporter = new InMemoryMetricExporter(AggregationTemporality.DELTA);
const metricReader = new PeriodicExportingMetricReader({ exporter: metricsMemoryExporter, exportIntervalMillis: 100 });
meterProvider.addMetricReader(metricReader);
instrumentation.setTracerProvider(tracerProvider);
instrumentation.setMeterProvider(meterProvider);
describe('metrics', () => {
beforeEach(() => {
metricsMemoryExporter.reset();
});
before(() => {
instrumentation.enable();
server = http.createServer((request, response) => {
response.end('Test Server Response');
});
server.listen(serverPort);
});
after(() => {
server.close();
instrumentation.disable();
});
it('should add server/client duration metrics', async () => {
const requestCount = 3;
for (let i = 0; i < requestCount; i++) {
await httpRequest.get(`${protocol}://${hostname}:${serverPort}${pathname}`);
}
await new Promise(resolve => setTimeout(resolve, 300));
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 the 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[SemanticAttributes.HTTP_SCHEME], 'http');
assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.HTTP_METHOD], 'GET');
assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.HTTP_FLAVOR], '1.1');
assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.NET_HOST_NAME], 'localhost');
assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.HTTP_STATUS_CODE], 200);
assert.strictEqual(metrics[0].dataPoints[0].attributes[SemanticAttributes.NET_HOST_PORT], 22346);
assert.strictEqual(metrics[1].dataPointType, DataPointType.HISTOGRAM);
assert.strictEqual(metrics[1].descriptor.description, 'measures the duration of the 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[SemanticAttributes.HTTP_METHOD], 'GET');
assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.NET_PEER_NAME], 'localhost');
assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.NET_PEER_PORT], 22346);
assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.HTTP_STATUS_CODE], 200);
assert.strictEqual(metrics[1].dataPoints[0].attributes[SemanticAttributes.HTTP_FLAVOR], '1.1');
});
});

View File

@ -125,7 +125,7 @@ export const assertSpan = (
validations.hostname, validations.hostname,
'must be consistent (PEER_NAME and hostname)' 'must be consistent (PEER_NAME and hostname)'
); );
if(!validations.noNetPeer) { if (!validations.noNetPeer) {
assert.ok( assert.ok(
span.attributes[SemanticAttributes.NET_PEER_IP], span.attributes[SemanticAttributes.NET_PEER_IP],
'must have PEER_IP' 'must have PEER_IP'
@ -178,6 +178,11 @@ export const assertSpan = (
'must have HOST_IP' 'must have HOST_IP'
); );
} }
assert.strictEqual(
span.attributes[SemanticAttributes.HTTP_SCHEME],
validations.component,
' must have http.scheme attribute'
);
assert.ok(typeof span.parentSpanId === 'string'); assert.ok(typeof span.parentSpanId === 'string');
assert.ok(isValidSpanId(span.parentSpanId)); assert.ok(isValidSpanId(span.parentSpanId));
} else if (validations.reqHeaders) { } else if (validations.reqHeaders) {

View File

@ -24,8 +24,14 @@
{ {
"path": "../../../packages/opentelemetry-semantic-conventions" "path": "../../../packages/opentelemetry-semantic-conventions"
}, },
{
"path": "../opentelemetry-api-metrics"
},
{ {
"path": "../opentelemetry-instrumentation" "path": "../opentelemetry-instrumentation"
},
{
"path": "../opentelemetry-sdk-metrics"
} }
] ]
} }