Files
tasq/node_modules/@opentelemetry/instrumentation-http/build/src/http.js
T
2026-04-09 19:01:53 +08:00

491 lines
24 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpInstrumentation = void 0;
/*
* 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.
*/
const api_1 = require("@opentelemetry/api");
const core_1 = require("@opentelemetry/core");
const semver = require("semver");
const url = require("url");
const utils = require("./utils");
const version_1 = require("./version");
const instrumentation_1 = require("@opentelemetry/instrumentation");
const core_2 = require("@opentelemetry/core");
const events_1 = require("events");
const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
/**
* Http instrumentation instrumentation for Opentelemetry
*/
class HttpInstrumentation extends instrumentation_1.InstrumentationBase {
constructor(config = {}) {
super('@opentelemetry/instrumentation-http', version_1.VERSION, config);
/** keep track on spans not ended */
this._spanNotEnded = new WeakSet();
this._headerCapture = this._createHeaderCapture();
}
_updateMetricInstruments() {
this._httpServerDurationHistogram = this.meter.createHistogram('http.server.duration', {
description: 'Measures the duration of inbound HTTP requests.',
unit: 'ms',
valueType: api_1.ValueType.DOUBLE,
});
this._httpClientDurationHistogram = this.meter.createHistogram('http.client.duration', {
description: 'Measures the duration of outbound HTTP requests.',
unit: 'ms',
valueType: api_1.ValueType.DOUBLE,
});
}
setConfig(config = {}) {
super.setConfig(config);
this._headerCapture = this._createHeaderCapture();
}
init() {
return [this._getHttpsInstrumentation(), this._getHttpInstrumentation()];
}
_getHttpInstrumentation() {
return new instrumentation_1.InstrumentationNodeModuleDefinition('http', ['*'], (moduleExports) => {
this._wrap(moduleExports, 'request', this._getPatchOutgoingRequestFunction('http'));
this._wrap(moduleExports, 'get', this._getPatchOutgoingGetFunction(moduleExports.request));
this._wrap(moduleExports.Server.prototype, 'emit', this._getPatchIncomingRequestFunction('http'));
return moduleExports;
}, (moduleExports) => {
if (moduleExports === undefined)
return;
this._unwrap(moduleExports, 'request');
this._unwrap(moduleExports, 'get');
this._unwrap(moduleExports.Server.prototype, 'emit');
});
}
_getHttpsInstrumentation() {
return new instrumentation_1.InstrumentationNodeModuleDefinition('https', ['*'], (moduleExports) => {
this._wrap(moduleExports, 'request', this._getPatchHttpsOutgoingRequestFunction('https'));
this._wrap(moduleExports, 'get', this._getPatchHttpsOutgoingGetFunction(moduleExports.request));
this._wrap(moduleExports.Server.prototype, 'emit', this._getPatchIncomingRequestFunction('https'));
return moduleExports;
}, (moduleExports) => {
if (moduleExports === undefined)
return;
this._unwrap(moduleExports, 'request');
this._unwrap(moduleExports, 'get');
this._unwrap(moduleExports.Server.prototype, 'emit');
});
}
/**
* Creates spans for incoming requests, restoring spans' context if applied.
*/
_getPatchIncomingRequestFunction(component) {
return (original) => {
return this._incomingRequestFunction(component, original);
};
}
/**
* Creates spans for outgoing requests, sending spans' context for distributed
* tracing.
*/
_getPatchOutgoingRequestFunction(component) {
return (original) => {
return this._outgoingRequestFunction(component, original);
};
}
_getPatchOutgoingGetFunction(clientRequest) {
return (_original) => {
// Re-implement http.get. This needs to be done (instead of using
// getPatchOutgoingRequestFunction to patch it) because we need to
// set the trace context header before the returned http.ClientRequest is
// ended. The Node.js docs state that the only differences between
// request and get are that (1) get defaults to the HTTP GET method and
// (2) the returned request object is ended immediately. The former is
// already true (at least in supported Node versions up to v10), so we
// simply follow the latter. Ref:
// https://nodejs.org/dist/latest/docs/api/http.html#http_http_get_options_callback
// https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/instrumentations/instrumentation-http.ts#L198
return function outgoingGetRequest(options, ...args) {
const req = clientRequest(options, ...args);
req.end();
return req;
};
};
}
/** Patches HTTPS outgoing requests */
_getPatchHttpsOutgoingRequestFunction(component) {
return (original) => {
const instrumentation = this;
return function httpsOutgoingRequest(
// eslint-disable-next-line node/no-unsupported-features/node-builtins
options, ...args) {
var _a;
// Makes sure options will have default HTTPS parameters
if (component === 'https' &&
typeof options === 'object' &&
((_a = options === null || options === void 0 ? void 0 : options.constructor) === null || _a === void 0 ? void 0 : _a.name) !== 'URL') {
options = Object.assign({}, options);
instrumentation._setDefaultOptions(options);
}
return instrumentation._getPatchOutgoingRequestFunction(component)(original)(options, ...args);
};
};
}
_setDefaultOptions(options) {
options.protocol = options.protocol || 'https:';
options.port = options.port || 443;
}
/** Patches HTTPS outgoing get requests */
_getPatchHttpsOutgoingGetFunction(clientRequest) {
return (original) => {
const instrumentation = this;
return function httpsOutgoingRequest(
// eslint-disable-next-line node/no-unsupported-features/node-builtins
options, ...args) {
return instrumentation._getPatchOutgoingGetFunction(clientRequest)(original)(options, ...args);
};
};
}
/**
* Attach event listeners to a client request to end span and add span attributes.
*
* @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 metricAttributes metric attributes
*/
_traceClientRequest(request, span, startTime, metricAttributes) {
if (this.getConfig().requestHook) {
this._callRequestHook(span, request);
}
/**
* Determines if the request has errored or the response has ended/errored.
*/
let responseFinished = false;
/*
* User 'response' event listeners can be added before our listener,
* force our listener to be the first, so response emitter is bound
* before any user listeners are added to it.
*/
request.prependListener('response', (response) => {
this._diag.debug('outgoingRequest on response()');
if (request.listenerCount('response') <= 1) {
response.resume();
}
const responseAttributes = utils.getOutgoingRequestAttributesOnResponse(response);
span.setAttributes(responseAttributes);
metricAttributes = Object.assign(metricAttributes, utils.getOutgoingRequestMetricAttributesOnResponse(responseAttributes));
if (this.getConfig().responseHook) {
this._callResponseHook(span, response);
}
this._headerCapture.client.captureRequestHeaders(span, header => request.getHeader(header));
this._headerCapture.client.captureResponseHeaders(span, header => response.headers[header]);
api_1.context.bind(api_1.context.active(), response);
const endHandler = () => {
this._diag.debug('outgoingRequest on end()');
if (responseFinished) {
return;
}
responseFinished = true;
let status;
if (response.aborted && !response.complete) {
status = { code: api_1.SpanStatusCode.ERROR };
}
else {
status = {
code: utils.parseResponseStatus(api_1.SpanKind.CLIENT, response.statusCode),
};
}
span.setStatus(status);
if (this.getConfig().applyCustomAttributesOnSpan) {
(0, instrumentation_1.safeExecuteInTheMiddle)(() => this.getConfig().applyCustomAttributesOnSpan(span, request, response), () => { }, true);
}
this._closeHttpSpan(span, api_1.SpanKind.CLIENT, startTime, metricAttributes);
};
response.on('end', endHandler);
// See https://github.com/open-telemetry/opentelemetry-js/pull/3625#issuecomment-1475673533
if (semver.lt(process.version, '16.0.0')) {
response.on('close', endHandler);
}
response.on(events_1.errorMonitor, (error) => {
this._diag.debug('outgoingRequest on error()', error);
if (responseFinished) {
return;
}
responseFinished = true;
utils.setSpanWithError(span, error);
span.setStatus({
code: api_1.SpanStatusCode.ERROR,
message: error.message,
});
this._closeHttpSpan(span, api_1.SpanKind.CLIENT, startTime, metricAttributes);
});
});
request.on('close', () => {
this._diag.debug('outgoingRequest on request close()');
if (request.aborted || responseFinished) {
return;
}
responseFinished = true;
this._closeHttpSpan(span, api_1.SpanKind.CLIENT, startTime, metricAttributes);
});
request.on(events_1.errorMonitor, (error) => {
this._diag.debug('outgoingRequest on request error()', error);
if (responseFinished) {
return;
}
responseFinished = true;
utils.setSpanWithError(span, error);
this._closeHttpSpan(span, api_1.SpanKind.CLIENT, startTime, metricAttributes);
});
this._diag.debug('http.ClientRequest return request');
return request;
}
_incomingRequestFunction(component, original) {
const instrumentation = this;
return function incomingRequest(event, ...args) {
// Only traces request events
if (event !== 'request') {
return original.apply(this, [event, ...args]);
}
const request = args[0];
const response = args[1];
const pathname = request.url
? url.parse(request.url).pathname || '/'
: '/';
const method = request.method || 'GET';
instrumentation._diag.debug(`${component} instrumentation incomingRequest`);
if (utils.isIgnored(pathname, instrumentation.getConfig().ignoreIncomingPaths, (e) => instrumentation._diag.error('caught ignoreIncomingPaths error: ', e)) ||
(0, instrumentation_1.safeExecuteInTheMiddle)(() => { var _a, _b; return (_b = (_a = instrumentation.getConfig()).ignoreIncomingRequestHook) === null || _b === void 0 ? void 0 : _b.call(_a, request); }, (e) => {
if (e != null) {
instrumentation._diag.error('caught ignoreIncomingRequestHook error: ', e);
}
}, true)) {
return api_1.context.with((0, core_1.suppressTracing)(api_1.context.active()), () => {
api_1.context.bind(api_1.context.active(), request);
api_1.context.bind(api_1.context.active(), response);
return original.apply(this, [event, ...args]);
});
}
const headers = request.headers;
const spanAttributes = utils.getIncomingRequestAttributes(request, {
component: component,
serverName: instrumentation.getConfig().serverName,
hookAttributes: instrumentation._callStartSpanHook(request, instrumentation.getConfig().startIncomingSpanHook),
});
const spanOptions = {
kind: api_1.SpanKind.SERVER,
attributes: spanAttributes,
};
const startTime = (0, core_1.hrTime)();
const metricAttributes = utils.getIncomingRequestMetricAttributes(spanAttributes);
const ctx = api_1.propagation.extract(api_1.ROOT_CONTEXT, headers);
const span = instrumentation._startHttpSpan(method, spanOptions, ctx);
const rpcMetadata = {
type: core_2.RPCType.HTTP,
span,
};
return api_1.context.with((0, core_2.setRPCMetadata)(api_1.trace.setSpan(ctx, span), rpcMetadata), () => {
api_1.context.bind(api_1.context.active(), request);
api_1.context.bind(api_1.context.active(), response);
if (instrumentation.getConfig().requestHook) {
instrumentation._callRequestHook(span, request);
}
if (instrumentation.getConfig().responseHook) {
instrumentation._callResponseHook(span, response);
}
instrumentation._headerCapture.server.captureRequestHeaders(span, header => request.headers[header]);
// After 'error', no further events other than 'close' should be emitted.
let hasError = false;
response.on('close', () => {
if (hasError) {
return;
}
instrumentation._onServerResponseFinish(request, response, span, metricAttributes, startTime);
});
response.on(events_1.errorMonitor, (err) => {
hasError = true;
instrumentation._onServerResponseError(span, metricAttributes, startTime, err);
});
return (0, instrumentation_1.safeExecuteInTheMiddle)(() => original.apply(this, [event, ...args]), error => {
if (error) {
utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(span, api_1.SpanKind.SERVER, startTime, metricAttributes);
throw error;
}
});
});
};
}
_outgoingRequestFunction(component, original) {
const instrumentation = this;
return function outgoingRequest(options, ...args) {
if (!utils.isValidOptionsType(options)) {
return original.apply(this, [options, ...args]);
}
const extraOptions = typeof args[0] === 'object' &&
(typeof options === 'string' || options instanceof url.URL)
? args.shift()
: undefined;
const { origin, pathname, method, optionsParsed } = utils.getRequestInfo(options, extraOptions);
/**
* Node 8's https module directly call the http one so to avoid creating
* 2 span for the same request we need to check that the protocol is correct
* See: https://github.com/nodejs/node/blob/v8.17.0/lib/https.js#L245
*/
if (component === 'http' &&
semver.lt(process.version, '9.0.0') &&
optionsParsed.protocol === 'https:') {
return original.apply(this, [optionsParsed, ...args]);
}
if (utils.isIgnored(origin + pathname, instrumentation.getConfig().ignoreOutgoingUrls, (e) => instrumentation._diag.error('caught ignoreOutgoingUrls error: ', e)) ||
(0, instrumentation_1.safeExecuteInTheMiddle)(() => {
var _a, _b;
return (_b = (_a = instrumentation
.getConfig()).ignoreOutgoingRequestHook) === null || _b === void 0 ? void 0 : _b.call(_a, optionsParsed);
}, (e) => {
if (e != null) {
instrumentation._diag.error('caught ignoreOutgoingRequestHook error: ', e);
}
}, true)) {
return original.apply(this, [optionsParsed, ...args]);
}
const { hostname, port } = utils.extractHostnameAndPort(optionsParsed);
const attributes = utils.getOutgoingRequestAttributes(optionsParsed, {
component,
port,
hostname,
hookAttributes: instrumentation._callStartSpanHook(optionsParsed, instrumentation.getConfig().startOutgoingSpanHook),
});
const startTime = (0, core_1.hrTime)();
const metricAttributes = utils.getOutgoingRequestMetricAttributes(attributes);
const spanOptions = {
kind: api_1.SpanKind.CLIENT,
attributes,
};
const span = instrumentation._startHttpSpan(method, spanOptions);
const parentContext = api_1.context.active();
const requestContext = api_1.trace.setSpan(parentContext, span);
if (!optionsParsed.headers) {
optionsParsed.headers = {};
}
else {
// Make a copy of the headers object to avoid mutating an object the
// caller might have a reference to.
optionsParsed.headers = Object.assign({}, optionsParsed.headers);
}
api_1.propagation.inject(requestContext, optionsParsed.headers);
return api_1.context.with(requestContext, () => {
/*
* The response callback is registered before ClientRequest is bound,
* thus it is needed to bind it before the function call.
*/
const cb = args[args.length - 1];
if (typeof cb === 'function') {
args[args.length - 1] = api_1.context.bind(parentContext, cb);
}
const request = (0, instrumentation_1.safeExecuteInTheMiddle)(() => original.apply(this, [optionsParsed, ...args]), error => {
if (error) {
utils.setSpanWithError(span, error);
instrumentation._closeHttpSpan(span, api_1.SpanKind.CLIENT, startTime, metricAttributes);
throw error;
}
});
instrumentation._diag.debug(`${component} instrumentation outgoingRequest`);
api_1.context.bind(parentContext, request);
return instrumentation._traceClientRequest(request, span, startTime, metricAttributes);
});
};
}
_onServerResponseFinish(request, response, span, metricAttributes, startTime) {
const attributes = utils.getIncomingRequestAttributesOnResponse(request, response);
metricAttributes = Object.assign(metricAttributes, utils.getIncomingRequestMetricAttributesOnResponse(attributes));
this._headerCapture.server.captureResponseHeaders(span, header => response.getHeader(header));
span.setAttributes(attributes).setStatus({
code: utils.parseResponseStatus(api_1.SpanKind.SERVER, response.statusCode),
});
const route = attributes[semantic_conventions_1.SEMATTRS_HTTP_ROUTE];
if (route) {
span.updateName(`${request.method || 'GET'} ${route}`);
}
if (this.getConfig().applyCustomAttributesOnSpan) {
(0, instrumentation_1.safeExecuteInTheMiddle)(() => this.getConfig().applyCustomAttributesOnSpan(span, request, response), () => { }, true);
}
this._closeHttpSpan(span, api_1.SpanKind.SERVER, startTime, metricAttributes);
}
_onServerResponseError(span, metricAttributes, startTime, error) {
utils.setSpanWithError(span, error);
this._closeHttpSpan(span, api_1.SpanKind.SERVER, startTime, metricAttributes);
}
_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);
}
this._spanNotEnded.add(span);
return span;
}
_closeHttpSpan(span, spanKind, startTime, metricAttributes) {
if (!this._spanNotEnded.has(span)) {
return;
}
span.end();
this._spanNotEnded.delete(span);
// Record metrics
const duration = (0, core_1.hrTimeToMilliseconds)((0, core_1.hrTimeDuration)(startTime, (0, core_1.hrTime)()));
if (spanKind === api_1.SpanKind.SERVER) {
this._httpServerDurationHistogram.record(duration, metricAttributes);
}
else if (spanKind === api_1.SpanKind.CLIENT) {
this._httpClientDurationHistogram.record(duration, metricAttributes);
}
}
_callResponseHook(span, response) {
(0, instrumentation_1.safeExecuteInTheMiddle)(() => this.getConfig().responseHook(span, response), () => { }, true);
}
_callRequestHook(span, request) {
(0, instrumentation_1.safeExecuteInTheMiddle)(() => this.getConfig().requestHook(span, request), () => { }, true);
}
_callStartSpanHook(request, hookFunc) {
if (typeof hookFunc === 'function') {
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: utils.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: utils.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: utils.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: utils.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.HttpInstrumentation = HttpInstrumentation;
//# sourceMappingURL=http.js.map