Skip to content

Commit

Permalink
feat: sns instrumentation (#2157)
Browse files Browse the repository at this point in the history
* Implements implementation of Amazon SNS for the v2 APIs

#1955

Co-authored-by: Trent Mick <[email protected]>
  • Loading branch information
astorm and trentm authored Aug 26, 2021
1 parent 0f28a16 commit 757e286
Show file tree
Hide file tree
Showing 6 changed files with 613 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ Notes:
option. Before this change only incoming requests supporting HTTP/2 would
be traced. ({pull}2143[#2143])
* Add instrumentation of the AWS SNS publish method when using the
https://www.npmjs.com/package/aws-sdk[JavaScript AWS SDK v2] (`aws-sdk`). ({pull}2157[#2157])
[float]
===== Bug fixes
Expand Down
2 changes: 1 addition & 1 deletion docs/supported-technologies.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ The Node.js agent will automatically instrument the following modules to give yo
[options="header"]
|=======================================================================
|Module |Version |Note
|https://www.npmjs.com/package/aws-sdk[aws-sdk] |>1 <3 |Will instrument SQS send/receive/delete messages, all S3 methods, and all DynamoDB methods
|https://www.npmjs.com/package/aws-sdk[aws-sdk] |>1 <3 |Will instrument SQS send/receive/delete messages, all S3 methods, all DynamoDB methods, and the SNS publish method
|https://www.npmjs.com/package/cassandra-driver[cassandra-driver] |>=3.0.0 |Will instrument all queries
|https://www.npmjs.com/package/elasticsearch[elasticsearch] |>=8.0.0 |Will instrument all queries
|https://www.npmjs.com/package/@elastic/elasticsearch[@elastic/elasticsearch] |>=7.0.0 <8.0.0 |Will instrument all queries
Expand Down
4 changes: 3 additions & 1 deletion lib/instrumentation/modules/aws-sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ const shimmer = require('../shimmer')
const { instrumentationS3 } = require('./aws-sdk/s3')
const { instrumentationSqs } = require('./aws-sdk/sqs')
const { instrumentationDynamoDb } = require('./aws-sdk/dynamodb.js')
const { instrumentationSns } = require('./aws-sdk/sns.js')

const instrumentorFromSvcId = {
s3: instrumentationS3,
sqs: instrumentationSqs,
dynamodb: instrumentationDynamoDb
dynamodb: instrumentationDynamoDb,
sns: instrumentationSns
}

// Called in place of AWS.Request.send and AWS.Request.promise
Expand Down
151 changes: 151 additions & 0 deletions lib/instrumentation/modules/aws-sdk/sns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
const constants = require('../../../constants')

const TYPE = 'messaging'
const SUBTYPE = 'sns'
const ACTION = 'publish'
const PHONE_NUMBER = '<PHONE_NUMBER>'

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),
service: {
resource: `${SUBTYPE}/${getDestinationNameFromRequest(request)}`,
type: TYPE,
name: SUBTYPE
},
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
}

function instrumentationSns (orig, origArguments, request, AWS, agent, { version, enabled }) {
const type = TYPE
const subtype = SUBTYPE
const action = ACTION

if (shouldIgnoreRequest(request, agent)) {
return orig.apply(request, origArguments)
}

const name = getSpanNameFromRequest(request)
const span = agent.startSpan(name, type, subtype, action)
if (!span) {
return orig.apply(request, origArguments)
}

span.setDestinationContext(getMessageDestinationContextFromRequest(request))
span.setMessageContext(getMessageContextFromRequest(request))

request.on('complete', function (response) {
if (response && response.error) {
const errOpts = {
skipOutcome: true
}
agent.captureError(response.error, errOpts)
span._setOutcomeFromErrorCapture(constants.OUTCOME_FAILURE)
}

// we'll need to manually mark this span as async. The actual async hop
// is captured by the agent's async hooks instrumentation
span.sync = false
span.end()
})

return orig.apply(request, origArguments)
}

module.exports = {
instrumentationSns,

// exported for testing
getSpanNameFromRequest,
getDestinationNameFromRequest,
getMessageDestinationContextFromRequest,
getArnOrPhoneNumberFromRequest
}
46 changes: 46 additions & 0 deletions test/instrumentation/modules/aws-sdk/fixtures/sns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use strict'
module.exports = {
publish: {
response: `<?xml version="1.0"?>
<PublishResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<PublishResult>
<MessageId>32a3b682-ce0c-5b2a-b97f-efe5112e9f06</MessageId>
</PublishResult>
<ResponseMetadata>
<RequestId>8e87dc3a-07f7-54f0-ae0d-855dd8d5f0dc</RequestId>
</ResponseMetadata>
</PublishResponse>`,
httpStatusCode: 200
},
publishNoTopic: {
response: `<?xml version="1.0"?>
<ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<Error>
<Type>Sender</Type>
<Code>NotFound</Code>
<Message>Topic does not exist</Message>
</Error>
<RequestId>02672fe4-577a-5c2a-9a11-7683bd8777e1</RequestId>
</ErrorResponse>`,
httpStatusCode: 404
},
listTopics: {
response: `<?xml version="1.0"?>
<ListTopicsResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ListTopicsResult>
<Topics>
<member>
<TopicArn>arn:aws:sns:us-west-2:111111111111:topic-name</TopicArn>
</member>
<member>
<TopicArn>arn:aws:sns:us-west-2:111111111111:dynamodb</TopicArn>
</member>
</Topics>
</ListTopicsResult>
<ResponseMetadata>
<RequestId>1cb1f1f2-48fa-523b-a01a-a895b59d244b</RequestId>
</ResponseMetadata>
</ListTopicsResponse>`,
httpStatusCode: 200
}
}
Loading

0 comments on commit 757e286

Please sign in to comment.