Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading