in lib/instrumentation/modules/undici.js [91:241]
function instrumentUndici(agent) {
if (isInstrumented) {
return;
}
isInstrumented = true;
const ins = agent._instrumentation;
spanFromReq = new WeakMap();
// Keep ref to avoid https://github.com/nodejs/node/issues/42170 bug and for
// unsubscribing.
chans = [];
function diagchSub(name, onMessage) {
const chan = diagch.channel(name);
chan.subscribe(onMessage);
chans.push({
name,
chan,
onMessage,
});
}
diagchSub('undici:request:create', ({ request }) => {
// We do not handle instrumenting HTTP CONNECT. See limitation notes above.
if (request.method === 'CONNECT') {
return;
}
const url = new URL(request.origin);
const span = ins.createSpan(
`${request.method} ${url.host}`,
'external',
'http',
request.method,
{ exitSpan: true },
);
// W3C trace-context propagation.
// If the span is null (e.g. hit `transactionMaxSpans`, unsampled
// transaction), then fallback to the current run context's span or
// transaction, if any.
const parentRunContext = ins.currRunContext();
const propSpan =
span || parentRunContext.currSpan() || parentRunContext.currTransaction();
if (propSpan) {
// Guard against adding a duplicate 'traceparent' header, because that
// breaks ES. https://github.com/elastic/apm-agent-nodejs/issues/3964
// Dev Note: This cheats a little and assumes the header names to add
// will include 'traceparent'.
let alreadyHasTp = false;
if (Array.isArray(request.headers)) {
// undici@6
for (let i = 0; i < request.headers.length; i += 2) {
if (request.headers[i].toLowerCase() === 'traceparent') {
alreadyHasTp = true;
break;
}
}
} else if (typeof request.headers === 'string') {
// undici@5
alreadyHasTp = headersStrHasTraceparentRe.test(request.headers);
}
if (!alreadyHasTp) {
propSpan.propagateTraceContextHeaders(
request,
function (req, name, value) {
if (typeof request.addHeader === 'function') {
req.addHeader(name, value);
} else if (Array.isArray(request.headers)) {
// undici@6.11.0 accidentally, briefly removed `request.addHeader()`.
req.headers.push(name, value);
}
},
);
}
}
if (span) {
spanFromReq.set(request, span);
// Set some initial HTTP context, in case the request errors out before a response.
span.setHttpContext({
method: request.method,
url: request.origin + request.path,
});
const destContext = {
address: url.hostname,
};
const port =
Number(url.port) ||
(url.protocol === 'https:' && 443) ||
(url.protocol === 'http:' && 80);
if (port) {
destContext.port = port;
}
span._setDestinationContext(destContext);
}
});
diagchSub('undici:request:headers', ({ request, response }) => {
const span = spanFromReq.get(request);
if (span !== undefined) {
// We are currently *not* capturing response headers, even though the
// intake API does allow it, because none of the other `setHttpContext`
// uses currently do.
const httpContext = {
method: request.method,
status_code: response.statusCode,
url: request.origin + request.path,
};
const cLen = contentLengthFromResponseHeaders(response.headers);
if (cLen !== null) {
httpContext.response = { encoded_body_size: cLen };
}
span.setHttpContext(httpContext);
span._setOutcomeFromHttpStatusCode(response.statusCode);
}
});
diagchSub('undici:request:trailers', ({ request }) => {
const span = spanFromReq.get(request);
if (span !== undefined) {
span.end();
spanFromReq.delete(request);
}
});
diagchSub('undici:request:error', ({ request, error }) => {
const span = spanFromReq.get(request);
const errOpts = {};
if (span !== undefined) {
errOpts.parent = span;
// Cases where we won't have an undici parent span:
// - We've hit transactionMaxSpans.
// - The undici HTTP span was suppressed because it is a child of an
// exit span (e.g. when used as the transport for the Elasticsearch
// client).
// It might be debatable whether we want to capture the error in the
// latter case. This could be revisited later.
}
agent.captureError(error, errOpts);
if (span !== undefined) {
span.end();
spanFromReq.delete(request);
}
});
}