UNPKG

@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
"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