@splunk/otel
Version:
The Splunk distribution of OpenTelemetry Node Instrumentation provides a Node agent that automatically instruments your Node application to capture and report distributed traces to Splunk APM.
516 lines • 26.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpDcInstrumentation = void 0;
/*
* Copyright Splunk Inc., 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
*
* http://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.
*/
const api_1 = require("@opentelemetry/api");
const core_1 = require("@opentelemetry/core");
const version_1 = require("../../version");
const instrumentation_1 = require("@opentelemetry/instrumentation");
const events_1 = require("events");
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
const utils_1 = require("./utils");
const diagnostics_channel = require("node:diagnostics_channel");
const INSTRUMENTATION_SYMBOL = Symbol.for('HTTPDC_INSTRUMENTATION');
/**
* `node:http` and `node:https` instrumentation for OpenTelemetry
*/
class HttpDcInstrumentation extends instrumentation_1.InstrumentationBase {
constructor(config = {}) {
super('@opentelemetry/instrumentation-httpdc', version_1.VERSION, config);
this._semconvStability = instrumentation_1.SemconvStability.OLD;
this._headerCapture = this._createHeaderCapture();
this._semconvStability = (0, instrumentation_1.semconvStabilityFromStr)('http', process.env.OTEL_SEMCONV_STABILITY_OPT_IN);
}
_updateMetricInstruments() {
this._oldHttpServerDurationHistogram = this.meter.createHistogram('http.server.duration', {
description: 'Measures the duration of inbound HTTP requests.',
unit: 'ms',
valueType: api_1.ValueType.DOUBLE,
});
this._oldHttpClientDurationHistogram = this.meter.createHistogram('http.client.duration', {
description: 'Measures the duration of outbound HTTP requests.',
unit: 'ms',
valueType: api_1.ValueType.DOUBLE,
});
this._stableHttpServerDurationHistogram = this.meter.createHistogram(semantic_conventions_1.METRIC_HTTP_SERVER_REQUEST_DURATION, {
description: 'Duration of HTTP server requests.',
unit: 's',
valueType: api_1.ValueType.DOUBLE,
advice: {
explicitBucketBoundaries: [
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5,
7.5, 10,
],
},
});
this._stableHttpClientDurationHistogram = this.meter.createHistogram(semantic_conventions_1.METRIC_HTTP_CLIENT_REQUEST_DURATION, {
description: 'Duration of HTTP client requests.',
unit: 's',
valueType: api_1.ValueType.DOUBLE,
advice: {
explicitBucketBoundaries: [
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5,
7.5, 10,
],
},
});
}
_recordServerDuration(durationMs, oldAttributes, stableAttributes) {
if (oldAttributes && this._semconvStability & instrumentation_1.SemconvStability.OLD) {
// old histogram is counted in MS
this._oldHttpServerDurationHistogram.record(durationMs, oldAttributes);
}
if (stableAttributes && this._semconvStability & instrumentation_1.SemconvStability.STABLE) {
// stable histogram is counted in S
this._stableHttpServerDurationHistogram.record(durationMs / 1000, stableAttributes);
}
}
_recordClientDuration(durationMs, oldAttributes, stableAttributes) {
if (oldAttributes && this._semconvStability & instrumentation_1.SemconvStability.OLD) {
// old histogram is counted in MS
this._oldHttpClientDurationHistogram.record(durationMs, oldAttributes);
}
if (stableAttributes && this._semconvStability & instrumentation_1.SemconvStability.STABLE) {
// stable histogram is counted in S
this._stableHttpClientDurationHistogram.record(durationMs / 1000, stableAttributes);
}
}
setConfig(config = {}) {
super.setConfig(config);
this._headerCapture = this._createHeaderCapture();
this._semconvStability = config.semconvStability
? config.semconvStability
: (0, instrumentation_1.semconvStabilityFromStr)('http', process.env.OTEL_SEMCONV_STABILITY_OPT_IN);
}
_wrapSyncClientError() {
const instrumentation = this;
return (original) => function patchedRequest(...args) {
const start = (0, core_1.hrTime)();
try {
return original.apply(this, args);
}
catch (err) {
instrumentation._handleClientRequestError(start, args, err);
throw err;
}
};
}
_handleClientRequestError(start, args, err) {
var _a, _b, _c;
const durationMs = (0, core_1.hrTimeToMilliseconds)((0, core_1.hrTimeDuration)(start, (0, core_1.hrTime)()));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const request = args[0];
const method = ((_a = request.method) !== null && _a !== void 0 ? _a : 'GET').toUpperCase();
const hostname = (_b = request.hostname) !== null && _b !== void 0 ? _b : 'localhost';
const port = (_c = request.port) !== null && _c !== void 0 ? _c : 80;
let oldMetricAttrs;
let stableMetricAttrs;
if (this._semconvStability & instrumentation_1.SemconvStability.OLD) {
const oldSpanAttrs = {
[semantic_conventions_1.SEMATTRS_HTTP_METHOD]: method,
[semantic_conventions_1.SEMATTRS_NET_PEER_NAME]: hostname,
};
if (port !== undefined)
oldSpanAttrs[semantic_conventions_1.SEMATTRS_NET_PEER_PORT] = port;
oldMetricAttrs = (0, utils_1.getOutgoingRequestMetricAttributes)(oldSpanAttrs);
}
if (this._semconvStability & instrumentation_1.SemconvStability.STABLE) {
stableMetricAttrs = {
[semantic_conventions_1.ATTR_HTTP_REQUEST_METHOD]: method,
[semantic_conventions_1.ATTR_SERVER_ADDRESS]: hostname,
[semantic_conventions_1.ATTR_ERROR_TYPE]: err.name,
};
if (port !== undefined)
stableMetricAttrs[semantic_conventions_1.ATTR_SERVER_PORT] = port;
}
this._recordClientDuration(durationMs, oldMetricAttrs, stableMetricAttrs);
}
init() {
diagnostics_channel.subscribe('http.server.request.start', this._httpServerRequestStart.bind(this));
diagnostics_channel.subscribe('http.server.response.finish', this._httpServerResponseFinished.bind(this));
diagnostics_channel.subscribe('http.client.request.created', this._httpClientRequestCreated.bind(this));
diagnostics_channel.subscribe('http.client.request.error', this._httpClientRequestError.bind(this));
diagnostics_channel.subscribe('http.client.response.finish', this._httpClientResponseFinished.bind(this));
return [this._getHttpInstrumentation()];
}
_getHttpInstrumentation() {
return new instrumentation_1.InstrumentationNodeModuleDefinition('http', ['*'], (moduleExports) => {
this._wrap(moduleExports.Server.prototype, 'emit', this._getPatchServerEmit());
this._wrap(moduleExports, 'request', this._wrapSyncClientError());
this._wrap(moduleExports, 'get', this._wrapSyncClientError());
return moduleExports;
}, (moduleExports) => {
if (moduleExports === undefined)
return;
this._unwrap(moduleExports.Server.prototype, 'emit');
this._unwrap(moduleExports, 'request');
this._unwrap(moduleExports, 'get');
});
}
_getPatchServerEmit() {
return (original) => {
return function patchedEmit(event, ...args) {
if (event !== 'request') {
return original.apply(this, [event, ...args]);
}
const response = args[1];
const spanDetails = response[INSTRUMENTATION_SYMBOL];
if ((spanDetails === null || spanDetails === void 0 ? void 0 : spanDetails.span) === undefined) {
return original.apply(this, [event, ...args]);
}
const span = spanDetails.span;
const rpcMetadata = {
type: core_1.RPCType.HTTP,
span,
};
return api_1.context.with((0, core_1.setRPCMetadata)(api_1.trace.setSpan(api_1.context.active(), span), rpcMetadata), () => {
const request = args[0];
api_1.context.bind(api_1.context.active(), request);
return original.apply(this, [event, ...args]);
});
};
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_httpServerRequestStart(message) {
if (!this.isEnabled()) {
return;
}
const request = message.request;
const response = message.response;
const config = this.getConfig();
const ignoreIncomingRequestHook = config.ignoreIncomingRequestHook;
if (ignoreIncomingRequestHook !== undefined) {
if ((0, instrumentation_1.safeExecuteInTheMiddle)(() => ignoreIncomingRequestHook(request), (e) => {
if (e !== null) {
this._diag.error('caught ignoreIncomingRequestHook error: ', e);
}
}, true)) {
return;
}
}
let hookAttributes;
if (config.startIncomingSpanHook !== undefined) {
hookAttributes = this._callStartSpanHook(request, config.startIncomingSpanHook);
}
const spanAttributes = (0, utils_1.getIncomingRequestAttributes)(request, {
serverName: config.serverName,
hookAttributes,
semconvStability: this._semconvStability,
enableSyntheticSourceDetection: config.enableSyntheticSourceDetection || false,
}, this._diag);
const spanOptions = {
kind: api_1.SpanKind.SERVER,
attributes: spanAttributes,
};
let oldMetricAttributes;
let stableMetricAttributes;
const startTime = (0, core_1.hrTime)();
if (this._semconvStability & instrumentation_1.SemconvStability.OLD) {
oldMetricAttributes = (0, utils_1.getIncomingRequestMetricAttributes)(spanAttributes);
}
if (this._semconvStability & instrumentation_1.SemconvStability.STABLE) {
stableMetricAttributes = {
[semantic_conventions_1.ATTR_HTTP_REQUEST_METHOD]: spanAttributes[semantic_conventions_1.ATTR_HTTP_REQUEST_METHOD],
[semantic_conventions_1.ATTR_URL_SCHEME]: spanAttributes[semantic_conventions_1.ATTR_URL_SCHEME],
};
// recommended if and only if one was sent, same as span recommendation
if (spanAttributes[semantic_conventions_1.ATTR_NETWORK_PROTOCOL_VERSION]) {
stableMetricAttributes[semantic_conventions_1.ATTR_NETWORK_PROTOCOL_VERSION] =
spanAttributes[semantic_conventions_1.ATTR_NETWORK_PROTOCOL_VERSION];
}
}
const headers = request.headers;
const method = request.method || 'GET';
const ctx = api_1.propagation.extract(api_1.ROOT_CONTEXT, headers);
const span = this._startHttpSpan(method, spanOptions, ctx);
response[INSTRUMENTATION_SYMBOL] = {
span,
spanKind: api_1.SpanKind.SERVER,
startTime,
oldMetricAttributes,
stableMetricAttributes,
};
response.on(events_1.errorMonitor, (err) => {
const spanDetails = response[INSTRUMENTATION_SYMBOL];
if ((spanDetails === null || spanDetails === void 0 ? void 0 : spanDetails.span) === undefined) {
return;
}
if (!spanDetails.stableMetricAttributes) {
spanDetails.stableMetricAttributes = {};
}
spanDetails.stableMetricAttributes[semantic_conventions_1.ATTR_ERROR_TYPE] = err.name;
(0, utils_1.setSpanWithError)(spanDetails.span, err, this._semconvStability);
this._closeHttpSpan(response);
});
this._callRequestHook(span, request);
this._callResponseHook(span, response);
this._headerCapture.server.captureRequestHeaders(span, (header) => request.headers[header]);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_httpServerResponseFinished(message) {
if (!this.isEnabled()) {
return;
}
const response = message.response;
const spanDetails = response[INSTRUMENTATION_SYMBOL];
if ((spanDetails === null || spanDetails === void 0 ? void 0 : spanDetails.span) !== undefined) {
const request = message.request;
this._onServerResponseFinish(request, response, spanDetails.span);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_httpClientRequestCreated(message) {
if (!this.isEnabled()) {
return;
}
const parentContext = api_1.context.active();
if ((0, core_1.isTracingSuppressed)(parentContext)) {
return;
}
const request = message.request;
const config = this.getConfig();
const ignoreOutgoingRequestHook = config.ignoreOutgoingRequestHook;
if (ignoreOutgoingRequestHook !== undefined) {
if ((0, instrumentation_1.safeExecuteInTheMiddle)(() => ignoreOutgoingRequestHook(request), (e) => {
if (e !== null) {
this._diag.error('caught ignoreOutgoingRequestHook error: ', e);
}
}, true)) {
return;
}
}
const attributes = (0, utils_1.getOutgoingRequestAttributes)(request, this._semconvStability, config.redactedQueryParams, config.enableSyntheticSourceDetection || false);
if (config.startOutgoingSpanHook !== undefined) {
const hookAttributes = this._callStartSpanHook(request, config.startOutgoingSpanHook);
Object.assign(attributes, hookAttributes);
}
// here attributes related to metrics
const startTime = (0, core_1.hrTime)();
let oldMetricAttributes;
let stableMetricAttributes;
if (this._semconvStability & instrumentation_1.SemconvStability.OLD) {
oldMetricAttributes = (0, utils_1.getOutgoingRequestMetricAttributes)(attributes);
}
if (this._semconvStability & instrumentation_1.SemconvStability.STABLE) {
// request method, server address, and server port are both required span attributes
stableMetricAttributes = {
[semantic_conventions_1.ATTR_HTTP_REQUEST_METHOD]: attributes[semantic_conventions_1.ATTR_HTTP_REQUEST_METHOD],
[semantic_conventions_1.ATTR_SERVER_ADDRESS]: attributes[semantic_conventions_1.ATTR_SERVER_ADDRESS],
[semantic_conventions_1.ATTR_SERVER_PORT]: attributes[semantic_conventions_1.ATTR_SERVER_PORT],
};
// required if and only if one was sent, same as span requirement
if (attributes[semantic_conventions_1.ATTR_HTTP_RESPONSE_STATUS_CODE]) {
stableMetricAttributes[semantic_conventions_1.ATTR_HTTP_RESPONSE_STATUS_CODE] =
attributes[semantic_conventions_1.ATTR_HTTP_RESPONSE_STATUS_CODE];
}
// recommended if and only if one was sent, same as span recommendation
if (attributes[semantic_conventions_1.ATTR_NETWORK_PROTOCOL_VERSION]) {
stableMetricAttributes[semantic_conventions_1.ATTR_NETWORK_PROTOCOL_VERSION] =
attributes[semantic_conventions_1.ATTR_NETWORK_PROTOCOL_VERSION];
}
}
const spanOptions = {
kind: api_1.SpanKind.CLIENT,
attributes,
};
const span = this._startHttpSpan(request.method, spanOptions, parentContext);
this._callRequestHook(span, request);
request[INSTRUMENTATION_SYMBOL] = {
span,
spanKind: api_1.SpanKind.CLIENT,
startTime,
oldMetricAttributes,
stableMetricAttributes,
};
const requestContext = api_1.trace.setSpan(parentContext, span);
api_1.context.bind(parentContext, request);
api_1.propagation.inject(requestContext, request, {
set: (req, key, value) => {
req.setHeader(key, value);
},
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_httpClientRequestError(message) {
if (!this.isEnabled()) {
return;
}
const request = message.request;
const spanDetails = request[INSTRUMENTATION_SYMBOL];
if ((spanDetails === null || spanDetails === void 0 ? void 0 : spanDetails.span) === undefined) {
return;
}
if (!spanDetails.stableMetricAttributes) {
spanDetails.stableMetricAttributes = {};
}
spanDetails.stableMetricAttributes[semantic_conventions_1.ATTR_ERROR_TYPE] = message.error.name;
(0, utils_1.setSpanWithError)(spanDetails.span, message.error, this._semconvStability);
this._closeHttpSpan(request);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_httpClientResponseFinished(message) {
if (!this.isEnabled()) {
return;
}
const req = message.request;
const spanDetails = req[INSTRUMENTATION_SYMBOL];
if ((spanDetails === null || spanDetails === void 0 ? void 0 : spanDetails.span) === undefined) {
return;
}
const span = spanDetails.span;
const response = message.response;
const attributes = (0, utils_1.getOutgoingRequestAttributesOnResponse)(req, response, this._semconvStability, this.getConfig().redactedQueryParams);
let status;
if (response.destroyed && !response.complete) {
status = { code: api_1.SpanStatusCode.ERROR };
}
else {
// behaves same for new and old semconv
status = {
code: (0, utils_1.parseResponseStatus)(400, response.statusCode),
};
}
span.setStatus(status);
span.setAttributes(attributes);
if (this._semconvStability & instrumentation_1.SemconvStability.OLD) {
const currentOldAttributes = spanDetails.oldMetricAttributes;
spanDetails.oldMetricAttributes = Object.assign(currentOldAttributes !== null && currentOldAttributes !== void 0 ? currentOldAttributes : {}, (0, utils_1.getOutgoingRequestMetricAttributesOnResponse)(attributes));
}
if (this._semconvStability & instrumentation_1.SemconvStability.STABLE) {
const currentStableAttributes = spanDetails.stableMetricAttributes;
spanDetails.stableMetricAttributes = Object.assign(currentStableAttributes !== null && currentStableAttributes !== void 0 ? currentStableAttributes : {}, (0, utils_1.getOutgoingStableRequestMetricAttributesOnResponse)(attributes));
}
this._callResponseHook(span, response);
this._headerCapture.client.captureRequestHeaders(span, (header) => req.getHeader(header));
this._headerCapture.client.captureResponseHeaders(span, (header) => response.headers[header]);
const applyCustomAttributesOnSpan = this.getConfig().applyCustomAttributesOnSpan;
if (applyCustomAttributesOnSpan !== undefined) {
(0, instrumentation_1.safeExecuteInTheMiddle)(() => applyCustomAttributesOnSpan(span, req, response), () => { }, true);
}
this._closeHttpSpan(req);
}
_onServerResponseFinish(request, response, span) {
if (!this.isEnabled()) {
return;
}
const attributes = (0, utils_1.getIncomingRequestAttributesOnResponse)(request, response, this._semconvStability);
const spanDetails = response[INSTRUMENTATION_SYMBOL];
if (spanDetails) {
if (this._semconvStability & instrumentation_1.SemconvStability.OLD) {
const currentOldAttributes = spanDetails.oldMetricAttributes;
spanDetails.oldMetricAttributes = Object.assign(currentOldAttributes !== null && currentOldAttributes !== void 0 ? currentOldAttributes : {}, (0, utils_1.getIncomingRequestMetricAttributesOnResponse)(attributes));
}
if (this._semconvStability & instrumentation_1.SemconvStability.STABLE) {
const currentStableAttributes = spanDetails.stableMetricAttributes;
spanDetails.stableMetricAttributes = Object.assign(currentStableAttributes !== null && currentStableAttributes !== void 0 ? currentStableAttributes : {}, (0, utils_1.getIncomingStableRequestMetricAttributesOnResponse)(attributes));
}
}
this._headerCapture.server.captureResponseHeaders(span, (header) => response.getHeader(header));
span.setAttributes(attributes).setStatus({
code: (0, utils_1.parseResponseStatus)(500, response.statusCode),
});
const route = attributes[semantic_conventions_1.ATTR_HTTP_ROUTE];
if (route) {
span.updateName(`${request.method || 'GET'} ${route}`);
}
const applyCustomAttributesOnSpan = this.getConfig().applyCustomAttributesOnSpan;
if (applyCustomAttributesOnSpan !== undefined) {
(0, instrumentation_1.safeExecuteInTheMiddle)(() => applyCustomAttributesOnSpan(span, request, response), () => { }, true);
}
this._closeHttpSpan(response);
}
_startHttpSpan(name, options, ctx = api_1.context.active()) {
/*
* If a parent is required but not present, we use a `NoopSpan` to still
* propagate context without recording it.
*/
const requireParent = options.kind === api_1.SpanKind.CLIENT
? this.getConfig().requireParentforOutgoingSpans
: this.getConfig().requireParentforIncomingSpans;
let span;
const currentSpan = api_1.trace.getSpan(ctx);
if (requireParent === true && currentSpan === undefined) {
span = api_1.trace.wrapSpanContext(api_1.INVALID_SPAN_CONTEXT);
}
else if (requireParent === true && (currentSpan === null || currentSpan === void 0 ? void 0 : currentSpan.spanContext().isRemote)) {
span = currentSpan;
}
else {
span = this.tracer.startSpan(name, options, ctx);
}
return span;
}
_closeHttpSpan(traced) {
var _a;
const spanDetails = traced[INSTRUMENTATION_SYMBOL];
if (!spanDetails) {
return;
}
(_a = spanDetails.span) === null || _a === void 0 ? void 0 : _a.end();
const { startTime, oldMetricAttributes, stableMetricAttributes, spanKind } = spanDetails;
if (startTime === undefined ||
spanKind === undefined ||
(oldMetricAttributes === undefined &&
stableMetricAttributes === undefined)) {
delete traced[INSTRUMENTATION_SYMBOL];
return;
}
// Record metrics
const duration = (0, core_1.hrTimeToMilliseconds)((0, core_1.hrTimeDuration)(startTime, (0, core_1.hrTime)()));
if (spanKind === api_1.SpanKind.SERVER) {
this._recordServerDuration(duration, oldMetricAttributes, stableMetricAttributes);
}
else if (spanKind === api_1.SpanKind.CLIENT) {
this._recordClientDuration(duration, oldMetricAttributes, stableMetricAttributes);
}
delete traced[INSTRUMENTATION_SYMBOL];
}
_callResponseHook(span, response) {
const hook = this.getConfig().responseHook;
if (hook === undefined) {
return;
}
(0, instrumentation_1.safeExecuteInTheMiddle)(() => hook(span, response), () => { }, true);
}
_callRequestHook(span, request) {
const hook = this.getConfig().requestHook;
if (hook === undefined) {
return;
}
(0, instrumentation_1.safeExecuteInTheMiddle)(() => hook(span, request), () => { }, true);
}
_callStartSpanHook(request, hookFunc) {
return (0, instrumentation_1.safeExecuteInTheMiddle)(() => hookFunc(request), () => { }, true);
}
_createHeaderCapture() {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
const config = this.getConfig();
return {
client: {
captureRequestHeaders: (0, utils_1.headerCapture)('request', (_c = (_b = (_a = config.headersToSpanAttributes) === null || _a === void 0 ? void 0 : _a.client) === null || _b === void 0 ? void 0 : _b.requestHeaders) !== null && _c !== void 0 ? _c : []),
captureResponseHeaders: (0, utils_1.headerCapture)('response', (_f = (_e = (_d = config.headersToSpanAttributes) === null || _d === void 0 ? void 0 : _d.client) === null || _e === void 0 ? void 0 : _e.responseHeaders) !== null && _f !== void 0 ? _f : []),
},
server: {
captureRequestHeaders: (0, utils_1.headerCapture)('request', (_j = (_h = (_g = config.headersToSpanAttributes) === null || _g === void 0 ? void 0 : _g.server) === null || _h === void 0 ? void 0 : _h.requestHeaders) !== null && _j !== void 0 ? _j : []),
captureResponseHeaders: (0, utils_1.headerCapture)('response', (_m = (_l = (_k = config.headersToSpanAttributes) === null || _k === void 0 ? void 0 : _k.server) === null || _l === void 0 ? void 0 : _l.responseHeaders) !== null && _m !== void 0 ? _m : []),
},
};
}
}
exports.HttpDcInstrumentation = HttpDcInstrumentation;
//# sourceMappingURL=httpdc.js.map