lib/instrumentation/modules/aws-sdk/sns.js (189 lines of code) (raw):
/*
* 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.
*/
'use strict';
const TYPE = 'messaging';
const SUBTYPE = 'sns';
const ACTION = 'publish';
const PHONE_NUMBER = '<PHONE_NUMBER>';
const MAX_SNS_MESSAGE_ATTRIBUTES = 10;
function getArnOrPhoneNumberFromRequest(request) {
let arn = request && request.params && request.params.TopicArn;
if (!arn) {
arn = request && request.params && request.params.TargetArn;
}
if (!arn) {
arn = request && request.params && request.params.PhoneNumber;
}
return arn;
}
function getRegionFromRequest(request) {
return (
request &&
request.service &&
request.service.config &&
request.service.config.region
);
}
function getSpanNameFromRequest(request) {
const topicName = getDestinationNameFromRequest(request);
return `SNS PUBLISH to ${topicName}`;
}
function getMessageContextFromRequest(request) {
return {
queue: {
name: getDestinationNameFromRequest(request),
},
};
}
function getAddressFromRequest(request) {
return (
request &&
request.service &&
request.service.endpoint &&
request.service.endpoint.hostname
);
}
function getPortFromRequest(request) {
return (
request &&
request.service &&
request.service.endpoint &&
request.service.endpoint.port
);
}
function getMessageDestinationContextFromRequest(request) {
return {
address: getAddressFromRequest(request),
port: getPortFromRequest(request),
cloud: {
region: getRegionFromRequest(request),
},
};
}
function getDestinationNameFromRequest(request) {
const phoneNumber = request && request.params && request.params.PhoneNumber;
if (phoneNumber) {
return PHONE_NUMBER;
}
const topicArn = request && request.params && request.params.TopicArn;
const targetArn = request && request.params && request.params.TargetArn;
if (topicArn) {
const parts = topicArn.split(':');
const topicName = parts.pop();
return topicName;
}
if (targetArn) {
const fullName = targetArn.split(':').pop();
if (fullName.lastIndexOf('/') !== -1) {
return fullName.substring(0, fullName.lastIndexOf('/'));
} else {
return fullName;
}
}
}
function shouldIgnoreRequest(request, agent) {
if (request.operation !== 'publish') {
return true;
}
// is the named topic on our ignore list?
if (agent._conf && agent._conf.ignoreMessageQueuesRegExp) {
const queueName = getArnOrPhoneNumberFromRequest(request);
if (queueName) {
for (const rule of agent._conf.ignoreMessageQueuesRegExp) {
if (rule.test(queueName)) {
return true;
}
}
}
}
return false;
}
// Though our spec only mentions a 10-message-attribute limit for *SQS*, we'll
// do the same limit here, because
// https://docs.aws.amazon.com/sns/latest/dg/sns-message-attributes.html
// mentions the 10-message-attribute limit for SQS subscriptions.
function propagateTraceContextInMessageAttributes(
parentSpan,
msgParams,
log,
targetId,
) {
const msgAttrs = msgParams.MessageAttributes;
if (
msgAttrs &&
Object.keys(msgAttrs).length + 2 > MAX_SNS_MESSAGE_ATTRIBUTES
) {
log.warn(
'cannot propagate trace-context with SNS message to %s, too many MessageAttributes',
targetId,
);
} else {
parentSpan.propagateTraceContextHeaders(
msgAttrs,
function (msgAttrs, name, value) {
if (name.startsWith('elastic-')) {
return;
}
msgAttrs[name] = { DataType: 'String', StringValue: value };
},
);
}
}
function instrumentationSns(
orig,
origArguments,
request,
AWS,
agent,
{ version, enabled },
) {
if (shouldIgnoreRequest(request, agent)) {
return orig.apply(request, origArguments);
}
const ins = agent._instrumentation;
const log = agent.logger;
const parentRunContext = ins.currRunContext();
const name = getSpanNameFromRequest(request);
const span = ins.createSpan(name, TYPE, SUBTYPE, ACTION, { exitSpan: true });
// W3C trace-context propagation.
const parent =
span || parentRunContext.currSpan() || parentRunContext.currTransaction();
if (parent) {
// Currently only instrumenting a 'publish' operation, for which we want
// to propagate trace-context.
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SNS.html#publish-property
const paramsCopy = Object.assign({}, request.params);
paramsCopy.MessageAttributes = Object.assign(
{},
paramsCopy.MessageAttributes,
);
request.params = paramsCopy;
const targetId = getArnOrPhoneNumberFromRequest(request);
propagateTraceContextInMessageAttributes(
parent,
request.params,
log,
targetId,
);
}
if (!span) {
return orig.apply(request, origArguments);
}
span._setDestinationContext(getMessageDestinationContextFromRequest(request));
span.setMessageContext(getMessageContextFromRequest(request));
const onComplete = function (response) {
if (response && response.error) {
agent.captureError(response.error);
}
span.end();
};
// Bind onComplete to the span's run context so that `captureError` picks
// up the correct currentSpan.
const spanRunContext = parentRunContext.enterSpan(span);
request.on(
'complete',
ins.bindFunctionToRunContext(spanRunContext, onComplete),
);
const cb = origArguments[origArguments.length - 1];
if (typeof cb === 'function') {
origArguments[origArguments.length - 1] = ins.bindFunctionToRunContext(
parentRunContext,
cb,
);
}
return ins.withRunContext(spanRunContext, orig, request, ...origArguments);
}
module.exports = {
instrumentationSns,
// exported for testing
getSpanNameFromRequest,
getDestinationNameFromRequest,
getMessageDestinationContextFromRequest,
getArnOrPhoneNumberFromRequest,
};