Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 15 commits into from
Sep 6, 2022
1 change: 1 addition & 0 deletions experimental/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ All notable changes to experimental packages in this project will be documented

### :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
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@
"@opentelemetry/api": "^1.0.0"
},
"dependencies": {
"@opentelemetry/api-metrics": "0.32.0",
"@opentelemetry/core": "1.6.0",
"@opentelemetry/instrumentation": "0.32.0",
"@opentelemetry/sdk-metrics": "0.32.0",
"@opentelemetry/semantic-conventions": "1.6.0",
"semver": "^7.3.5"
},
Expand Down
101 changes: 74 additions & 27 deletions experimental/packages/opentelemetry-instrumentation-http/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
import {
context,
HrTime,
INVALID_SPAN_CONTEXT,
propagation,
ROOT_CONTEXT,
Expand All @@ -25,7 +26,8 @@ import {
SpanStatusCode,
trace,
} 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 https from 'https';
import { Socket } from 'net';
Expand Down Expand Up @@ -58,15 +60,35 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
private readonly _spanNotEnded: WeakSet<Span> = new WeakSet<Span>();
private readonly _version = process.versions.node;
private _headerCapture;
private _httpServerDurationHistogram!: Histogram;
private _httpClientDurationHistogram!: Histogram;

constructor(config?: HttpInstrumentationConfig) {
super(
'@opentelemetry/instrumentation-http',
VERSION,
config
);

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', {
dyladan marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down Expand Up @@ -272,11 +294,15 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
* @param request The original request object.
* @param options The arguments to the original function.
* @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(
request: http.ClientRequest,
hostname: string,
span: Span
span: Span,
startTime: HrTime,
metricAttributes: MetricAttributes
): http.ClientRequest {
if (this._getConfig().requestHook) {
this._callRequestHook(span, request);
Expand All @@ -294,6 +320,8 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
response,
);
span.setAttributes(responseAttributes);
metricAttributes = Object.assign(metricAttributes, utils.getOutgoingRequestMetricAttributesOnResponse(responseAttributes));

if (this._getConfig().responseHook) {
this._callResponseHook(span, response);
}
Expand Down Expand Up @@ -323,32 +351,32 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
request,
response
),
() => {},
() => { },
true
);
}

this._closeHttpSpan(span);
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
});
response.on('error', (error: Err) => {
this._diag.debug('outgoingRequest on error()', error);
utils.setSpanWithError(span, error);
const code = utils.parseResponseStatus(SpanKind.CLIENT, response.statusCode);
span.setStatus({ code, message: error.message });
this._closeHttpSpan(span);
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
});
}
);
request.on('close', () => {
this._diag.debug('outgoingRequest on request close()');
if (!request.aborted) {
this._closeHttpSpan(span);
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
}
});
request.on('error', (error: Err) => {
this._diag.debug('outgoingRequest on request error()', error);
utils.setSpanWithError(span, error);
this._closeHttpSpan(span);
this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
});

this._diag.debug('http.ClientRequest return request');
Expand Down Expand Up @@ -404,18 +432,23 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {

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 = {
kind: SpanKind.SERVER,
attributes: utils.getIncomingRequestAttributes(request, {
component: component,
serverName: instrumentation._getConfig().serverName,
hookAttributes: instrumentation._callStartSpanHook(
request,
instrumentation._getConfig().startIncomingSpanHook
),
}),
attributes: spanAttributes,
};

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

const ctx = propagation.extract(ROOT_CONTEXT, headers);
const span = instrumentation._startHttpSpan(
`${component.toLocaleUpperCase()} ${method}`,
Expand Down Expand Up @@ -456,7 +489,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
error => {
if (error) {
utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(span);
instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
throw error;
}
}
Expand All @@ -466,6 +499,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
request,
response
);
metricAttributes = Object.assign(metricAttributes, utils.getIncomingRequestMetricAttributesOnResponse(attributes));

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

Expand All @@ -481,12 +515,12 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
request,
response
),
() => {},
() => { },
true
);
}

instrumentation._closeHttpSpan(span);
instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
return returned;
};

Expand All @@ -495,7 +529,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
error => {
if (error) {
utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(span);
instrumentation._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes);
throw error;
}
}
Expand All @@ -520,7 +554,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
}
const extraOptions =
typeof args[0] === 'object' &&
(typeof options === 'string' || options instanceof url.URL)
(typeof options === 'string' || options instanceof url.URL)
? (args.shift() as http.RequestOptions)
: undefined;
const { origin, pathname, method, optionsParsed } = utils.getRequestInfo(
Expand Down Expand Up @@ -572,6 +606,9 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
),
});

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

const spanOptions: SpanOptions = {
kind: SpanKind.CLIENT,
attributes,
Expand Down Expand Up @@ -601,7 +638,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
error => {
if (error) {
utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(span);
instrumentation._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes);
throw error;
}
}
Expand All @@ -612,7 +649,9 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
return instrumentation._traceClientRequest(
request,
hostname,
span
span,
startTime,
metricAttributes
);
});
};
Expand Down Expand Up @@ -646,13 +685,21 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
return span;
}

private _closeHttpSpan(span: Span) {
private _closeHttpSpan(span: Span, spanKind: SpanKind, startTime: HrTime, metricAttributes: MetricAttributes) {
if (!this._spanNotEnded.has(span)) {
return;
}

span.end();
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(
Expand All @@ -661,7 +708,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
) {
safeExecuteInTheMiddle(
() => this._getConfig().responseHook!(span, response),
() => {},
() => { },
true
);
}
Expand All @@ -672,7 +719,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
) {
safeExecuteInTheMiddle(
() => this._getConfig().requestHook!(span, request),
() => {},
() => { },
true
);
}
Expand All @@ -681,7 +728,7 @@ export class HttpInstrumentation extends InstrumentationBase<Http> {
request: http.IncomingMessage | http.RequestOptions,
hookFunc: Function | undefined,
) {
if(typeof hookFunc === 'function'){
if (typeof hookFunc === 'function') {
return safeExecuteInTheMiddle(
() => hookFunc(request),
() => { },
Expand Down
Loading