Skip to content

Commit 5f88def

Browse files
zarirhamzawconti27
andauthored
Enhance Service Representation for Serverless (#5967)
Adds peer.service tag explicitly for network call spans when the aws-sdk is called within an AWS environment Cleans up serverless service map --------- Co-authored-by: William Conti <william.conti@datadoghq.com>
1 parent 619c301 commit 5f88def

File tree

5 files changed

+382
-13
lines changed

5 files changed

+382
-13
lines changed

packages/datadog-plugin-aws-sdk/src/base.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,25 @@ class BaseAwsSdkPlugin extends ClientPlugin {
7373
span.addTags(requestTags)
7474
}
7575

76+
this.enter(span)
7677
const store = storage('legacy').getStore()
7778

78-
this.enter(span, store)
79+
const peerServerlessStorage = storage('peerServerless')
80+
if (!this._tracerConfig?._isInServerlessEnvironment()) return
81+
82+
// Try to resolve the hostname immediately; if not possible, keep enough
83+
// information so the region callback can resolve it later.
84+
const hostname = getHostname({ awsParams: request.params, awsService }, awsRegion)
85+
const peerServerlessStore = {}
86+
peerServerlessStorage.enterWith(peerServerlessStore)
87+
88+
if (hostname) {
89+
span.setTag('peer.service', hostname)
90+
peerServerlessStore.peerHostname = hostname
91+
} else {
92+
store.awsParams = request.params
93+
store.awsService = awsService
94+
}
7995
})
8096

