function elasticApmAwsLambda()

in lib/lambda.js [471:805]


function elasticApmAwsLambda(agent) {
  const log = agent.logger;
  const ins = agent._instrumentation;

  /**
   * Register this transaction with the Lambda extension, if possible.  This
   * function is `await`able so that the transaction is registered before
   * executing the user's Lambda handler.
   *
   * Perf note: Using a Lambda sized to have 1 vCPU (1769MB memory), some
   * rudimentary perf tests showed an average of 0.8ms for this call to the ext.
   */
  function registerTransaction(trans, awsRequestId) {
    if (!agent._apmClient) {
      return;
    }
    if (!agent._apmClient.lambdaShouldRegisterTransactions()) {
      return;
    }

    // Reproduce the filtering logic from `Instrumentation.prototype.addEndedTransaction`.
    if (agent._conf.contextPropagationOnly) {
      return;
    }
    if (
      !trans.sampled &&
      !agent._apmClient.supportsKeepingUnsampledTransaction()
    ) {
      return;
    }

    var payload = trans.toJSON();
    // If this partial transaction is used, the Lambda Extension will fill in:
    // - `transaction.result` will be set to one of:
    //    - The "status" field from the Logs API platform `runtimeDone` message.
    //      https://docs.aws.amazon.com/lambda/latest/dg/runtimes-logs-api.html#runtimes-logs-api-ref-done
    //      Values: "success", "failure"
    //    - The "shutdownReason" field from the `Shutdown` event from the Extensions API.
    //      https://docs.aws.amazon.com/lambda/latest/dg/runtimes-extensions-api.html#runtimes-lifecycle-shutdown
    //      Values: "spindown", "timeout", "failure"   (I think these are the values.)
    // - `transaction.outcome` will be set to "failure" if the status above is
    //   not "success". Therefore we want a default outcome value.
    // - `transaction.duration` will be estimated
    delete payload.result;
    delete payload.duration;

    payload = agent._transactionFilters.process(payload);
    if (!payload) {
      log.trace(
        { traceId: trans.traceId, transactionId: trans.id },
        'transaction ignored by filter',
      );
      return;
    }

    return agent._apmClient.lambdaRegisterTransaction(payload, awsRequestId);
  }

  function endAndFlushTransaction(
    err,
    result,
    trans,
    event,
    context,
    triggerType,
    cb,
  ) {
    log.trace(
      { awsRequestId: context && context.awsRequestId },
      'lambda: fn end',
    );

    switch (triggerType) {
      case TRIGGER_API_GATEWAY:
        setTransDataFromApiGatewayResult(err, result, trans, event);
        break;
      case TRIGGER_ELB:
        setTransDataFromElbResult(err, result, trans);
        break;
      default:
        if (err) {
          trans.result = constants.RESULT_FAILURE;
          trans.setOutcome(constants.OUTCOME_FAILURE);
        } else {
          trans.result = constants.RESULT_SUCCESS;
          trans.setOutcome(constants.OUTCOME_SUCCESS);
        }
        break;
    }

    if (err) {
      // Capture the error before trans.end() so it associates with the
      // current trans.  `skipOutcome` to avoid setting outcome on a possible
      // currentSpan, because this error applies to the transaction, not any
      // sub-span.
      agent.captureError(err, { skipOutcome: true });
    }

    trans.end();

    agent._flush({ lambdaEnd: true, inflightTimeout: 100 }, (flushErr) => {
      if (flushErr) {
        log.error(
          { err: flushErr, awsRequestId: context && context.awsRequestId },
          'lambda: flush error',
        );
      }
      log.trace(
        { awsRequestId: context && context.awsRequestId },
        'lambda: wrapper end',
      );
      cb();
    });
  }

  function wrapContext(runContext, trans, event, context, triggerType) {
    shimmer.wrap(context, 'succeed', (origSucceed) => {
      return ins.bindFunctionToRunContext(
        runContext,
        function wrappedSucceed(result) {
          endAndFlushTransaction(
            null,
            result,
            trans,
            event,
            context,
            triggerType,
            function () {
              origSucceed(result);
            },
          );
        },
      );
    });

    shimmer.wrap(context, 'fail', (origFail) => {
      return ins.bindFunctionToRunContext(
        runContext,
        function wrappedFail(err) {
          endAndFlushTransaction(
            err,
            null,
            trans,
            event,
            context,
            triggerType,
            function () {
              origFail(err);
            },
          );
        },
      );
    });

    shimmer.wrap(context, 'done', (origDone) => {
      return wrapLambdaCallback(
        runContext,
        trans,
        event,
        context,
        triggerType,
        origDone,
      );
    });
  }

  function wrapLambdaCallback(
    runContext,
    trans,
    event,
    context,
    triggerType,
    callback,
  ) {
    return ins.bindFunctionToRunContext(
      runContext,
      function wrappedLambdaCallback(err, result) {
        endAndFlushTransaction(
          err,
          result,
          trans,
          event,
          context,
          triggerType,
          () => {
            callback(err, result);
          },
        );
      },
    );
  }

  return function wrapLambdaHandler(type, fn) {
    if (typeof type === 'function') {
      fn = type;
      type = 'request';
    }
    if (!agent._conf.active) {
      // Manual usage of `apm.lambda(...)` should be a no-op when not active.
      return fn;
    }

    return async function wrappedLambdaHandler(event, context, callback) {
      if (!(event && context && typeof callback === 'function')) {
        // Skip instrumentation if arguments are unexpected.
        // https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html
        return fn.call(this, ...arguments);
      }
      log.trace({ awsRequestId: context.awsRequestId }, 'lambda: fn start');

      const isColdStart = isFirstRun;
      if (isFirstRun) {
        isFirstRun = false;

        // E.g. 'arn:aws:lambda:us-west-2:123456789012:function:my-function:someAlias'
        const arnParts = context.invokedFunctionArn.split(':');
        gFaasId = arnParts.slice(0, 7).join(':');
        const cloudAccountId = arnParts[4];

        if (agent._apmClient) {
          log.trace(
            { awsRequestId: context.awsRequestId },
            'lambda: setExtraMetadata',
          );
          agent._apmClient.setExtraMetadata(getMetadata(agent, cloudAccountId));
        }
      }

      if (agent._apmClient) {
        agent._apmClient.lambdaStart();
      }

      const triggerType = triggerTypeFromEvent(event);

      // Look for trace-context info in headers or messageAttributes.
      let traceparent;
      let tracestate;
      if (
        (triggerType === TRIGGER_API_GATEWAY || triggerType === TRIGGER_ELB) &&
        event.headers
      ) {
        // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
        // says "Header names are lowercased." However, that isn't the case for
        // payload format version 1.0. We need lowercased headers for processing.
        if (!event.requestContext.http) {
          // 1.0
          event.normedHeaders = lowerCaseObjectKeys(event.headers);
        } else {
          event.normedHeaders = event.headers;
        }
        traceparent =
          event.normedHeaders.traceparent ||
          event.normedHeaders['elastic-apm-traceparent'];
        tracestate = event.normedHeaders.tracestate;
      }

      // Start the transaction and set some possibly trigger-specific data.
      const trans = agent.startTransaction(context.functionName, type, {
        childOf: traceparent,
        tracestate,
      });
      switch (triggerType) {
        case TRIGGER_API_GATEWAY:
          setApiGatewayData(agent, trans, event, context, gFaasId, isColdStart);
          break;
        case TRIGGER_ELB:
          setElbData(agent, trans, event, context, gFaasId, isColdStart);
          break;
        case TRIGGER_SQS:
          setSqsData(agent, trans, event, context, gFaasId, isColdStart);
          break;
        case TRIGGER_SNS:
          setSnsData(agent, trans, event, context, gFaasId, isColdStart);
          break;
        case TRIGGER_S3_SINGLE_EVENT:
          setS3SingleData(trans, event, context, gFaasId, isColdStart);
          break;
        case TRIGGER_GENERIC:
          setGenericData(trans, event, context, gFaasId, isColdStart);
          break;
        default:
          log.warn(
            `not setting transaction data for triggerType=${triggerType}`,
          );
      }

      // Wrap context and callback to finish and send transaction.
      // Note: Wrapping context needs to happen *before any `await` calls* in
      // this function, otherwise the Lambda Node.js Runtime will call the
      // *unwrapped* `context.{succeed,fail,done}()` methods.
      const transRunContext = ins.currRunContext();
      wrapContext(transRunContext, trans, event, context, triggerType);
      const wrappedCallback = wrapLambdaCallback(
        transRunContext,
        trans,
        event,
        context,
        triggerType,
        callback,
      );

      await registerTransaction(trans, context.awsRequestId);

      try {
        const retval = ins.withRunContext(
          transRunContext,
          fn,
          this,
          event,
          context,
          wrappedCallback,
        );
        if (retval instanceof Promise) {
          return retval;
        } else {
          // In this case, our wrapping of the user's handler has changed it
          // from a sync function to an async function. We need to ensure the
          // Lambda Runtime does not end the invocation based on this returned
          // promise -- the invocation should end when the `callback` is called
          // -- so we return a promise that never resolves.
          return new Promise((resolve, reject) => {
            /* never resolves */
          });
        }
      } catch (handlerErr) {
        wrappedCallback(handlerErr);
        // Return a promise that never resolves, so that the Lambda Runtime's
        // doesn't attempt its "success" handling.
        return new Promise((resolve, reject) => {
          /* never resolves */
        });
      }
    };
  };
}