Skip to content

Commit a97f9bf

Browse files
committed
lambda topology issue fix
1 parent 470c0d1 commit a97f9bf

File tree

6 files changed

+96
-1
lines changed

6 files changed

+96
-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 (span.attributes[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: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ 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 { AwsSdkInstrumentationConfig, NormalizedRequest, NormalizedResponse } from '@opentelemetry/instrumentation-aws-sdk';
1618
import { AWSXRAY_TRACE_ID_HEADER, AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
1719
import { APIGatewayProxyEventHeaders, Context } from 'aws-lambda';
1820
import { AWS_ATTRIBUTE_KEYS } from '../aws-attribute-keys';
@@ -215,10 +217,41 @@ function patchLambdaServiceExtension(lambdaServiceExtension: any): void {
215217
if (resourceMappingId) {
216218
requestMetadata.spanAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID] = resourceMappingId;
217219
}
220+
221+
const requestFunctionNameFormat = request.commandInput?.FunctionName;
222+
let functionName = requestFunctionNameFormat;
223+
224+
if (requestFunctionNameFormat) {
225+
if (requestFunctionNameFormat.startsWith('arn:aws:lambda')) {
226+
const split = requestFunctionNameFormat.split(':');
227+
functionName = split[split.length - 1];
228+
}
229+
requestMetadata.spanAttributes[AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME] = functionName;
230+
}
218231
}
219232
return requestMetadata;
220233
};
221234

222235
lambdaServiceExtension.requestPreSpanHook = patchedRequestPreSpanHook;
236+
237+
if (typeof lambdaServiceExtension.responseHook === 'function') {
238+
const originalResponseHook = lambdaServiceExtension.responseHook;
239+
240+
lambdaServiceExtension.responseHook = (
241+
response: NormalizedResponse,
242+
span: Span,
243+
tracer: Tracer,
244+
config: AwsSdkInstrumentationConfig
245+
): void => {
246+
originalResponseHook.call(lambdaServiceExtension, response, span, tracer, config);
247+
248+
if (response.data && response.data.Configuration) {
249+
const functionArn = response.data.Configuration.FunctionArn;
250+
if (functionArn) {
251+
span.setAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, functionArn);
252+
}
253+
}
254+
};
255+
}
223256
}
224257
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,13 @@ 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(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, 'arn:aws:lambda:us-east-1:123456789012:function:aws_lambda_function_name');
780+
validateRemoteResourceAttributes('AWS::Lambda::Function', 'aws_lambda_function_name');
781+
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_NAME, undefined);
782+
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_FUNCTION_ARN, undefined);
783+
777784
// Validate behaviour of AWS_LAMBDA_RESOURCE_MAPPING_ID attribute then remove it.
778785
mockAttribute(AWS_ATTRIBUTE_KEYS.AWS_LAMBDA_RESOURCE_MAPPING_ID, 'aws_lambda_resource_mapping_id');
779786
validateRemoteResourceAttributes('AWS::Lambda::EventSourceMapping', 'aws_lambda_resource_mapping_id');

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

Lines changed: 27 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,25 @@ describe('InstrumentationPatchTest', () => {
507515
return doResponseHook(services, 'SecretsManager', results as NormalizedResponse);
508516
}
509517

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

0 commit comments

Comments
 (0)