Skip to content

Commit da8291e

Browse files
authored
Fix: Lambda Topology Issue (#149)
**Issue #, if available:** Lambda Topology Issue Context: aws-observability/aws-otel-python-instrumentation#319 **Description of changes:** - Apply fix for the Lambda Topology issue. The logic mimics the fix in our ADOT Python SDK. - Adding back logic to generate `aws.lambda.function.name` and `aws.lambda.function.arn` span attributes for Lambda. Previously, we removed these changes in the AWS Resources expansion due to the Lambda Topology issue. - More context about adding support for Lambda as AWS Resource: - aws-observability/aws-otel-python-instrumentation#265 **Test plan:** The same cases in the test plan of this [PR](aws-observability/aws-otel-python-instrumentation#319) were validated by building a custom JavaScript lambda layer. Below is a screenshot of all the test cases in a single topology. We can observe the following: - Service entity node for `Invoke` call to downstream lambda. - AWS Resource node for `GetFunction` call to downstream lambda. - AWS Resource node for `ListBuckets` call to downstream s3. - Another AWS Resource node for `GetBucketLocation` call to downstream s3. <img width="1359" alt="Screenshot 2025-02-05 at 10 15 26 AM" src="https://github.com/user-attachments/assets/14d38cad-32c7-4e2c-a987-c424c7fb7296" /> Here we see downstream lambda called with `Invoke` is correctly treated as RemoteService entity when not instrumented: ![image](https://github.com/user-attachments/assets/7f1cfcec-8827-4dc7-beaf-2a1b9b98e4f4) Additionally, I generated the spans locally to validate the new lambda instrumentation patch behaves correctly. **Generated span for `Invoke` call** We see the new `aws.remote.environment` attribute is present in span so the downstream lambda will be treated as a Service type in Application Signals backend. <img width="1124" alt="invoke" src="https://github.com/user-attachments/assets/81cefb44-c951-4ad0-8aeb-9a2064d7d0ea" /> **Generated span for `GetFunction` call** We see `aws.remote.resource.identifier` and `aws.cloudformation.primary.identifier` are set so the downstream lambda will be treated as an AWS resource type in Application Signals backend. <img width="1124" alt="get-function" src="https://github.com/user-attachments/assets/abfd700b-7966-4d93-bb47-868c31be9a34" /> 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 470c0d1 commit da8291e

File tree

5 files changed

+101
-1
lines changed

5 files changed

+101
-1
lines changed

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-attribute-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const AWS_ATTRIBUTE_KEYS: { [key: string]: string } = {
99
AWS_LOCAL_SERVICE: 'aws.local.service',
1010
AWS_LOCAL_OPERATION: 'aws.local.operation',
1111
AWS_REMOTE_SERVICE: 'aws.remote.service',
12+
AWS_REMOTE_ENVIRONMENT: 'aws.remote.environment',
1213
AWS_REMOTE_OPERATION: 'aws.remote.operation',
1314
AWS_REMOTE_RESOURCE_TYPE: 'aws.remote.resource.type',
1415
AWS_REMOTE_RESOURCE_IDENTIFIER: 'aws.remote.resource.identifier',

aws-distro-opentelemetry-node-autoinstrumentation/src/aws-metric-attribute-generator.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,33 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
409409
this.extractResourceNameFromArn(activityArn)
410410
);
411411
cloudFormationIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(activityArn);
412+
} else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME)) {
413+
// Handling downstream Lambda as a service vs. an AWS resource:
414+
// - If the method call is "Invoke", we treat downstream Lambda as a service.
415+
// - Otherwise, we treat it as an AWS resource.
416+
//
417+
// This addresses a Lambda topology issue in Application Signals.
418+
// More context in PR: https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
419+
//
420+
// NOTE: The env vars LAMBDA_APPLICATION_SIGNALS_REMOTE_SERVICE and
421+
// LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT were introduced as part of this fix.
422+
// They are optional and allow users to override the default values if needed.
423+
if (AwsMetricAttributeGenerator.getRemoteOperation(span, SEMATTRS_RPC_METHOD) === 'Invoke') {
424+
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_SERVICE] =
425+
process.env.LAMBDA_APPLICATION_SIGNALS_REMOTE_SERVICE ||
426+
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME];
427+
attributes[AWS_ATTRIBUTE_KEYS.AWS_REMOTE_ENVIRONMENT] = `lambda:${
428+
process.env.LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT || 'default'
429+
}`;
430+
} else {
431+
remoteResourceType = NORMALIZED_LAMBDA_SERVICE_NAME + '::Function';
432+
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(
433+
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME]
434+
);
435+
cloudFormationIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(
436+
span.attributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN]
437+
);
438+
}
412439
} else if (AwsSpanProcessingUtil.isKeyPresent(span, AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID)) {
413440
remoteResourceType = NORMALIZED_LAMBDA_SERVICE_NAME + '::EventSourceMapping';
414441
remoteResourceIdentifier = AwsMetricAttributeGenerator.escapeDelimiters(

aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,15 @@ import {
1010
ROOT_CONTEXT,
1111
TextMapGetter,
1212
trace,
13+
Span,
14+
Tracer,
1315
} from '@opentelemetry/api';
1416
import { Instrumentation } from '@opentelemetry/instrumentation';
15-
import { AwsSdkInstrumentationConfig, NormalizedRequest } from '@opentelemetry/instrumentation-aws-sdk';
17+
import {
18+
AwsSdkInstrumentationConfig,
19+
NormalizedRequest,
20+
NormalizedResponse,
21+
} from '@opentelemetry/instrumentation-aws-sdk';
1622
import { AWSXRAY_TRACE_ID_HEADER, AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
1723
import { APIGatewayProxyEventHeaders, Context } from 'aws-lambda';
1824
import { AWS_ATTRIBUTE_KEYS } from '../aws-attribute-keys';
@@ -215,10 +221,41 @@ function patchLambdaServiceExtension(lambdaServiceExtension: any): void {
215221
if (resourceMappingId) {
216222
requestMetadata.spanAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID] = resourceMappingId;
217223
}
224+
225+
const requestFunctionNameFormat = request.commandInput?.FunctionName;
226+
let functionName = requestFunctionNameFormat;
227+
228+
if (requestFunctionNameFormat) {
229+
if (requestFunctionNameFormat.startsWith('arn:aws:lambda')) {
230+
const split = requestFunctionNameFormat.split(':');
231+
functionName = split[split.length - 1];
232+
}
233+
requestMetadata.spanAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME] = functionName;
234+
}
218235
}
219236
return requestMetadata;
220237
};
221238

