exports.traceOutgoingRequest = function()

in lib/instrumentation/http-shared.js [204:378]


exports.traceOutgoingRequest = function (agent, moduleName, method) {
  var ins = agent._instrumentation;

  return function wrapHttpRequest(orig) {
    return function wrappedHttpRequest(input, options, cb) {
      const parentRunContext = ins.currRunContext();
      var span = ins.createSpan(null, 'external', 'http', { exitSpan: true });
      var id = span && span.transaction.id;
      agent.logger.debug('intercepted call to %s.%s %o', moduleName, method, {
        id,
      });

      // Reproduce the argument handling from node/lib/_http_client.js#ClientRequest().
      //
      // The `new URL(...)` calls in this block *could* throw INVALID_URL, but
      // that would happen anyway when calling `orig(...)`. The only slight
      // downside is that the Error stack won't originate inside "_http_client.js".
      if (!nodeHttpRequestSupportsSeparateUrlArg) {
        // Signature from node <10.9.0:
        //    http.request(options[, callback])
        //        options <Object> | <string> | <URL>
        cb = options;
        options = input;
        if (typeof options === 'string') {
          options = safeUrlToHttpOptions(new URL(options));
        } else if (options instanceof URL) {
          options = safeUrlToHttpOptions(options);
        } else {
          options = Object.assign({}, options);
        }
      } else {
        // Signature from node >=10.9.0:
        //    http.request(options[, callback])
        //    http.request(url[, options][, callback])
        //        url <string> | <URL>
        //        options <Object>
        if (typeof input === 'string') {
          input = safeUrlToHttpOptions(new URL(input));
        } else if (input instanceof URL) {
          input = safeUrlToHttpOptions(input);
        } else {
          cb = options;
          options = input;
          input = null;
        }

        if (typeof options === 'function') {
          cb = options;
          options = input || {};
        } else {
          options = Object.assign(input || {}, options);
        }
      }

      const newArgs = [options];
      if (cb !== undefined) {
        if (typeof cb === 'function') {
          newArgs.push(ins.bindFunctionToRunContext(parentRunContext, cb));
        } else {
          newArgs.push(cb);
        }
      }

      // W3C trace-context propagation.
      // There are a number of reasons why `span` might be null: child of an
      // exit span, `transactionMaxSpans` was hit, unsampled transaction, etc.
      // If so, then fallback to the current run context's span or transaction,
      // if any.
      const parent =
        span ||
        parentRunContext.currSpan() ||
        parentRunContext.currTransaction();
      if (parent) {
        const headers = Object.assign({}, options.headers);
        parent.propagateTraceContextHeaders(
          headers,
          function (carrier, name, value) {
            carrier[name] = value;
          },
        );
        options.headers = headers;
      }

      if (!span) {
        return orig.apply(this, newArgs);
      }

      const spanRunContext = parentRunContext.enterSpan(span);
      var req = ins.withRunContext(spanRunContext, orig, this, ...newArgs);

      var protocol = req.agent && req.agent.protocol;
      agent.logger.debug('request details: %o', {
        protocol,
        host: getSafeHost(req),
        id,
      });

      ins.bindEmitter(req);

      span.action = req.method;
      span.name = req.method + ' ' + getSafeHost(req);

      // TODO: Research if it's possible to add this to the prototype instead.
      // Or if it's somehow preferable to listen for when a `response` listener
      // is added instead of when `response` is emitted.
      const emit = req.emit;
      req.emit = function wrappedEmit(type, res) {
        if (type === 'response') onResponse(res);
        if (type === 'abort') onAbort(type);
        return emit.apply(req, arguments);
      };

      const url = getUrlFromRequestAndOptions(req, options, moduleName + ':');
      if (!url) {
        agent.logger.warn('unable to identify http.ClientRequest url %o', {
          id,
        });
      }
      let statusCode;
      return req;

      // In case the request is ended prematurely
      function onAbort(type) {
        if (span.ended) return;
        agent.logger.debug('intercepted http.ClientRequest abort event %o', {
          id,
        });

        onEnd();
      }

      function onEnd() {
        span.setHttpContext({
          method: req.method,
          status_code: statusCode,
          url,
        });

        // Add destination info only when socket conn is established
        if (url) {
          // The `getHTTPDestination` function might throw in case an
          // invalid URL is given to the `URL()` function. Until we can
          // be 100% sure this doesn't happen, we better catch it here.
          // For details, see:
          // https://github.com/elastic/apm-agent-nodejs/issues/1769
          try {
            span._setDestinationContext(getHTTPDestination(url));
          } catch (e) {
            agent.logger.error(
              'Could not set destination context: %s',
              e.message,
            );
          }
        }

        span._setOutcomeFromHttpStatusCode(statusCode);
        span.end();
      }

      function onResponse(res) {
        agent.logger.debug('intercepted http.ClientRequest response event %o', {
          id,
        });
        ins.bindEmitterToRunContext(parentRunContext, res);
        statusCode = res.statusCode;
        res.prependListener('end', function () {
          agent.logger.debug('intercepted http.IncomingMessage end event %o', {
            id,
          });
          onEnd();
        });
      }
    };
  };
};