Skip to content

Commit 7ee5904

Browse files
authored
Propagate Lambda ResourceId or FaasId for the child spans (#104)
*Description of changes:* For Lambda case, the Lambda resource attribute only exists on Handler span, it needs to be propagated to its child spans. This PR includes the changes, 1. if parent span contains `cloud.resource_id` or `faas.i` but not in child span, child span will be propagated with one of these attribute from parent. 2. if both exist `cloud.resource_id` takes priority 3. if none of two Lambda resource attrs exists, do nothing. *Test:* **Hander Span** ``` 2024-10-16T17:53:20.216Z | 'faas.id': 'arn:aws:lambda:us-west-1:889414516288:function:aws-opentelemetry-distro-nodejs', -- | --   | 2024-10-16T17:53:20.216Z | 'cloud.account.id': '889414516288',   | 2024-10-16T17:53:20.216Z | 'aws.is.local.root': true,   | 2024-10-16T17:53:20.216Z | 'aws.local.service': 'aws-opentelemetry-distro-nodejs',   | 2024-10-16T17:53:20.216Z | 'aws.local.operation': 'aws-opentelemetry-distro-nodejs/Handler', ``` **Child Span** ```   | 2024-10-16T17:53:20.194Z | attributes: { -- | -- | --   | 2024-10-16T17:53:20.194Z | 'rpc.system': 'aws-api',   | 2024-10-16T17:53:20.194Z | 'rpc.method': 'ListBuckets',   | 2024-10-16T17:53:20.194Z | 'rpc.service': 'S3',   | 2024-10-16T17:53:20.194Z | 'aws.is.local.root': false,   | 2024-10-16T17:53:20.194Z | 'faas.id': 'arn:aws:lambda:us-west-1:889414516288:function:aws-opentelemetry-distro-nodejs',   | 2024-10-16T17:53:20.194Z | 'aws.local.operation': 'aws-opentelemetry-distro-nodejs/Handler', ``` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 2907367 commit 7ee5904

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/attribute-propagating-span-processor.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Span as APISpan, AttributeValue, Context, SpanKind, trace } from '@open
55
import { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base';
66
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
77
import { AwsSpanProcessingUtil } from './aws-span-processing-util';
8+
import { SEMRESATTRS_FAAS_ID } from '@opentelemetry/semantic-conventions';
89

910
/**
1011
* AttributePropagatingSpanProcessor handles the propagation of attributes from parent spans to
@@ -85,6 +86,18 @@ export class AttributePropagatingSpanProcessor implements SpanProcessor {
8586
if (this.isConsumerKind(span) && this.isConsumerKind(parentReadableSpan)) {
8687
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_CONSUMER_PARENT_SPAN_KIND, SpanKind[parentReadableSpan.kind]);
8788
}
89+
90+
// If parent span contains "cloud.resource_id" or "faas.id" but not in child span, child span will be
91+
// propagated with one of these attribute from parent. "cloud.resource_id" takes priority if it exists
92+
const parentResourceId = AwsSpanProcessingUtil.getResourceId(parentSpan);
93+
const resourceId = AwsSpanProcessingUtil.getResourceId(span);
94+
if (!resourceId && parentResourceId) {
95+
if (AwsSpanProcessingUtil.isKeyPresent(parentSpan, AwsSpanProcessingUtil.CLOUD_RESOURCE_ID)) {
96+
span.setAttribute(AwsSpanProcessingUtil.CLOUD_RESOURCE_ID, parentResourceId);
97+
} else {
98+
span.setAttribute(SEMRESATTRS_FAAS_ID, parentResourceId);
99+
}
100+
}
88101
}
89102

90103
let propagationData: AttributeValue | undefined = undefined;

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
SEMATTRS_HTTP_URL,
1616
SEMATTRS_MESSAGING_OPERATION,
1717
SEMATTRS_RPC_SYSTEM,
18+
SEMRESATTRS_FAAS_ID,
1819
} from '@opentelemetry/semantic-conventions';
1920
import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys';
2021
import { AWS_LAMBDA_FUNCTION_NAME_CONFIG, isLambdaEnvironment } from './aws-opentelemetry-configurator';
@@ -33,6 +34,9 @@ export class AwsSpanProcessingUtil {
3334
static LOCAL_ROOT: string = 'LOCAL_ROOT';
3435
static SQS_RECEIVE_MESSAGE_SPAN_NAME: string = 'Sqs.ReceiveMessage';
3536
static AWS_SDK_INSTRUMENTATION_SCOPE_PREFIX: string = '@opentelemetry/instrumentation-aws-sdk';
37+
// "cloud.resource_id" is defined in semconv which has not yet picked up by OTel JS
38+
// https://opentelemetry.io/docs/specs/semconv/attributes-registry/cloud/
39+
static CLOUD_RESOURCE_ID: string = 'cloud.resource_id';
3640

3741
// Max keyword length supported by parsing into remote_operation from DB_STATEMENT.
3842
// The current longest command word is DATETIME_INTERVAL_PRECISION at 27 characters.
@@ -273,4 +277,15 @@ export class AwsSpanProcessingUtil {
273277
const isLocalRoot: boolean = span.parentSpanId === undefined || !isParentSpanContextValid || isParentSpanRemote;
274278
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT, isLocalRoot);
275279
}
280+
281+
static getResourceId(span: ReadableSpan): string | undefined {
282+
let resourceId: AttributeValue | undefined = undefined;
283+
if (AwsSpanProcessingUtil.isKeyPresent(span, AwsSpanProcessingUtil.CLOUD_RESOURCE_ID)) {
284+
resourceId = span.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID];
285+
} else if (AwsSpanProcessingUtil.isKeyPresent(span, SEMRESATTRS_FAAS_ID)) {
286+
resourceId = span.attributes[SEMRESATTRS_FAAS_ID];
287+
}
288+
289+
return typeof resourceId === 'string' ? resourceId : undefined;
290+
}
276291
}

aws-distro-opentelemetry-node-autoinstrumentation/test/attribute-propagating-span-processor.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,44 @@ describe('AttributePropagatingSpanProcessorTest', () => {
240240
);
241241
});
242242

243+
it('testLambdaResourceIdAttributeExist', () => {
244+
const parentSpan: APISpan = tracer.startSpan('parent', { kind: SpanKind.SERVER });
245+
246+
parentSpan.setAttribute(AwsSpanProcessingUtil.CLOUD_RESOURCE_ID, 'resource-123');
247+
248+
const childSpan: APISpan = createNestedSpan(parentSpan, 1);
249+
expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).not.toBeUndefined();
250+
expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).toEqual('resource-123');
251+
});
252+
253+
it('testLambdaFaasIdAttributeExist', () => {
254+
const parentSpan: APISpan = tracer.startSpan('parent', { kind: SpanKind.SERVER });
255+
256+
parentSpan.setAttribute('faas.id', 'faas-123');
257+
258+
const childSpan: APISpan = createNestedSpan(parentSpan, 1);
259+
expect((childSpan as any).attributes['faas.id']).not.toBeUndefined();
260+
expect((childSpan as any).attributes['faas.id']).toEqual('faas-123');
261+
});
262+
263+
it('testBothLambdaFaasIdAndResourceIdAttributesExist', () => {
264+
const parentSpan: APISpan = tracer.startSpan('parent', { kind: SpanKind.SERVER });
265+
266+
parentSpan.setAttribute('faas.id', 'faas-123');
267+
parentSpan.setAttribute(AwsSpanProcessingUtil.CLOUD_RESOURCE_ID, 'resource-123');
268+
269+
const childSpan: APISpan = createNestedSpan(parentSpan, 1);
270+
expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).not.toBeUndefined();
271+
expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).toEqual('resource-123');
272+
});
273+
274+
it('testLambdaNoneResourceAttributesExist', () => {
275+
const parentSpan: APISpan = tracer.startSpan('parent', { kind: SpanKind.SERVER });
276+
277+
const childSpan: APISpan = createNestedSpan(parentSpan, 1);
278+
expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).toBeUndefined();
279+
});
280+
243281
function createNestedSpan(parentSpan: APISpan, depth: number): APISpan {
244282
if (depth === 0) {
245283
return parentSpan;

aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,36 @@ describe('AwsSpanProcessingUtilTest', () => {
377377
const actualOperation: string = AwsSpanProcessingUtil.getIngressOperation(spanDataMock);
378378
expect(actualOperation).toEqual('TestFunction/Handler');
379379
});
380+
381+
it('should return cloud.resource_id when present', () => {
382+
spanDataMock.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID] = 'cloud-123';
383+
const result = AwsSpanProcessingUtil.getResourceId(spanDataMock);
384+
expect(result).toBe('cloud-123');
385+
});
386+
387+
it('should return faas.id when cloud.resource_id is not present', () => {
388+
spanDataMock.attributes['faas.id'] = 'faas-123';
389+
const result = AwsSpanProcessingUtil.getResourceId(spanDataMock);
390+
expect(result).toBe('faas-123');
391+
});
392+
393+
it('should return cloud.resource_id when both cloud.resource_id and faas.id are present', () => {
394+
spanDataMock.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID] = 'cloud-123';
395+
spanDataMock.attributes['faas.id'] = 'faas-123';
396+
const result = AwsSpanProcessingUtil.getResourceId(spanDataMock);
397+
expect(result).toBe('cloud-123');
398+
});
399+
400+
it('should return undefined when neither cloud.resource_id nor faas.id are present', () => {
401+
const result = AwsSpanProcessingUtil.getResourceId(spanDataMock);
402+
expect(result).toBeUndefined();
403+
});
404+
405+
it('should return undefined if cloud.resource_id is not a string', () => {
406+
spanDataMock.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID] = 123; // Incorrect type
407+
const result = AwsSpanProcessingUtil.getResourceId(spanDataMock);
408+
expect(result).toBeUndefined();
409+
});
380410
});
381411

382412
function createMockSpanContext(): SpanContext {

0 commit comments

Comments
 (0)