elastic-apm-node
Version:
The official Elastic APM agent for Node.js
993 lines (908 loc) • 29.8 kB
JavaScript
/*
* Copyright Elasticsearch B.V. and other contributors where applicable.
* Licensed under the BSD 2-Clause License; you may not use this file except in
* compliance with the BSD 2-Clause License.
*/
;
const constants = require('./constants');
const shimmer = require('./instrumentation/shimmer');
const fs = require('fs');
const path = require('path');
const querystring = require('querystring');
const { MAX_MESSAGES_PROCESSED_FOR_TRACE_CONTEXT } = require('./constants');
// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-aws-lambda.md#deriving-cold-starts
let isFirstRun = true;
let gFaasId; // Set on first invocation.
// The trigger types for which we support special handling.
// https://docs.aws.amazon.com/lambda/latest/dg/lambda-services.html
const TRIGGER_GENERIC = 1;
const TRIGGER_API_GATEWAY = 2; // This includes Lambda URLs which use the same event format.
const TRIGGER_SNS = 3;
const TRIGGER_SQS = 4;
const TRIGGER_S3_SINGLE_EVENT = 5;
const TRIGGER_ELB = 6; // Elastic Load Balancer, aka Application Load Balancer
function triggerTypeFromEvent(event) {
if (event.requestContext) {
if (event.requestContext.elb) {
return TRIGGER_ELB;
} else if (event.requestContext.requestId) {
return TRIGGER_API_GATEWAY;
}
}
if (event.Records && event.Records.length >= 1) {
const eventSource =
event.Records[0].eventSource || // S3 and SQS
event.Records[0].EventSource; // SNS
if (eventSource === 'aws:sns') {
return TRIGGER_SNS;
} else if (eventSource === 'aws:sqs') {
return TRIGGER_SQS;
} else if (eventSource === 'aws:s3' && event.Records.length === 1) {
return TRIGGER_S3_SINGLE_EVENT;
}
}
return TRIGGER_GENERIC;
}
// Gather APM metadata for this Lambda executor per
// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-aws-lambda.md#overwriting-metadata
function getMetadata(agent, cloudAccountId) {
return {
service: {
framework: {
// Passing this service.framework.name to Client#setExtraMetadata()
// ensures that it "wins" over a framework name from
// `agent.setFramework()`, because in the client `_extraMetadata`
// wins over `_conf.frameworkName`.
name: 'AWS Lambda',
},
runtime: {
name: process.env.AWS_EXECUTION_ENV,
},
node: {
configured_name: process.env.AWS_LAMBDA_LOG_STREAM_NAME,
},
},
cloud: {
provider: 'aws',
region: process.env.AWS_REGION,
service: {
name: 'lambda',
},
account: {
id: cloudAccountId,
},
},
};
}
function getFaasData(context, faasId, isColdStart, faasTriggerType, requestId) {
const faasData = {
id: faasId,
name: context.functionName,
version: context.functionVersion,
coldstart: isColdStart,
execution: context.awsRequestId,
trigger: {
type: faasTriggerType,
},
};
if (requestId) {
faasData.trigger.request_id = requestId;
}
return faasData;
}
function setGenericData(trans, event, context, faasId, isColdStart) {
trans.type = 'request';
trans.setDefaultName(context.functionName);
trans.setFaas(getFaasData(context, faasId, isColdStart, 'other'));
const cloudContext = {
origin: {
provider: 'aws',
},
};
trans.setCloudContext(cloudContext);
}
// Set transaction data for an API Gateway triggered invocation.
//
// Handle API Gateway payload format vers 1.0 (a.k.a "REST") and 2.0 ("HTTP").
// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html
function setApiGatewayData(agent, trans, event, context, faasId, isColdStart) {
const requestContext = event.requestContext;
let name;
let pseudoReq;
if (requestContext.http) {
// 2.0
if (agent._conf.usePathAsTransactionName) {
name = `${requestContext.http.method} ${requestContext.http.path}`;
} else {
// Get a routeKeyPath from the routeKey:
// GET /some/path -> /some/path
// ANY /some/path -> /some/path
// $default -> /$default
let routeKeyPath = requestContext.routeKey;
const spaceIdx = routeKeyPath.indexOf(' ');
if (spaceIdx === -1) {
routeKeyPath = '/' + routeKeyPath;
} else {
routeKeyPath = routeKeyPath.slice(spaceIdx + 1);
}
name = `${requestContext.http.method} /${requestContext.stage}${routeKeyPath}`;
}
pseudoReq = {
httpVersion: requestContext.http.protocol
? requestContext.http.protocol.split('/')[1] // 'HTTP/1.1' -> '1.1'
: undefined,
method: requestContext.http.method,
url:
event.rawPath +
(event.rawQueryString ? '?' + event.rawQueryString : ''),
headers: event.normedHeaders || {},
socket: { remoteAddress: requestContext.http.sourceIp },
body: event.body,
};
} else {
// payload version format 1.0
if (agent._conf.usePathAsTransactionName) {
name = `${requestContext.httpMethod} ${requestContext.path}`;
} else {
name = `${requestContext.httpMethod} /${requestContext.stage}${requestContext.resourcePath}`;
}
pseudoReq = {
httpVersion: requestContext.protocol
? requestContext.protocol.split('/')[1] // 'HTTP/1.1' -> '1.1'
: undefined,
method: requestContext.httpMethod,
url:
requestContext.path +
(event.queryStringParameters
? '?' + querystring.encode(event.queryStringParameters)
: ''),
headers: event.normedHeaders || {},
socket: {
remoteAddress:
requestContext.identity && requestContext.identity.sourceIp,
},
// Limitation: Note that `getContextFromRequest` does *not* use this body,
// because API Gateway payload format 1.0 does not include the
// Content-Length header from the original request.
body: event.body,
};
}
trans.type = 'request';
trans.setDefaultName(name);
trans.req = pseudoReq; // Used by parsers.getContextFromRequest() for adding context to transaction and errors.
trans.setFaas(
getFaasData(context, faasId, isColdStart, 'http', requestContext.requestId),
);
const serviceContext = {
origin: {
name: requestContext.domainName,
id: requestContext.apiId,
version: event.version || '1.0',
},
};
trans.setServiceContext(serviceContext);
const originSvcName =
// `<url-id>.lambda-url.<region>.on.aws` indicates this is a Lambda URL.
requestContext.domainName &&
requestContext.domainPrefix &&
requestContext.domainName.startsWith(
requestContext.domainPrefix + '.lambda-url.',
)
? 'lambda url'
: 'api gateway';
const cloudContext = {
origin: {
provider: 'aws',
service: {
name: originSvcName,
},
account: {
id: requestContext.accountId,
},
},
};
trans.setCloudContext(cloudContext);
}
function setTransDataFromApiGatewayResult(err, result, trans, event) {
if (err) {
trans.result = 'HTTP 5xx';
trans._setOutcomeFromHttpStatusCode(500);
} else if (result && result.statusCode) {
trans.result = 'HTTP ' + result.statusCode.toString()[0] + 'xx';
trans._setOutcomeFromHttpStatusCode(result.statusCode);
} else {
trans.result = constants.RESULT_SUCCESS;
trans._setOutcomeFromHttpStatusCode(200);
}
// This doc defines the format of API Gateway-triggered responses, from which
// we can infer `transaction.context.response` values.
// https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.response
// https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads
if (err) {
trans.res = {
statusCode: 500,
};
} else if (event.requestContext.http) {
// payload format version 2.0
if (result && result.statusCode) {
trans.res = {
statusCode: result.statusCode,
headers: result.headers,
};
} else {
trans.res = {
statusCode: 200,
headers: { 'content-type': 'application/json' },
};
}
} else {
// payload format version 1.0
if (result && result.statusCode) {
trans.res = {
statusCode: result.statusCode,
headers: result.headers,
};
}
}
}
// https://docs.aws.amazon.com/lambda/latest/dg/services-alb.html
function setElbData(agent, trans, event, context, faasId, isColdStart) {
trans.type = 'request';
let name;
if (agent._conf.usePathAsTransactionName) {
name = `${event.httpMethod} ${event.path}`;
} else {
name = `${event.httpMethod} unknown route`;
}
trans.setDefaultName(name);
trans.req = {
// Used by parsers.getContextFromRequest() for adding context to transaction and errors.
method: event.httpMethod,
url:
event.path +
(event.queryStringParameters &&
Object.keys(event.queryStringParameters) > 0
? '?' + querystring.encode(event.queryStringParameters)
: ''),
headers: event.normedHeaders || {},
body: event.body,
bodyIsBase64Encoded: event.isBase64Encoded,
};
trans.setFaas(getFaasData(context, faasId, isColdStart, 'http'));
const targetGroupArn = event.requestContext.elb.targetGroupArn;
const arnParts = targetGroupArn.split(':');
trans.setServiceContext({
origin: {
name: arnParts[5].split('/')[1],
id: targetGroupArn,
},
});
trans.setCloudContext({
origin: {
provider: 'aws',
region: arnParts[3],
service: {
name: 'elb',
},
account: {
id: arnParts[4],
},
},
});
}
function setTransDataFromElbResult(err, result, trans) {
// ELB defaults to 502 Bad Gateway if there is an error or `result.statusCode`
// is missing or not an integer.
const validStatusCode =
result &&
result.statusCode &&
typeof result.statusCode === 'number' &&
Number.isInteger(result.statusCode)
? result.statusCode
: null;
if (err) {
trans.result = 'HTTP 5xx';
} else if (validStatusCode) {
trans.result = 'HTTP ' + validStatusCode.toString()[0] + 'xx';
} else {
trans.result = 'HTTP 5xx';
}
// Set an appropriate pseudo `trans.res`, used by `parsers.getContextFromResponse()`.
if (err) {
trans.res = {
statusCode: 502,
};
} else {
trans.res = {
statusCode: validStatusCode || 502,
headers: result.headers,
};
}
trans._setOutcomeFromHttpStatusCode(validStatusCode || 502);
}
function setSqsData(agent, trans, event, context, faasId, isColdStart) {
const record = event && event.Records && event.Records[0];
const eventSourceARN = record.eventSourceARN ? record.eventSourceARN : '';
trans.setFaas(getFaasData(context, faasId, isColdStart, 'pubsub'));
const arnParts = eventSourceARN.split(':');
const queueName = arnParts[5];
const accountId = arnParts[4];
trans.setDefaultName(`RECEIVE ${queueName}`);
trans.type = 'messaging';
const serviceContext = {
origin: {
name: queueName,
id: eventSourceARN,
},
};
trans.setServiceContext(serviceContext);
const cloudContext = {
origin: {
provider: 'aws',
region: record.awsRegion,
service: {
name: 'sqs',
},
account: {
id: accountId,
},
},
};
trans.setCloudContext(cloudContext);
const links = spanLinksFromSqsRecords(event.Records);
trans.addLinks(links);
}
function setSnsData(agent, trans, event, context, faasId, isColdStart) {
const record = event && event.Records && event.Records[0];
const sns = record && record.Sns;
trans.setFaas(getFaasData(context, faasId, isColdStart, 'pubsub'));
const topicArn = (sns && sns.TopicArn) || '';
const arnParts = topicArn.split(':');
const topicName = arnParts[5];
const accountId = arnParts[4];
const region = arnParts[3];
trans.setDefaultName(`RECEIVE ${topicName}`);
trans.type = 'messaging';
const serviceContext = {
origin: {
name: topicName,
id: topicArn,
},
};
trans.setServiceContext(serviceContext);
const cloudContext = {
origin: {
provider: 'aws',
region,
service: {
name: 'sns',
},
account: {
id: accountId,
},
},
};
trans.setCloudContext(cloudContext);
const links = spanLinksFromSnsRecords(event.Records);
trans.addLinks(links);
}
function setS3SingleData(trans, event, context, faasId, isColdStart) {
const record = event.Records[0];
trans.setFaas(
getFaasData(
context,
faasId,
isColdStart,
'datasource',
record.responseElements && record.responseElements['x-amz-request-id'],
),
);
trans.setDefaultName(
`${record && record.eventName} ${
record && record.s3 && record.s3.bucket && record.s3.bucket.name
}`,
);
trans.type = 'request';
const serviceContext = {
origin: {
name: record && record.s3 && record.s3.bucket && record.s3.bucket.name,
id: record && record.s3 && record.s3.bucket && record.s3.bucket.arn,
version: record.eventVersion,
},
};
trans.setServiceContext(serviceContext);
const cloudContext = {
origin: {
provider: 'aws',
service: {
name: 's3',
},
region: record.awsRegion,
},
};
trans.setCloudContext(cloudContext);
}
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 */
});
}
};
};
}
function isLambdaExecutionEnvironment() {
return !!process.env.AWS_LAMBDA_FUNCTION_NAME;
}
// Returns the full file path to the user's handler handler module
//
// The Lambda Runtime allows a user's handler module to have either a .js or
// .cjs extension. The getFilePath looks for a .js file first, and if not found
// presumes a csj file exists. If neither file exists this means the user either
// as a misconfigured handler (they'd never reach this code) or is using a
// .mjs file extension (which indicates an ECMAScript/import module, which the
// agent does not support.
//
// TODO: support "extensionless"? per https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L149 Is this for a dir/index.js?
// TODO: support ESM and .mjs
//
// @param {string} taskRoot
// @param {string} moduleRoot - The subdir under `taskRoot` holding the module.
// @param {string} module - The module name.
// @return {string | null}
function getFilePath(taskRoot, moduleRoot, module) {
const lambdaStylePath = path.resolve(taskRoot, moduleRoot, module);
if (fs.existsSync(lambdaStylePath + '.js')) {
return lambdaStylePath + '.js';
} else if (fs.existsSync(lambdaStylePath + '.cjs')) {
return lambdaStylePath + '.cjs';
} else {
return null;
}
}
/**
* Gather module and export info for the Lambda "handler" string.
*
* Compare to the Node.js Lambda runtime's equivalent processing here:
* https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L288
*
* @param {object} env - The process environment.
* @param {any} [logger] - Optional logger for trace/warn log output.
*/
function getLambdaHandlerInfo(env, logger) {
if (
!isLambdaExecutionEnvironment() ||
!env._HANDLER ||
!env.LAMBDA_TASK_ROOT
) {
return null;
}
// Dev Note: This intentionally uses some of the same var names at
// https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/v3.2.1/src/UserFunction.js#L288
const fullHandlerString = env._HANDLER;
const moduleAndHandler = path.basename(fullHandlerString);
const moduleRoot = fullHandlerString.substring(
0,
fullHandlerString.indexOf(moduleAndHandler),
);
const FUNCTION_EXPR = /^([^.]*)\.(.*)$/;
const match = moduleAndHandler.match(FUNCTION_EXPR);
if (!match || match.length !== 3) {
if (logger) {
logger.warn(
{ fullHandlerString, moduleAndHandler },
'Lambda handler string did not match FUNCTION_EXPR',
);
}
return null;
}
const module = match[1];
const handlerPath = match[2];
const moduleAbsPath = getFilePath(env.LAMBDA_TASK_ROOT, moduleRoot, module);
if (!moduleAbsPath) {
if (logger) {
logger.warn(
{ fullHandlerString, moduleRoot, module },
'could not find Lambda handler module file (ESM not yet supported)',
);
}
return null;
}
const lambdaHandlerInfo = {
filePath: moduleAbsPath,
modName: module,
propPath: handlerPath,
};
if (logger) {
logger.trace({ fullHandlerString, lambdaHandlerInfo }, 'lambdaHandlerInfo');
}
return lambdaHandlerInfo;
}
function lowerCaseObjectKeys(obj) {
const lowerCased = {};
for (const key of Object.keys(obj)) {
lowerCased[key.toLowerCase()] = obj[key];
}
return lowerCased;
}
// Extract span links from up to 1000 messages in this batch.
// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-messaging.md#receiving-trace-context
//
// A span link is created from a `traceparent` message attribute in a message.
// `msg.messageAttributes` is of the form:
// { <attribute-name>: { DataType: <attr-type>, StringValue: <attr-value>, ... } }
// For example:
// { traceparent: { DataType: 'String', StringValue: 'test-traceparent' } }
function spanLinksFromSqsRecords(records) {
const links = [];
const limit = Math.min(
records.length,
MAX_MESSAGES_PROCESSED_FOR_TRACE_CONTEXT,
);
for (let i = 0; i < limit; i++) {
const attrs = records[i].messageAttributes;
if (!attrs) {
continue;
}
let traceparent;
const attrNames = Object.keys(attrs);
for (let j = 0; j < attrNames.length; j++) {
const attrVal = attrs[attrNames[j]];
if (attrVal.dataType !== 'String') {
continue;
}
const attrNameLc = attrNames[j].toLowerCase();
if (attrNameLc === 'traceparent') {
traceparent = attrVal.stringValue;
break;
}
}
if (traceparent) {
links.push({ context: traceparent });
}
}
return links;
}
// Extract span links from up to 1000 messages in this batch.
// https://github.com/elastic/apm/blob/main/specs/agents/tracing-instrumentation-messaging.md#receiving-trace-context
//
// A span link is created from a `traceparent` message attribute in a message.
// `record.Sns.MessageAttributes` is of the form:
// { <attribute-name>: { Type: <attr-type>, Value: <attr-value> } }
// For example:
// { traceparent: { Type: 'String', Value: 'test-traceparent' } }
function spanLinksFromSnsRecords(records) {
const links = [];
const limit = Math.min(
records.length,
MAX_MESSAGES_PROCESSED_FOR_TRACE_CONTEXT,
);
for (let i = 0; i < limit; i++) {
const attrs = records[i].Sns && records[i].Sns.MessageAttributes;
if (!attrs) {
continue;
}
let traceparent;
const attrNames = Object.keys(attrs);
for (let j = 0; j < attrNames.length; j++) {
const attrVal = attrs[attrNames[j]];
if (attrVal.Type !== 'String') {
continue;
}
const attrNameLc = attrNames[j].toLowerCase();
if (attrNameLc === 'traceparent') {
traceparent = attrVal.Value;
break;
}
}
if (traceparent) {
links.push({ context: traceparent });
}
}
return links;
}
module.exports = {
isLambdaExecutionEnvironment,
elasticApmAwsLambda,
getLambdaHandlerInfo,
};