function wrapRequest()

in lib/instrumentation/modules/@elastic/elasticsearch.js [130:342]


  function wrapRequest(origRequest) {
    return function wrappedRequest(params, options, cb) {
      options = options || {};
      if (typeof options === 'function') {
        cb = options;
        options = {};
      }

      if (typeof cb !== 'function' && doubleCallsRequestIfNoCb) {
        return origRequest.apply(this, arguments);
      }

      const method = (params && params.method) || '<UnknownMethod>';
      const path = (params && params.path) || '<UnknownPath>';
      agent.logger.debug(
        { method, path },
        'intercepted call to @elastic/elasticsearch.Transport.prototype.request',
      );
      const span = ins.createSpan(
        `Elasticsearch: ${method} ${path}`,
        'db',
        'elasticsearch',
        'request',
        { exitSpan: true },
      );
      if (!span) {
        return origRequest.apply(this, arguments);
      }

      const parentRunContext = ins.currRunContext();
      const spanRunContext = parentRunContext.enterSpan(span);
      const finish = ins.bindFunctionToRunContext(
        spanRunContext,
        (err, result) => {
          // Set destination context.
          // Use the connection from wrappedGetConnection() above, if that worked.
          // Otherwise, fallback to using the first connection on
          // `Transport#connectionPool`, if any.  (This is the best parsed
          // representation of connection options passed to the Client ctor.)
          let conn = connFromSpan.get(span);
          if (conn) {
            connFromSpan.delete(span);
          } else if (this.connectionPool && this.connectionPool.connections) {
            conn = this.connectionPool.connections[0];
          }
          const connUrl = conn && conn.url;
          span._setDestinationContext(
            getDBDestination(
              connUrl && connUrl.hostname,
              connUrl && connUrl.port,
            ),
          );

          // Gather some HTTP context.
          // We are *not* including the response headers b/c they are boring:
          //
          //    X-elastic-product: Elasticsearch
          //    content-type: application/json
          //    content-length: ...
          //
          // Getting the ES client request "DiagnosticResult" object has some edge cases:
          // - In v7 using a callback, we always get it as `result`.
          // - In v7 using a Promise, if the promise is rejected, then `result` is
          //   not passed.
          // - In v8, `result` only includes HTTP response info if `options.meta`
          //   is true. We use the diagnostic 'response' event instead.
          // - In v7, see the limitation note above for the rare start case where
          //   the diagnostic 'response' event may have the wrong currentSpan.
          // The result is that with Promise usage of v7, ES client requests that
          // are queued behind the "product-check" and that reject, won't have a
          // `diagResult`.
          const httpContext = {};
          let haveHttpContext = false;
          let diagResult = isGteV8 ? null : result;
          if (!diagResult) {
            diagResult = diagResultFromSpan.get(span);
            if (diagResult) {
              diagResultFromSpan.delete(span);
            }
          }
          if (diagResult) {
            if (diagResult.statusCode) {
              haveHttpContext = true;
              httpContext.status_code = diagResult.statusCode;
            }

            if (diagResult.headers && 'content-length' in diagResult.headers) {
              const contentLength = Number(
                diagResult.headers['content-length'],
              );
              if (!isNaN(contentLength)) {
                haveHttpContext = true;
                httpContext.response = { encoded_body_size: contentLength };
              }
            }
          }

          // Reconstruct the full URL (including query params).
          let origin;
          if (connUrl) {
            origin = connUrl.origin;
          } else if (
            diagResult &&
            diagResult.meta &&
            diagResult.meta.connection &&
            diagResult.meta.connection.url
          ) {
            try {
              origin = new URL(diagResult.meta.connection.url).origin;
            } catch (_ignoredErr) {}
          }
          if (origin && params && params.path) {
            const fullUrl = new URL(origin);
            fullUrl.pathname = params.path;
            fullUrl.search = new URLSearchParams(params.querystring).toString();
            httpContext.url = fullUrl.toString();
            haveHttpContext = true;
          }

          if (haveHttpContext) {
            span.setHttpContext(httpContext);
          }

          // Set DB context.
          const dbContext = {
            type: 'elasticsearch',
          };
          if (params) {
            const statement = getElasticsearchDbStatement(
              params.path,
              params.body || params.bulkBody,
              elasticsearchCaptureBodyUrlsRegExp,
            );
            if (statement) {
              dbContext.statement = statement;
            }
          }
          const clusterName = getESClusterName(diagResult);
          if (clusterName) {
            dbContext.instance = clusterName;
          }
          span.setDbContext(dbContext);

          if (err) {
            // Error properties are specified here:
            // https://github.com/elastic/elasticsearch-js/blob/master/lib/errors.d.ts
            // - We capture some data from ResponseError, which is for
            //   Elasticsearch API errors:
            //   https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#common-options-error-options
            // - Otherwise we explicitly turn off `captureAttributes` to avoid
            //   grabbing potentially large and sensitive properties like
            //   `err.data` on DeserializationError.
            const errOpts = {
              captureAttributes: false,
            };
            const errBody = err.body;
            if (err.name === 'ResponseError' && errBody && errBody.error) {
              // Include some data from the Elasticsearch API response body:
              // https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#common-options-error-options
              const errType = errBody.error.type;
              if (errType) {
                // Specialize `error.exception.type` for better error grouping.
                errOpts.exceptionType = `ResponseError (${errType})`;
              }
              errOpts.custom = {
                type: errType,
                reason: errBody.error.reason,
                status: errBody.status,
              };
              if (errBody.error.caused_by) {
                errOpts.custom.caused_by = errBody.error.caused_by;
              }
            }
            agent.captureError(err, errOpts);
          }

          span.end();
        },
      );

      if (typeof cb === 'function') {
        const wrappedCb = (err, result) => {
          finish(err, result);
          ins.withRunContext(parentRunContext, cb, this, err, result);
        };
        return ins.withRunContext(
          spanRunContext,
          origRequest,
          this,
          params,
          options,
          wrappedCb,
        );
      } else {
        const origPromise = ins.withRunContext(
          spanRunContext,
          origRequest,
          this,
          ...arguments,
        );
        origPromise.then(
          function onResolve(result) {
            finish(null, result);
          },
          function onReject(err) {
            finish(err, null);
          },
        );

        return origPromise;
      }
    };
  }