222239
lambdaServiceExtension.requestPreSpanHook = patchedRequestPreSpanHook;
240+
241+
if (typeof lambdaServiceExtension.responseHook === 'function') {
242+
const originalResponseHook = lambdaServiceExtension.responseHook;
243+
244+
lambdaServiceExtension.responseHook = (
245+
response: NormalizedResponse,
246+
span: Span,
247+
tracer: Tracer,
248+
config: AwsSdkInstrumentationConfig
249+
): void => {
250+
originalResponseHook.call(lambdaServiceExtension, response, span, tracer, config);
251+
252+
if (response.data && response.data.Configuration) {
253+
const functionArn = response.data.Configuration.FunctionArn;
254+
if (functionArn) {
255+
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, functionArn);
256+
}
257+
}
258+
};
259+
}
223260
}
224261
}

aws-distro-opentelemetry-node-autoinstrumentation/test/aws-metric-attribute-generator.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,16 @@ describe('AwsMetricAttributeGeneratorTest', () => {
774774
validateRemoteResourceAttributes('AWS::SecretsManager::Secret', 'testSecret');
775775
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_SECRETSMANAGER_SECRET_ARN, undefined);
776776

777+
// Validate behaviour of AWS_LAMBDA_FUNCTION_NAME and AWS_LAMBDA_FUNCTION_ARN
778+
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, 'aws_lambda_function_name');
779+
mockAttribute(
780+
AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN,
781+
'arn:aws:lambda:us-east-1:123456789012:function:aws_lambda_function_name'
782+
);
783+
validateRemoteResourceAttributes('AWS::Lambda::Function', 'aws_lambda_function_name');
784+
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, undefined);
785+
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, undefined);
786+
777787
// Validate behaviour of AWS_LAMBDA_RESOURCE_MAPPING_ID attribute then remove it.
778788
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID, 'aws_lambda_resource_mapping_id');
779789
validateRemoteResourceAttributes('AWS::Lambda::EventSourceMapping', 'aws_lambda_resource_mapping_id');

aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ const _SECRETS_ARN: string = 'arn:aws:secretsmanager:us-east-1:123456789123:secr
3838
const _UUID: string = 'random-uuid';
3939
const _TOPIC_ARN: string = 'arn:aws:sns:us-east-1:123456789012:mystack-mytopic-NZJ5JSMVGFIE';
4040
const _QUEUE_URL: string = 'https://sqs.us-east-1.amazonaws.com/123412341234/queueName';
41+
const _FUNCTION_NAME: string = 'testFunction';
42+
const _FUNCTION_ARN: string = `arn:aws:lambda:us-east-1:123456789012:function:${_FUNCTION_NAME}`;
4143
const _BEDROCK_AGENT_ID: string = 'agentId';
4244
const _BEDROCK_DATASOURCE_ID: string = 'DataSourceId';
4345
const _BEDROCK_GUARDRAIL_ID: string = 'GuardrailId';
@@ -165,6 +167,8 @@ describe('InstrumentationPatchTest', () => {
165167

166168
const lambdaAttributes: Attributes = doExtractLambdaAttributes(services);
167169
expect(lambdaAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID]).toBeUndefined();
170+
expect(lambdaAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME]).toBeUndefined();
171+
expect(lambdaAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN]).toBeUndefined();
168172
});
169173

170174
it('SFN without patching', () => {
@@ -228,6 +232,9 @@ describe('InstrumentationPatchTest', () => {
228232
const services: Map<string, any> = extractServicesFromAwsSdkInstrumentation(patchedAwsSdkInstrumentation);
229233
const requestLambdaAttributes: Attributes = doExtractLambdaAttributes(services);
230234
expect(requestLambdaAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID]).toEqual(_UUID);
235+
expect(requestLambdaAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME]).toEqual(_FUNCTION_NAME);
236+
const responseLambdaAttributes: Attributes = doResponseHookLambda(services);
237+
expect(responseLambdaAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN]).toEqual(_FUNCTION_ARN);
231238
});
232239

233240
it('SFN with patching', () => {
@@ -429,6 +436,7 @@ describe('InstrumentationPatchTest', () => {
429436
commandName: 'mockCommandName',
430437
commandInput: {
431438
UUID: _UUID,
439+
FunctionName: _FUNCTION_NAME,
432440
},
433441
};
434442
return doExtractAttributes(services, serviceName, params);
@@ -507,6 +515,23 @@ describe('InstrumentationPatchTest', () => {
507515
return doResponseHook(services, 'SecretsManager', results as NormalizedResponse);
508516
}
509517

518+
function doResponseHookLambda(services: Map<string, ServiceExtension>): Attributes {
519+
const results: Partial<NormalizedResponse> = {
520+
data: {
521+
Configuration: {
522+
FunctionArn: _FUNCTION_ARN,
523+
},
524+
},
525+
request: {
526+
commandInput: {},
527+
commandName: 'dummy_operation',
528+
serviceName: 'Lambda',
529+
},
530+
};
531+
532+
return doResponseHook(services, 'Lambda', results as NormalizedResponse);
533+
}
534+
510535
function doResponseHookBedrock(
511536
services: Map<string, ServiceExtension>,
512537
serviceName: string,

0 commit comments

Comments
 (0)