103103_NORMALIZED_LAMBDA_SERVICE_NAME : str = "AWS::Lambda"
104104_DB_CONNECTION_STRING_TYPE : str = "DB::Connection"
105105
106+ # Constants for Lambda operations
107+ _LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT : str = "LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT"
108+ _LAMBDA_INVOKE_OPERATION : str = "Invoke"
109+
106110# Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
107111_GRAPHQL : str = "graphql"
108112
@@ -145,6 +149,7 @@ def _generate_dependency_metric_attributes(span: ReadableSpan, resource: Resourc
145149 _set_egress_operation (span , attributes )
146150 _set_remote_service_and_operation (span , attributes )
147151 _set_remote_type_and_identifier (span , attributes )
152+ _set_remote_environment (span , attributes )
148153 _set_remote_db_user (span , attributes )
149154 _set_span_kind_for_dependency (span , attributes )
150155 return attributes
@@ -317,7 +322,17 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str
317322 "Secrets Manager" : _NORMALIZED_SECRETSMANAGER_SERVICE_NAME ,
318323 "SNS" : _NORMALIZED_SNS_SERVICE_NAME ,
319324 "SFN" : _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME ,
325+ "Lambda" : _NORMALIZED_LAMBDA_SERVICE_NAME ,
320326 }
327+
328+ # Special handling for Lambda invoke operations
329+ if _is_lambda_invoke_operation (span ):
330+ lambda_function_name = span .attributes .get (AWS_LAMBDA_FUNCTION_NAME )
331+ # If Lambda name is not present, use UnknownRemoteService
332+ # This is intentional - we want to clearly indicate when the Lambda function name
333+ # is missing rather than falling back to a generic service name
334+ return lambda_function_name if lambda_function_name else UNKNOWN_REMOTE_SERVICE
335+
321336 return aws_sdk_service_mapping .get (service_name , "AWS::" + service_name )
322337 return service_name
323338
@@ -450,22 +465,9 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
450465 )[- 1 ]
451466 cloudformation_primary_identifier = _escape_delimiters (span .attributes .get (AWS_STEPFUNCTIONS_ACTIVITY_ARN ))
452467 elif is_key_present (span , AWS_LAMBDA_FUNCTION_NAME ):
453- # Handling downstream Lambda as a service vs. an AWS resource:
454- # - If the method call is "Invoke", we treat downstream Lambda as a service.
455- # - Otherwise, we treat it as an AWS resource.
456- #
457- # This addresses a Lambda topology issue in Application Signals.
458- # More context in PR: https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
459- #
460- # NOTE: The env var LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT was introduced as part of this fix.
461- # It is optional and allows users to override the default value if needed.
462- if span .attributes .get (_RPC_METHOD ) == "Invoke" :
463- attributes [AWS_REMOTE_SERVICE ] = _escape_delimiters (span .attributes .get (AWS_LAMBDA_FUNCTION_NAME ))
464-
465- attributes [AWS_REMOTE_ENVIRONMENT ] = (
466- f'lambda:{ os .environ .get ("LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT" , "default" )} '
467- )
468- else :
468+ # For non-Invoke Lambda operations, treat Lambda as a resource,
469+ # see normalize_remote_service_name for more information.
470+ if not _is_lambda_invoke_operation (span ):
469471 remote_resource_type = _NORMALIZED_LAMBDA_SERVICE_NAME + "::Function"
470472 remote_resource_identifier = _escape_delimiters (span .attributes .get (AWS_LAMBDA_FUNCTION_NAME ))
471473 cloudformation_primary_identifier = _escape_delimiters (span .attributes .get (AWS_LAMBDA_FUNCTION_ARN ))
@@ -491,6 +493,32 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
491493 attributes [AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER ] = cloudformation_primary_identifier
492494
493495
496+ def _set_remote_environment (span : ReadableSpan , attributes : BoundedAttributes ) -> None :
497+ """
498+ Remote environment is used to identify the environment of downstream services. Currently only
499+ set to "lambda:default" for Lambda Invoke operations when aws-api system is detected.
500+ """
501+ # We want to treat downstream Lambdas as a service rather than a resource because
502+ # Application Signals topology map gets disconnected due to conflicting Lambda Entity
503+ # definitions
504+ # Additional context can be found in
505+ # https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
506+ if _is_lambda_invoke_operation (span ):
507+ remote_environment = os .environ .get (_LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT , "" ).strip ()
508+ if not remote_environment :
509+ remote_environment = "default"
510+ attributes [AWS_REMOTE_ENVIRONMENT ] = f"lambda:{ remote_environment } "
511+
512+
513+ def _is_lambda_invoke_operation (span : ReadableSpan ) -> bool :
514+ """Check if the span represents a Lambda Invoke operation."""
515+ if not is_aws_sdk_span (span ):
516+ return False
517+
518+ rpc_service = _get_remote_service (span , _RPC_SERVICE )
519+ return rpc_service == "Lambda" and span .attributes .get (_RPC_METHOD ) == _LAMBDA_INVOKE_OPERATION
520+
521+
494522def _get_db_connection (span : ReadableSpan ) -> None :
495523 """
496524 RemoteResourceIdentifier is populated with rule:
0 commit comments