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