4545import static io .opentelemetry .semconv .SemanticAttributes .SERVER_SOCKET_ADDRESS ;
4646import static io .opentelemetry .semconv .SemanticAttributes .SERVER_SOCKET_PORT ;
4747import static io .opentelemetry .semconv .SemanticAttributes .URL_FULL ;
48+ import static software .amazon .opentelemetry .javaagent .providers .AwsApplicationSignalsCustomizerProvider .LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT ;
4849import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_AGENT_ID ;
4950import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_BUCKET_NAME ;
5051import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER ;
5152import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_DATA_SOURCE_ID ;
5253import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_GUARDRAIL_ARN ;
5354import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_GUARDRAIL_ID ;
5455import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_KNOWLEDGE_BASE_ID ;
56+ import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_LAMBDA_ARN ;
57+ import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_LAMBDA_NAME ;
5558import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_LAMBDA_RESOURCE_ID ;
5659import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_LOCAL_OPERATION ;
5760import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_LOCAL_SERVICE ;
5861import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_QUEUE_NAME ;
5962import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_QUEUE_URL ;
6063import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_REMOTE_DB_USER ;
64+ import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_REMOTE_ENVIRONMENT ;
6165import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_REMOTE_OPERATION ;
6266import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_REMOTE_RESOURCE_IDENTIFIER ;
6367import static software .amazon .opentelemetry .javaagent .providers .AwsAttributeKeys .AWS_REMOTE_RESOURCE_TYPE ;
@@ -129,6 +133,9 @@ final class AwsMetricAttributeGenerator implements MetricAttributeGenerator {
129133 private static final String NORMALIZED_SECRETSMANAGER_SERVICE_NAME = "AWS::SecretsManager" ;
130134 private static final String NORMALIZED_LAMBDA_SERVICE_NAME = "AWS::Lambda" ;
131135
136+ // Constants for Lambda operations
137+ private static final String LAMBDA_INVOKE_OPERATION = "Invoke" ;
138+
132139 // Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
133140 private static final String GRAPHQL = "graphql" ;
134141
@@ -167,6 +174,7 @@ private Attributes generateDependencyMetricAttributes(SpanData span, Resource re
167174 setService (resource , span , builder );
168175 setEgressOperation (span , builder );
169176 setRemoteServiceAndOperation (span , builder );
177+ setRemoteEnvironment (span , builder );
170178 setRemoteResourceTypeAndIdentifier (span , builder );
171179 setSpanKindForDependency (span , builder );
172180 setHttpStatus (span , builder );
@@ -291,6 +299,27 @@ private static void setRemoteServiceAndOperation(SpanData span, AttributesBuilde
291299 builder .put (AWS_REMOTE_OPERATION , remoteOperation );
292300 }
293301
302+ /**
303+ * Remote environment is used to identify the environment of downstream services. Currently only
304+ * set to "lambda:default" for Lambda Invoke operations when aws-api system is detected.
305+ */
306+ private static void setRemoteEnvironment (SpanData span , AttributesBuilder builder ) {
307+ // We want to treat downstream Lambdas as a service rather than a resource because
308+ // Application Signals topology map gets disconnected due to conflicting Lambda Entity
309+ // definitions
310+ // Additional context can be found in
311+ // https://github.com/aws-observability/aws-otel-python-instrumentation/pull/319
312+ if (isLambdaInvokeOperation (span )) {
313+ // TODO: This should be passed via ConfigProperties from
314+ // AwsApplicationSignalsCustomizerProvider
315+ String remoteEnvironment =
316+ Optional .ofNullable (System .getenv (LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT ))
317+ .filter (s -> !s .isEmpty ())
318+ .orElse ("default" );
319+ builder .put (AWS_REMOTE_ENVIRONMENT , "lambda:" + remoteEnvironment );
320+ }
321+ }
322+
294323 /**
295324 * When the remote call operation is undetermined for http use cases, will try to extract the
296325 * remote operation name from http url string
@@ -373,6 +402,15 @@ private static String generateRemoteService(SpanData span) {
373402 return remoteService ;
374403 }
375404
405+ private static boolean isLambdaInvokeOperation (SpanData span ) {
406+ if (!isAwsSDKSpan (span )) {
407+ return false ;
408+ }
409+ String rpcService = getRemoteService (span , RPC_SERVICE );
410+ return ("AWSLambda" .equals (rpcService ) || "Lambda" .equals (rpcService ))
411+ && LAMBDA_INVOKE_OPERATION .equals (span .getAttributes ().get (RPC_METHOD ));
412+ }
413+
376414 /**
377415 * If the span is an AWS SDK span, normalize the name to align with <a
378416 * href="https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html">AWS
@@ -420,7 +458,19 @@ private static String normalizeRemoteServiceName(SpanData span, String serviceNa
420458 return NORMALIZED_SECRETSMANAGER_SERVICE_NAME ;
421459 case "AWSLambda" : // AWS SDK v1
422460 case "Lambda" : // AWS SDK v2
423- return NORMALIZED_LAMBDA_SERVICE_NAME ;
461+ if (isLambdaInvokeOperation (span )) {
462+ // AWS_LAMBDA_NAME can contain either a function name or function ARN since Lambda AWS
463+ // SDK calls accept both formats
464+ Optional <String > lambdaFunctionName =
465+ getLambdaFunctionNameFromArn (
466+ Optional .ofNullable (span .getAttributes ().get (AWS_LAMBDA_NAME )));
467+ // If Lambda name is not present, use UnknownRemoteService
468+ // This is intentional - we want to clearly indicate when the Lambda function name
469+ // is missing rather than falling back to a generic service name
470+ return lambdaFunctionName .orElse (UNKNOWN_REMOTE_SERVICE );
471+ } else {
472+ return NORMALIZED_LAMBDA_SERVICE_NAME ;
473+ }
424474 default :
425475 return "AWS::" + serviceName ;
426476 }
@@ -518,6 +568,19 @@ private static void setRemoteResourceTypeAndIdentifier(SpanData span, Attributes
518568 Optional .ofNullable (escapeDelimiters (span .getAttributes ().get (AWS_SECRET_ARN ))));
519569 cloudformationPrimaryIdentifier =
520570 Optional .ofNullable (escapeDelimiters (span .getAttributes ().get (AWS_SECRET_ARN )));
571+ } else if (isKeyPresent (span , AWS_LAMBDA_NAME )) {
572+ // For non-Invoke Lambda operations, treat Lambda as a resource,
573+ // see normalizeRemoteServiceName for more information.
574+ if (!isLambdaInvokeOperation (span )) {
575+ remoteResourceType = Optional .of (NORMALIZED_LAMBDA_SERVICE_NAME + "::Function" );
576+ // AWS_LAMBDA_NAME can contain either a function name or function ARN since Lambda AWS SDK
577+ // calls accept both formats
578+ remoteResourceIdentifier =
579+ getLambdaFunctionNameFromArn (
580+ Optional .ofNullable (escapeDelimiters (span .getAttributes ().get (AWS_LAMBDA_NAME ))));
581+ cloudformationPrimaryIdentifier =
582+ Optional .ofNullable (escapeDelimiters (span .getAttributes ().get (AWS_LAMBDA_ARN )));
583+ }
521584 } else if (isKeyPresent (span , AWS_LAMBDA_RESOURCE_ID )) {
522585 remoteResourceType = Optional .of (NORMALIZED_LAMBDA_SERVICE_NAME + "::EventSourceMapping" );
523586 remoteResourceIdentifier =
@@ -539,6 +602,14 @@ private static void setRemoteResourceTypeAndIdentifier(SpanData span, Attributes
539602 }
540603 }
541604
605+ private static Optional <String > getLambdaFunctionNameFromArn (Optional <String > stringArn ) {
606+ if (stringArn .isPresent () && stringArn .get ().startsWith ("arn:aws:lambda:" )) {
607+ Arn resourceArn = Arn .fromString (stringArn .get ());
608+ return Optional .of (resourceArn .getResource ().toString ().split (":" )[1 ]);
609+ }
610+ return stringArn ;
611+ }
612+
542613 private static Optional <String > getSecretsManagerResourceNameFromArn (Optional <String > stringArn ) {
543614 Arn resourceArn = Arn .fromString (stringArn .get ());
544615 return Optional .of (resourceArn .getResource ().toString ().split (":" )[1 ]);
0 commit comments