function instrumentUndici()

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);
    }
  });
}