8197
this.addSub(`apm:aws:request:region:${this.serviceIdentifier}`, region => {
@@ -85,6 +101,17 @@ class BaseAwsSdkPlugin extends ClientPlugin {
85101
if (!span) return
86102
span.setTag('aws.region', region)
87103
span.setTag('region', region)
104+
105+
if (!this._tracerConfig?._isInServerlessEnvironment()) return
106+
107+
const hostname = getHostname(store, region)
108+
if (!hostname) return
109+
110+
span.setTag('peer.service', hostname)
111+
const peerServerlessStore = storage('peerServerless').getStore()
112+
if (peerServerlessStore) {
113+
peerServerlessStore.peerHostname = hostname
114+
}
88115
})
89116

90117
this.addSub(`apm:aws:request:complete:${this.serviceIdentifier}`, ({ response, cbExists = false }) => {
@@ -243,4 +270,27 @@ function getHooks (config) {
243270
return { request }
244271
}
245272

273+
function getHostname (store, region) {
274+
if (!store) return
275+
if (!region) return
276+
const { awsParams, awsService } = store
277+
switch (awsService) {
278+
case 'EventBridge':
279+
return `events.${region}.amazonaws.com`
280+
case 'SQS':
281+
return `sqs.${region}.amazonaws.com`
282+
case 'SNS':
283+
return `sns.${region}.amazonaws.com`
284+
case 'Kinesis':
285+
return `kinesis.${region}.amazonaws.com`
286+
case 'DynamoDBDocument':
287+
case 'DynamoDB':
288+
return `dynamodb.${region}.amazonaws.com`
289+
case 'S3':
290+
return awsParams?.Bucket
291+
? `${awsParams.Bucket}.s3.${region}.amazonaws.com`
292+
: `s3.${region}.amazonaws.com`
293+
}
294+
}
295+
246296
module.exports = BaseAwsSdkPlugin
Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
'use strict'
2+
3+
const agent = require('../../dd-trace/test/plugins/agent')
4+
const helpers = require('./kinesis_helpers')
5+
const { promisify } = require('util')
6+
const { setup } = require('./spec_helpers')
7+
8+
describe('Plugin', () => {
9+
describe('Serverless', function () {
10+
setup()
11+
12+
withVersions('aws-sdk', ['aws-sdk', '@aws-sdk/smithy-client'], (version, moduleName) => {
13+
let AWS
14+
15+
before(async () => {
16+
process.env.DD_TRACE_EXPERIMENTAL_EXPORTER = 'agent'
17+
process.env.AWS_LAMBDA_FUNCTION_NAME = 'test'
18+
await agent.load(['aws-sdk', 'http'], [{}, { server: false }])
19+
})
20+
21+
after(async () => {
22+
delete process.env.DD_TRACE_EXPERIMENTAL_EXPORTER
23+
delete process.env.AWS_LAMBDA_FUNCTION_NAME
24+
await agent.close({ ritmReset: false })
25+
})
26+
27+
describe('DynamoDB-Serverless', () => {
28+
let dynamo
29+
const tableName = 'PeerServiceTestTable'
30+
const dynamoClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-dynamodb' : 'aws-sdk'
31+
32+
function getCreateTableParams () {
33+
return {
34+
TableName: tableName,
35+
KeySchema: [{ AttributeName: 'id', KeyType: 'HASH' }],
36+
AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }],
37+
ProvisionedThroughput: { ReadCapacityUnits: 5, WriteCapacityUnits: 5 }
38+
}
39+
}
40+
41+
before(async () => {
42+
AWS = require(`../../../versions/${dynamoClientName}@${version}`).get()
43+
44+
dynamo = new AWS.DynamoDB({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' })
45+
46+
// ignore error if the table already exists
47+
if (typeof dynamo.createTable === 'function') {
48+
const createTable = toPromise(dynamo, dynamo.createTable)
49+
try { await createTable(getCreateTableParams()) } catch (_) {}
50+
}
51+
})
52+
53+
it('propagates peer.service from aws span to underlying http span', async () => {
54+
const send = toPromise(dynamo, dynamo.putItem)
55+
56+
const tracesPromise = agent.assertSomeTraces(traces => {
57+
const peerService = 'dynamodb.us-east-1.amazonaws.com'
58+
const spans = traces[0]
59+
const awsSpan = spans.find(s => s.name === 'aws.request')
60+
const httpSpan = spans.find(s => s.name === 'http.request')
61+
expect(awsSpan.meta['peer.service']).to.equal(peerService)
62+
expect(httpSpan.meta['peer.service']).to.equal(peerService)
63+
})
64+
65+
await Promise.all([
66+
tracesPromise,
67+
send({
68+
TableName: tableName,
69+
Item: {
70+
id: { S: '123' }
71+
}
72+
})
73+
])
74+
})
75+
})
76+
77+
describe('Kinesis-Serverless', () => {
78+
let kinesis
79+
const streamName = 'PeerStream'
80+
const kinesisClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-kinesis' : 'aws-sdk'
81+
82+
function createStream (cb) {
83+
AWS = require(`../../../versions/${kinesisClientName}@${version}`).get()
84+
85+
const params = {
86+
endpoint: 'http://127.0.0.1:4566',
87+
region: 'us-east-1'
88+
}
89+
90+
if (moduleName === '@aws-sdk/smithy-client') {
91+
const { NodeHttpHandler } = require(`../../../versions/@aws-sdk/node-http-handler@${version}`).get()
92+
93+
params.requestHandler = new NodeHttpHandler()
94+
}
95+
96+
kinesis = new AWS.Kinesis(params)
97+
98+
kinesis.createStream({
99+
StreamName: streamName,
100+
ShardCount: 1
101+
}, (err) => {
102+
if (err) return cb(err)
103+
104+
helpers.waitForActiveStream(kinesis, streamName, cb)
105+
})
106+
}
107+
108+
before(done => {
109+
createStream(done)
110+
})
111+
112+
after(done => {
113+
kinesis.deleteStream({
114+
StreamName: streamName
115+
}, (err, res) => {
116+
if (err) return done(err)
117+
118+
helpers.waitForDeletedStream(kinesis, streamName, done)
119+
})
120+
})
121+
122+
it('propagates peer.service from aws span to underlying http span', done => {
123+
agent.assertSomeTraces(traces => {
124+
const peerService = 'kinesis.us-east-1.amazonaws.com'
125+
const spans = traces[0]
126+
const awsSpan = spans.find(s => s.name === 'aws.request')
127+
const httpSpan = spans.find(s => s.name === 'http.request')
128+
expect(awsSpan.meta['peer.service']).to.equal(peerService)
129+
expect(httpSpan.meta['peer.service']).to.equal(peerService)
130+
}, { timeoutMs: 10000 })
131+
.then(done, done)
132+
133+
helpers.putTestRecord(kinesis, streamName, helpers.dataBuffer, e => e && done(e))
134+
})
135+
})
136+
137+
describe('SNS-Serverless', () => {
138+
let sns, sqs, TopicArn, QueueUrl
139+
140+
const queueName = 'PeerQueue'
141+
const topicName = 'PeerTopic'
142+
143+
const snsClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-sns' : 'aws-sdk'
144+
const sqsClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-sqs' : 'aws-sdk'
145+
146+
function createTopicAndQueue (cb) {
147+
const { SNS } = require(`../../../versions/${snsClientName}@${version}`).get()
148+
const { SQS } = require(`../../../versions/${sqsClientName}@${version}`).get()
149+
150+
sns = new SNS({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' })
151+
sqs = new SQS({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' })
152+
153+
sns.createTopic({ Name: topicName }, (err, data) => {
154+
if (err) return cb(err)
155+
156+
TopicArn = data.TopicArn
157+
158+
sqs.createQueue({ QueueName: queueName }, (err, data) => {
159+
if (err) return cb(err)
160+
161+
QueueUrl = data.QueueUrl
162+
163+
sqs.getQueueAttributes({ QueueUrl, AttributeNames: ['All'] }, (err, data) => {
164+
if (err) return cb(err)
165+
166+
cb()
167+
})
168+
})
169+
})
170+
}
171+
172+
before(done => {
173+
createTopicAndQueue(done)
174+
})
175+
176+
after(done => {
177+
sns.deleteTopic({ TopicArn }, done)
178+
})
179+
180+
after(done => {
181+
sqs.deleteQueue({ QueueUrl }, done)
182+
})
183+
184+
it('propagates peer.service from aws span to underlying http span', done => {
185+
agent.assertSomeTraces(traces => {
186+
const peerService = 'sns.us-east-1.amazonaws.com'
187+
const spans = traces[0]
188+
const awsSpan = spans.find(s => s.name === 'aws.request')
189+
const httpSpan = spans.find(s => s.name === 'http.request')
190+
expect(awsSpan.meta['peer.service']).to.equal(peerService)
191+
expect(httpSpan.meta['peer.service']).to.equal(peerService)
192+
})
193+
.then(done, done)
194+
195+
sns.publish({
196+
TopicArn,
197+
Message: 'message 1',
198+
MessageAttributes: {
199+
baz: { DataType: 'String', StringValue: 'bar' },
200+
keyOne: { DataType: 'String', StringValue: 'keyOne' },
201+
keyTwo: { DataType: 'String', StringValue: 'keyTwo' }
202+
}
203+
}, e => e && done(e))
204+
})
205+
})
206+
207+
describe('SQS-Serverless', () => {
208+
let sqs
209+
const queueName = 'SQS_QUEUE_NAME'
210+
211+
const getQueueParams = (queueName) => {
212+
return {
213+
QueueName: queueName,
214+
Attributes: {
215+
MessageRetentionPeriod: '86400'
216+
}
217+
}
218+
}
219+
220+
const queueOptions = getQueueParams(queueName)
221+
const QueueUrl = 'http://127.0.0.1:4566/00000000000000000000/SQS_QUEUE_NAME'
222+
223+
const sqsClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-sqs' : 'aws-sdk'
224+
225+
before(done => {
226+
AWS = require(`../../../versions/${sqsClientName}@${version}`).get()
227+
228+
sqs = new AWS.SQS({ endpoint: 'http://127.0.0.1:4566', region: 'us-east-1' })
229+
sqs.createQueue(queueOptions, done)
230+
})
231+
232+
after(done => {
233+
sqs.deleteQueue({ QueueUrl }, done)
234+
})
235+
236+
it('propagates peer.service from aws span to underlying http span', done => {
237+
agent.assertSomeTraces(traces => {
238+
const peerService = 'sqs.us-east-1.amazonaws.com'
239+
const spans = traces[0]
240+
const awsSpan = spans.find(s => s.name === 'aws.request')
241+
const httpSpan = spans.find(s => s.name === 'http.request')
242+
expect(awsSpan.meta['peer.service']).to.equal(peerService)
243+
expect(httpSpan.meta['peer.service']).to.equal(peerService)
244+
})
245+
.then(done, done)
246+
247+
sqs.sendMessage({
248+
MessageBody: 'test body',
249+
QueueUrl
250+
}, () => {})
251+
})
252+
})
253+
254+
describe('S3-Serverless', () => {
255+
let s3
256+
const bucketName = 's3-bucket-name-test'
257+
258+
const s3ClientName = moduleName === '@aws-sdk/smithy-client' ? '@aws-sdk/client-s3' : 'aws-sdk'
259+
260+
before(done => {
261+
AWS = require(`../../../versions/${s3ClientName}@${version}`).get()
262+
s3 = new AWS.S3({ endpoint: 'http://127.0.0.1:4566', s3ForcePathStyle: true, region: 'us-east-1' })
263+
264+
if (s3ClientName === 'aws-sdk') {
265+
s3.api.globalEndpoint = '127.0.0.1'
266+
}
267+
268+
s3.createBucket({ Bucket: bucketName }, done)
269+
})
270+
271+
it('propagates peer.service from aws span to underlying http span', async () => {
272+
const put = toPromise(s3, s3.putObject)
273+
274+
await put({
275+
Bucket: bucketName,
276+
Key: 'test-key',
277+
Body: 'dummy-data'
278+
})
279+
280+
await agent.assertSomeTraces(traces => {
281+
const peerService = 's3-bucket-name-test.s3.us-east-1.amazonaws.com'
282+
const spans = traces[0]
283+
const awsSpan = spans.find(s => s.name === 'aws.request')
284+
const httpSpan = spans.find(s => s.name === 'http.request')
285+
expect(awsSpan.meta['peer.service']).to.equal(peerService)
286+
expect(httpSpan.meta['peer.service']).to.equal(peerService)
287+
})
288+
})
289+
})
290+
})
291+
})
292+
})
293+
294+
function toPromise (client, fn) {
295+
return function (...args) {
296+
if (typeof fn === 'function' && typeof fn.bind === 'function') {
297+
fn = fn.bind(client)
298+
}
299+
300+
const result = fn(...args)
301+
302+
if (result && typeof result.then === 'function') {
303+
return result
304+
}
305+
306+
if (result && typeof result.promise === 'function') {
307+
return result.promise()
308+
}
309+
310+
return promisify(fn)(...args)
311+
}
312+
}

0 commit comments

Comments
 (0)