Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sns instrumentation #2157

Merged
merged 23 commits into from
Aug 26, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
147 changes: 147 additions & 0 deletions lib/instrumentation/modules/aws-sdk/sns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
const constants = require('../../../constants')

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

function getArnFromRequest (request) {
astorm marked this conversation as resolved.
Show resolved Hide resolved
let arn = request && request.params && request.params.TopicArn
if (!arn) {
arn = request && request.params && request.params.TargetArn
}
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.indexOf('/') !== -1) {
astorm marked this conversation as resolved.
Show resolved Hide resolved
const parts = fullName.split('/')
parts.pop()
return parts.join('/')
} 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 = getArnFromRequest(request)
astorm marked this conversation as resolved.
Show resolved Hide resolved
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)
}
astorm marked this conversation as resolved.
Show resolved Hide resolved

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
}
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