Skip to content

Commit 6bf1ccc

Browse files
feat: AWS entity linking segment attributes (#2978)
1 parent cb4e2f7 commit 6bf1ccc

File tree

4 files changed

+144
-9
lines changed

4 files changed

+144
-9
lines changed

lib/otel/constants.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ module.exports = {
6161
*/
6262
ATTR_DB_SYSTEM: 'db.system',
6363

64+
/**
65+
* The cloud provider of the invoked function.
66+
*
67+
* @example aws
68+
* @see https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/specification/trace/semantic_conventions/faas.md#outgoing-invocations
69+
*/
70+
ATTR_FAAS_INVOKED_PROVIDER: 'faas.invoked_provider',
71+
6472
/**
6573
* The full resource URL.
6674
*
@@ -182,7 +190,8 @@ module.exports = {
182190
* Target messaging system name.
183191
*
184192
* @example kafka
185-
* @see https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/
193+
* @example aws_sqs
194+
* @see https://opentelemetry.io/docs/specs/semconv/messaging/messaging-spans/#messaging-attributes
186195
*/
187196
ATTR_MESSAGING_SYSTEM: 'messaging.system',
188197

@@ -233,7 +242,7 @@ module.exports = {
233242
ATTR_NET_HOST_NAME: 'net.host.name',
234243

235244
/**
236-
* Poort of the local HTTP server that received the request.
245+
* Port of the local HTTP server that received the request.
237246
*
238247
* @example 80
239248
*/

lib/otel/span-processor.js

+46-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
ATTR_DB_NAME,
1717
ATTR_DB_STATEMENT,
1818
ATTR_DB_SYSTEM,
19+
ATTR_FAAS_INVOKED_PROVIDER,
1920
ATTR_FULL_URL,
2021
ATTR_GRPC_STATUS_CODE,
2122
ATTR_HTTP_METHOD,
@@ -34,9 +35,11 @@ const {
3435
ATTR_NET_PEER_NAME,
3536
ATTR_NET_PEER_PORT,
3637
ATTR_RPC_SYSTEM,
38+
ATTR_RPC_SERVICE,
3739
ATTR_SERVER_ADDRESS,
3840
ATTR_SERVER_PORT,
3941
ATTR_URL_QUERY,
42+
DB_SYSTEM_VALUES,
4043
EXCEPTION_MESSAGE,
4144
EXCEPTION_STACKTRACE,
4245
EXCEPTION_TYPE,
@@ -111,10 +114,12 @@ module.exports = class NrSpanProcessor {
111114
} else if (span.kind === SpanKind.CLIENT && (span.attributes[ATTR_HTTP_METHOD] || span.attributes[ATTR_HTTP_REQUEST_METHOD])) {
112115
this.reconcileHttpExternalAttributes({ segment, span })
113116
}
117+
118+
this.addAWSLinkingAttributes({ segment, span })
114119
}
115120

116121
reconcileHttpExternalAttributes({ segment, span }) {
117-
const noOpMapper = () => {}
122+
const noOpMapper = () => { }
118123
const statusCode = (value) => segment.addSpanAttribute('http.statusCode', value)
119124
const statusText = (value) => segment.addSpanAttribute('http.statusText', value)
120125
const mapper = {
@@ -165,6 +170,7 @@ module.exports = class NrSpanProcessor {
165170
[ATTR_MESSAGING_DESTINATION_NAME]: queueNameMapper,
166171
[ATTR_MESSAGING_DESTINATION]: queueNameMapper
167172
}
173+
168174
this.#reconciler.reconcile({ segment: baseSegment, otelSpan: span, mapper })
169175

170176
transaction.end()
@@ -258,7 +264,7 @@ module.exports = class NrSpanProcessor {
258264
* to be skipped because it will be the raw value.
259265
*/
260266
},
261-
[ATTR_DB_STATEMENT]: () => {}
267+
[ATTR_DB_STATEMENT]: () => { }
262268
}
263269
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
264270
}
@@ -270,6 +276,44 @@ module.exports = class NrSpanProcessor {
270276
[ATTR_MESSAGING_MESSAGE_CONVERSATION_ID]: (value) => segment.addAttribute('correlation_id', value),
271277
[ATTR_MESSAGING_RABBITMQ_DESTINATION_ROUTING_KEY]: (value) => segment.addAttribute('routing_key', value)
272278
}
279+
273280
this.#reconciler.reconcile({ segment, otelSpan: span, mapper })
274281
}
282+
283+
addAWSLinkingAttributes({ segment, span }) {
284+
const accountId = this.agent?.config?.cloud?.aws?.account_id
285+
const region = span.attributes['faas.invoked_region'] ?? span.attributes['aws.region'] // faas.invoked_region is for Lambda and takes precedence
286+
287+
// DynamoDB
288+
if (span.attributes[ATTR_DB_SYSTEM] === DB_SYSTEM_VALUES.DYNAMODB ||
289+
span.attributes[ATTR_RPC_SERVICE]?.toLowerCase() === 'dynamodb'
290+
) {
291+
const collection = span.attributes?.['aws.dynamodb.table_names'][0]
292+
if (region && accountId && collection) {
293+
segment.addAttribute('cloud.resource_id', `arn:aws:dynamodb:${region}:${accountId}:table/${collection}`)
294+
}
295+
}
296+
297+
// Lambda
298+
if (span.attributes[ATTR_FAAS_INVOKED_PROVIDER] === 'aws') {
299+
const functionName = span.attributes['faas.invoked_name']
300+
if (region && accountId && functionName) {
301+
segment.addAttribute('cloud.platform', 'aws_lambda')
302+
segment.addAttribute('cloud.resource_id', `arn:aws:lambda:${region}:${accountId}:function:${functionName}`)
303+
}
304+
}
305+
306+
// SQS
307+
if (span.attributes[ATTR_RPC_SERVICE]?.toLowerCase() === 'sqs') {
308+
if (accountId) {
309+
segment.addAttribute('cloud.account.id', accountId)
310+
}
311+
if (region) {
312+
segment.addAttribute('cloud.region', region)
313+
}
314+
const messagingDestination = span.attributes[ATTR_MESSAGING_DESTINATION]
315+
segment.addAttribute('messaging.destination.name', messagingDestination)
316+
segment.addAttribute('messaging.system', 'aws_sqs')
317+
}
318+
}
275319
}

test/versioned/otel-bridge/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
]
1616
}
1717
]
18-
}
18+
}

