diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/attribute-propagating-span-processor.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/attribute-propagating-span-processor.ts index b80e8d9f..d084998f 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/attribute-propagating-span-processor.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/attribute-propagating-span-processor.ts @@ -5,6 +5,7 @@ import { Span as APISpan, AttributeValue, Context, SpanKind, trace } from '@open import { ReadableSpan, Span, SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys'; import { AwsSpanProcessingUtil } from './aws-span-processing-util'; +import { SEMRESATTRS_FAAS_ID } from '@opentelemetry/semantic-conventions'; /** * AttributePropagatingSpanProcessor handles the propagation of attributes from parent spans to @@ -85,6 +86,18 @@ export class AttributePropagatingSpanProcessor implements SpanProcessor { if (this.isConsumerKind(span) && this.isConsumerKind(parentReadableSpan)) { span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_CONSUMER_PARENT_SPAN_KIND, SpanKind[parentReadableSpan.kind]); } + + // If parent span contains "cloud.resource_id" or "faas.id" but not in child span, child span will be + // propagated with one of these attribute from parent. "cloud.resource_id" takes priority if it exists + const parentResourceId = AwsSpanProcessingUtil.getResourceId(parentSpan); + const resourceId = AwsSpanProcessingUtil.getResourceId(span); + if (!resourceId && parentResourceId) { + if (AwsSpanProcessingUtil.isKeyPresent(parentSpan, AwsSpanProcessingUtil.CLOUD_RESOURCE_ID)) { + span.setAttribute(AwsSpanProcessingUtil.CLOUD_RESOURCE_ID, parentResourceId); + } else { + span.setAttribute(SEMRESATTRS_FAAS_ID, parentResourceId); + } + } } let propagationData: AttributeValue | undefined = undefined; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts index e6090a8a..65329724 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/src/aws-span-processing-util.ts @@ -15,6 +15,7 @@ import { SEMATTRS_HTTP_URL, SEMATTRS_MESSAGING_OPERATION, SEMATTRS_RPC_SYSTEM, + SEMRESATTRS_FAAS_ID, } from '@opentelemetry/semantic-conventions'; import { AWS_ATTRIBUTE_KEYS } from './aws-attribute-keys'; import { AWS_LAMBDA_FUNCTION_NAME_CONFIG, isLambdaEnvironment } from './aws-opentelemetry-configurator'; @@ -33,6 +34,9 @@ export class AwsSpanProcessingUtil { static LOCAL_ROOT: string = 'LOCAL_ROOT'; static SQS_RECEIVE_MESSAGE_SPAN_NAME: string = 'Sqs.ReceiveMessage'; static AWS_SDK_INSTRUMENTATION_SCOPE_PREFIX: string = '@opentelemetry/instrumentation-aws-sdk'; + // "cloud.resource_id" is defined in semconv which has not yet picked up by OTel JS + // https://opentelemetry.io/docs/specs/semconv/attributes-registry/cloud/ + static CLOUD_RESOURCE_ID: string = 'cloud.resource_id'; // Max keyword length supported by parsing into remote_operation from DB_STATEMENT. // The current longest command word is DATETIME_INTERVAL_PRECISION at 27 characters. @@ -273,4 +277,15 @@ export class AwsSpanProcessingUtil { const isLocalRoot: boolean = span.parentSpanId === undefined || !isParentSpanContextValid || isParentSpanRemote; span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_IS_LOCAL_ROOT, isLocalRoot); } + + static getResourceId(span: ReadableSpan): string | undefined { + let resourceId: AttributeValue | undefined = undefined; + if (AwsSpanProcessingUtil.isKeyPresent(span, AwsSpanProcessingUtil.CLOUD_RESOURCE_ID)) { + resourceId = span.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]; + } else if (AwsSpanProcessingUtil.isKeyPresent(span, SEMRESATTRS_FAAS_ID)) { + resourceId = span.attributes[SEMRESATTRS_FAAS_ID]; + } + + return typeof resourceId === 'string' ? resourceId : undefined; + } } diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/attribute-propagating-span-processor.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/attribute-propagating-span-processor.test.ts index 605e32f6..9a97f2c4 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/attribute-propagating-span-processor.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/attribute-propagating-span-processor.test.ts @@ -240,6 +240,44 @@ describe('AttributePropagatingSpanProcessorTest', () => { ); }); + it('testLambdaResourceIdAttributeExist', () => { + const parentSpan: APISpan = tracer.startSpan('parent', { kind: SpanKind.SERVER }); + + parentSpan.setAttribute(AwsSpanProcessingUtil.CLOUD_RESOURCE_ID, 'resource-123'); + + const childSpan: APISpan = createNestedSpan(parentSpan, 1); + expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).not.toBeUndefined(); + expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).toEqual('resource-123'); + }); + + it('testLambdaFaasIdAttributeExist', () => { + const parentSpan: APISpan = tracer.startSpan('parent', { kind: SpanKind.SERVER }); + + parentSpan.setAttribute('faas.id', 'faas-123'); + + const childSpan: APISpan = createNestedSpan(parentSpan, 1); + expect((childSpan as any).attributes['faas.id']).not.toBeUndefined(); + expect((childSpan as any).attributes['faas.id']).toEqual('faas-123'); + }); + + it('testBothLambdaFaasIdAndResourceIdAttributesExist', () => { + const parentSpan: APISpan = tracer.startSpan('parent', { kind: SpanKind.SERVER }); + + parentSpan.setAttribute('faas.id', 'faas-123'); + parentSpan.setAttribute(AwsSpanProcessingUtil.CLOUD_RESOURCE_ID, 'resource-123'); + + const childSpan: APISpan = createNestedSpan(parentSpan, 1); + expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).not.toBeUndefined(); + expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).toEqual('resource-123'); + }); + + it('testLambdaNoneResourceAttributesExist', () => { + const parentSpan: APISpan = tracer.startSpan('parent', { kind: SpanKind.SERVER }); + + const childSpan: APISpan = createNestedSpan(parentSpan, 1); + expect((childSpan as any).attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID]).toBeUndefined(); + }); + function createNestedSpan(parentSpan: APISpan, depth: number): APISpan { if (depth === 0) { return parentSpan; diff --git a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts index 68d69c24..1119b587 100644 --- a/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts +++ b/aws-distro-opentelemetry-node-autoinstrumentation/test/aws-span-processing-util.test.ts @@ -377,6 +377,36 @@ describe('AwsSpanProcessingUtilTest', () => { const actualOperation: string = AwsSpanProcessingUtil.getIngressOperation(spanDataMock); expect(actualOperation).toEqual('TestFunction/Handler'); }); + + it('should return cloud.resource_id when present', () => { + spanDataMock.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID] = 'cloud-123'; + const result = AwsSpanProcessingUtil.getResourceId(spanDataMock); + expect(result).toBe('cloud-123'); + }); + + it('should return faas.id when cloud.resource_id is not present', () => { + spanDataMock.attributes['faas.id'] = 'faas-123'; + const result = AwsSpanProcessingUtil.getResourceId(spanDataMock); + expect(result).toBe('faas-123'); + }); + + it('should return cloud.resource_id when both cloud.resource_id and faas.id are present', () => { + spanDataMock.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID] = 'cloud-123'; + spanDataMock.attributes['faas.id'] = 'faas-123'; + const result = AwsSpanProcessingUtil.getResourceId(spanDataMock); + expect(result).toBe('cloud-123'); + }); + + it('should return undefined when neither cloud.resource_id nor faas.id are present', () => { + const result = AwsSpanProcessingUtil.getResourceId(spanDataMock); + expect(result).toBeUndefined(); + }); + + it('should return undefined if cloud.resource_id is not a string', () => { + spanDataMock.attributes[AwsSpanProcessingUtil.CLOUD_RESOURCE_ID] = 123; // Incorrect type + const result = AwsSpanProcessingUtil.getResourceId(spanDataMock); + expect(result).toBeUndefined(); + }); }); function createMockSpanContext(): SpanContext {