@@ -33,6 +33,7 @@ import {
3333 SERVICE_METRIC ,
3434} from './metric-attribute-generator' ;
3535import { SqsUrlParser } from './sqs-url-parser' ;
36+ import { LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT } from './aws-opentelemetry-configurator' ;
3637
3738// Does not exist in @opentelemetry /semantic-conventions
3839const _SERVER_SOCKET_ADDRESS : string = 'server.socket.address' ;
@@ -50,6 +51,9 @@ const _GRAPHQL_OPERATION_TYPE: string = 'graphql.operation.type';
5051// Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
5152const GRAPHQL : string = 'graphql' ;
5253
54+ // Constants for Lambda operations
55+ const LAMBDA_INVOKE_OPERATION : string = 'Invoke' ;
56+
5357// Normalized remote service names for supported AWS services
5458const NORMALIZED_DYNAMO_DB_SERVICE_NAME : string = 'AWS::DynamoDB' ;
5559const NORMALIZED_KINESIS_SERVICE_NAME : string = 'AWS::Kinesis' ;
@@ -109,6 +113,7 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
109113 AwsMetricAttributeGenerator . setEgressOperation ( span , attributes ) ;
110114 AwsMetricAttributeGenerator . setRemoteServiceAndOperation ( span , attributes ) ;
111115 AwsMetricAttributeGenerator . setRemoteResourceTypeAndIdentifier ( span , attributes ) ;
116+ AwsMetricAttributeGenerator . setRemoteEnvironment ( span , attributes ) ;
112117 AwsMetricAttributeGenerator . setSpanKindForDependency ( span , attributes ) ;
113118 AwsMetricAttributeGenerator . setRemoteDbUser ( span , attributes ) ;
114119
@@ -336,7 +341,18 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
336341 BedrockRuntime : NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME ,
337342 SecretsManager : NORMALIZED_SECRETSMANAGER_SERVICE_NAME ,
338343 SFN : NORMALIZED_STEPFUNCTIONS_SERVICE_NAME ,
344+ Lambda : NORMALIZED_LAMBDA_SERVICE_NAME ,
339345 } ;
346+
347+ // Special handling for Lambda invoke operations
348+ if ( AwsMetricAttributeGenerator . isLambdaInvokeOperation ( span ) ) {
349+ const lambdaFunctionName = span . attributes [ AWS_ATTRIBUTE_KEYS . AWS_LAMBDA_FUNCTION_NAME ] ;
350+ // If Lambda name is not present, use UnknownRemoteService
351+ // This is intentional - we want to clearly indicate when the Lambda function name
352+ // is missing rather than falling back to a generic service name
353+ return lambdaFunctionName ? String ( lambdaFunctionName ) : AwsSpanProcessingUtil . UNKNOWN_REMOTE_SERVICE ;
354+ }
355+
340356 return awsSdkServiceMapping [ serviceName ] || 'AWS::' + serviceName ;
341357 }
342358 return serviceName ;
@@ -410,24 +426,9 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
410426 ) ;
411427 cloudFormationIdentifier = AwsMetricAttributeGenerator . escapeDelimiters ( activityArn ) ;
412428 } 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 var LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT was introduced as part of this fix.
421- // It is optional and allow users to override the default value if needed.
422- if ( AwsMetricAttributeGenerator . getRemoteOperation ( span , SEMATTRS_RPC_METHOD ) === 'Invoke' ) {
423- attributes [ AWS_ATTRIBUTE_KEYS . AWS_REMOTE_SERVICE ] = AwsMetricAttributeGenerator . escapeDelimiters (
424- span . attributes [ AWS_ATTRIBUTE_KEYS . AWS_LAMBDA_FUNCTION_NAME ]
425- ) ;
426-
427- attributes [ AWS_ATTRIBUTE_KEYS . AWS_REMOTE_ENVIRONMENT ] = `lambda:${
428- process . env . LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT || 'default'
429- } `;
430- } else {
429+ // For non-Invoke Lambda operations, treat Lambda as a resource,
430+ // see normalizeRemoteServiceName for more information.
431+ if ( ! AwsMetricAttributeGenerator . isLambdaInvokeOperation ( span ) ) {
431432 remoteResourceType = NORMALIZED_LAMBDA_SERVICE_NAME + '::Function' ;
432433 remoteResourceIdentifier = AwsMetricAttributeGenerator . escapeDelimiters (
433434 span . attributes [ AWS_ATTRIBUTE_KEYS . AWS_LAMBDA_FUNCTION_NAME ]
@@ -491,17 +492,33 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
491492 remoteResourceIdentifier = AwsMetricAttributeGenerator . getDbConnection ( span ) ;
492493 }
493494
495+ if ( cloudFormationIdentifier === undefined ) {
496+ cloudFormationIdentifier = remoteResourceIdentifier ;
497+ }
498+
494499 if ( remoteResourceType !== undefined && remoteResourceIdentifier !== undefined ) {
495500 attributes [ AWS_ATTRIBUTE_KEYS . AWS_REMOTE_RESOURCE_TYPE ] = remoteResourceType ;
496501 attributes [ AWS_ATTRIBUTE_KEYS . AWS_REMOTE_RESOURCE_IDENTIFIER ] = remoteResourceIdentifier ;
502+ attributes [ AWS_ATTRIBUTE_KEYS . AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER ] = cloudFormationIdentifier ;
503+ }
504+ }
497505
498- if ( AwsSpanProcessingUtil . isAwsSDKSpan ( span ) ) {
499- if ( cloudFormationIdentifier === undefined ) {
500- cloudFormationIdentifier = remoteResourceIdentifier ;
501- }
502-
503- attributes [ AWS_ATTRIBUTE_KEYS . AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER ] = cloudFormationIdentifier ;
506+ /**
507+ * Remote environment is used to identify the environment of downstream services. Currently only
508+ * set to "lambda:default" for Lambda Invoke operations when aws-api system is detected.
509+ */
510+ private static setRemoteEnvironment ( span : ReadableSpan , attributes : Attributes ) : void {
511+ // We want to treat downstream Lambdas as a service rather than a resource because
512+ // Application Signals topology map gets disconnected due to conflicting Lambda Entity
513+ // definitions
514+ // Additional context can be found in
515+ // https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
516+ if ( AwsMetricAttributeGenerator . isLambdaInvokeOperation ( span ) ) {
517+ let remoteEnvironment = process . env [ LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT ] ?. trim ( ) ;
518+ if ( ! remoteEnvironment ) {
519+ remoteEnvironment = 'default' ;
504520 }
521+ attributes [ AWS_ATTRIBUTE_KEYS . AWS_REMOTE_ENVIRONMENT ] = `lambda:${ remoteEnvironment } ` ;
505522 }
506523 }
507524
@@ -620,6 +637,18 @@ export class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
620637 return input . split ( '^' ) . join ( '^^' ) . split ( '|' ) . join ( '^|' ) ;
621638 }
622639
640+ /**
641+ * Check if the span represents a Lambda Invoke operation.
642+ */
643+ private static isLambdaInvokeOperation ( span : ReadableSpan ) : boolean {
644+ if ( ! AwsSpanProcessingUtil . isAwsSDKSpan ( span ) ) {
645+ return false ;
646+ }
647+
648+ const rpcService = AwsMetricAttributeGenerator . getRemoteService ( span , SEMATTRS_RPC_SERVICE ) ;
649+ return rpcService === 'Lambda' && span . attributes [ SEMATTRS_RPC_METHOD ] === LAMBDA_INVOKE_OPERATION ;
650+ }
651+
623652 // Extracts the name of the resource from an arn
624653 private static extractResourceNameFromArn ( attribute : AttributeValue | undefined ) : string | undefined {
625654 if ( typeof attribute === 'string' && attribute . startsWith ( 'arn:aws:' ) ) {
0 commit comments