test/versioned/otel-bridge/span.test.js

+86-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const {
2020
ATTR_DB_STATEMENT,
2121
ATTR_DB_SYSTEM,
2222
ATTR_GRPC_STATUS_CODE,
23+
ATTR_FAAS_INVOKED_PROVIDER,
2324
ATTR_FULL_URL,
2425
ATTR_HTTP_HOST,
2526
ATTR_HTTP_METHOD,
@@ -56,6 +57,12 @@ test.beforeEach((ctx) => {
5657
const agent = helper.instrumentMockedAgent({
5758
feature_flag: {
5859
opentelemetry_bridge: true
60+
},
61+
// for AWS entity linking tests
62+
cloud: {
63+
aws: {
64+
account_id: 123456789123
65+
}
5966
}
6067
})
6168
const api = helper.getAgentApi()
@@ -289,14 +296,14 @@ test('client span(db) is bridge accordingly(statement test)', (t, end) => {
289296
const metrics = tx.metrics.scoped[tx.name]
290297
assert.equal(metrics['Datastore/statement/postgresql/test/select'].callCount, 1)
291298
const unscopedMetrics = tx.metrics.unscoped
292-
;[
299+
;[
293300
'Datastore/all',
294301
'Datastore/allWeb',
295302
'Datastore/postgresql/all',
296303
'Datastore/postgresql/allWeb',
297304
'Datastore/operation/postgresql/select',
298305
'Datastore/statement/postgresql/test/select',
299-
`Datastore/instance/postgresql/${expectedHost}/5436`
306+
`Datastore/instance/postgresql/${expectedHost}/5436`
300307
].forEach((expectedMetric) => {
301308
assert.equal(unscopedMetrics[expectedMetric].callCount, 1)
302309
})
@@ -338,13 +345,13 @@ test('client span(db) is bridged accordingly(operation test)', (t, end) => {
338345
const metrics = tx.metrics.scoped[tx.name]
339346
assert.equal(metrics['Datastore/operation/redis/hset'].callCount, 1)
340347
const unscopedMetrics = tx.metrics.unscoped
341-
;[
348+
;[
342349
'Datastore/all',
343350
'Datastore/allWeb',
344351
'Datastore/redis/all',
345352
'Datastore/redis/allWeb',
346353
'Datastore/operation/redis/hset',
347-
`Datastore/instance/redis/${expectedHost}/5436`
354+
`Datastore/instance/redis/${expectedHost}/5436`
348355
].forEach((expectedMetric) => {
349356
assert.equal(unscopedMetrics[expectedMetric].callCount, 1)
350357
})
@@ -808,6 +815,81 @@ test('Span errors are not added on transaction when span status code is not erro
808815
})
809816
})
810817

818+
test('aws dynamodb span has correct entity linking attributes', (t, end) => {
819+
const { agent, tracer } = t.nr
820+
const attributes = {
821+
[ATTR_DB_NAME]: 'test-db',
822+
[ATTR_DB_SYSTEM]: DB_SYSTEM_VALUES.DYNAMODB,
823+
[ATTR_DB_STATEMENT]: 'select foo from test-table where foo = "bar"',
824+
'aws.region': 'us-east-1',
825+
'aws.dynamodb.table_names': ['test-table']
826+
}
827+
helper.runInTransaction(agent, (tx) => {
828+
tx.name = 'db-test'
829+
tracer.startActiveSpan('db-test', { kind: otel.SpanKind.CLIENT, attributes }, (span) => {
830+
const segment = agent.tracer.getSegment()
831+
span.end()
832+
const duration = hrTimeToMilliseconds(span.duration)
833+
assert.equal(duration, segment.getDurationInMillis())
834+
tx.end()
835+
const attrs = segment.getAttributes()
836+
assert.equal(attrs['cloud.resource_id'], 'arn:aws:dynamodb:us-east-1:123456789123:table/test-table')
837+
end()
838+
})
839+
})
840+
})
841+
842+
test('aws lambda span has correct entity linking attributes', (t, end) => {
843+
const { agent, tracer } = t.nr
844+
// see: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.7.0/specification/trace/semantic_conventions/faas.md#example "Span A"
845+
const attributes = {
846+
'faas.invoked_name': 'test-function',
847+
[ATTR_FAAS_INVOKED_PROVIDER]: 'aws',
848+
'faas.invoked_region': 'us-east-1'
849+
}
850+
helper.runInTransaction(agent, (tx) => {
851+
tx.name = 'lambda-test'
852+
tracer.startActiveSpan('lambda-test', { kind: otel.SpanKind.CLIENT, attributes }, (span) => {
853+
const segment = agent.tracer.getSegment()
854+
span.end()
855+
const duration = hrTimeToMilliseconds(span.duration)
856+
assert.equal(duration, segment.getDurationInMillis())
857+
tx.end()
858+
const attrs = segment.getAttributes()
859+
assert.equal(attrs['cloud.resource_id'], 'arn:aws:lambda:us-east-1:123456789123:function:test-function')
860+
assert.equal(attrs['cloud.platform'], 'aws_lambda')
861+
end()
862+
})
863+
})
864+
})
865+
866+
test('aws sqs span has correct entity linking attributes', (t, end) => {
867+
const { agent, tracer } = t.nr
868+
// see: https://github.com/open-telemetry/opentelemetry-js-contrib/blob/b520d048465d9b3dfdf275976010c989d2a78a2c/plugins/node/opentelemetry-instrumentation-aws-sdk/src/services/sqs.ts#L62
869+
const attributes = {
870+
'rpc.service': 'sqs',
871+
[ATTR_MESSAGING_DESTINATION_KIND]: MESSAGING_SYSTEM_KIND_VALUES.QUEUE,
872+
[ATTR_MESSAGING_DESTINATION]: 'test-queue',
873+
'aws.region': 'us-east-1'
874+
}
875+
helper.runInTransaction(agent, (tx) => {
876+
tx.name = 'sqs-test'
877+
tracer.startActiveSpan('sqs-test', { kind: otel.SpanKind.PRODUCER, attributes }, (span) => {
878+
const segment = agent.tracer.getSegment()
879+
span.end()
880+
const duration = hrTimeToMilliseconds(span.duration)
881+
assert.equal(duration, segment.getDurationInMillis())
882+
tx.end()
883+
const attrs = segment.getAttributes()
884+
assert.equal(attrs['cloud.account.id'], agent.config.cloud.aws.account_id)
885+
assert.equal(attrs['cloud.region'], 'us-east-1')
886+
assert.equal(attrs['messaging.destination.name'], 'test-queue')
887+
assert.equal(attrs['messaging.system'], 'aws_sqs')
888+
end()
889+
})
890+
})
891+
})
892+
811893
function setupDtHeaders(agent) {
812894
agent.config.trusted_account_key = 1
813895
agent.config.primary_application_id = 2

0 commit comments

Comments
 (0)