diff --git a/.github/patches/opentelemetry-java-instrumentation.patch b/.github/patches/opentelemetry-java-instrumentation.patch index 1de3294474..280bd01bad 100644 --- a/.github/patches/opentelemetry-java-instrumentation.patch +++ b/.github/patches/opentelemetry-java-instrumentation.patch @@ -1,42 +1,38 @@ diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt -index 93437ef1e0..4e9248fd01 100644 +index 93437ef1e0..3f564d25bc 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt @@ -1,2 +1,2 @@ --Comparing source compatibility of opentelemetry-instrumentation-annotations-2.11.0.jar against opentelemetry-instrumentation-annotations-2.10.0.jar + Comparing source compatibility of opentelemetry-instrumentation-annotations-2.11.0.jar against opentelemetry-instrumentation-annotations-2.10.0.jar -No changes. \ No newline at end of file -+Comparing source compatibility of opentelemetry-instrumentation-annotations-2.11.0-adot1.jar against opentelemetry-instrumentation-annotations-2.11.0.jar +No changes. diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt -index d759eed30a..1c725a0a25 100644 +index d759eed30a..385bd90663 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-api.txt @@ -1,2 +1,2 @@ --Comparing source compatibility of opentelemetry-instrumentation-api-2.11.0.jar against opentelemetry-instrumentation-api-2.10.0.jar + Comparing source compatibility of opentelemetry-instrumentation-api-2.11.0.jar against opentelemetry-instrumentation-api-2.10.0.jar -No changes. \ No newline at end of file -+Comparing source compatibility of opentelemetry-instrumentation-api-2.11.0-adot1.jar against opentelemetry-instrumentation-api-2.11.0.jar +No changes. diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt -index f657f219ae..a6ec574fe5 100644 +index f657f219ae..2b4a59db8f 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-autoconfigure.txt @@ -1,2 +1,2 @@ --Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.11.0.jar against opentelemetry-spring-boot-autoconfigure-2.10.0.jar + Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.11.0.jar against opentelemetry-spring-boot-autoconfigure-2.10.0.jar -No changes. \ No newline at end of file -+Comparing source compatibility of opentelemetry-spring-boot-autoconfigure-2.11.0-adot1.jar against opentelemetry-spring-boot-autoconfigure-2.11.0.jar +No changes. diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt -index 02f520fd45..2109c5a927 100644 +index 02f520fd45..99505334b7 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-spring-boot-starter.txt @@ -1,2 +1,2 @@ --Comparing source compatibility of opentelemetry-spring-boot-starter-2.11.0.jar against opentelemetry-spring-boot-starter-2.10.0.jar + Comparing source compatibility of opentelemetry-spring-boot-starter-2.11.0.jar against opentelemetry-spring-boot-starter-2.10.0.jar -No changes. \ No newline at end of file -+Comparing source compatibility of opentelemetry-spring-boot-starter-2.11.0-adot1.jar against opentelemetry-spring-boot-starter-2.11.0.jar +No changes. diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts index f357a19f88..fa90530579 100644 @@ -58,58 +54,132 @@ index f357a19f88..fa90530579 100644 testImplementation(project(":instrumentation:aws-sdk:aws-sdk-1.11:testing")) diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSpanAssertions.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSpanAssertions.java -index 483a0c5230..2415577e37 100644 +index 483a0c5230..5b1ee9ac4a 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSpanAssertions.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsSpanAssertions.java -@@ -85,11 +85,35 @@ class AwsSpanAssertions { +@@ -37,6 +37,7 @@ class AwsSpanAssertions { + satisfies(stringKey("aws.endpoint"), v -> v.isInstanceOf(String.class)), + equalTo(stringKey("aws.queue.name"), queueName), + equalTo(stringKey("aws.queue.url"), queueUrl), ++ equalTo(stringKey("aws.auth.account.access_key"), "test"), + satisfies(AWS_REQUEST_ID, v -> v.isInstanceOf(String.class)), + equalTo(RPC_METHOD, rpcMethod), + equalTo(RPC_SYSTEM, "aws-api"), +@@ -71,6 +72,7 @@ class AwsSpanAssertions { + equalTo(RPC_METHOD, rpcMethod), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "Amazon S3"), ++ equalTo(stringKey("aws.auth.account.access_key"), "test"), + equalTo(HTTP_REQUEST_METHOD, requestMethod), + equalTo(HTTP_RESPONSE_STATUS_CODE, responseStatusCode), + satisfies(URL_FULL, val -> val.startsWith("http://")), +@@ -85,28 +87,52 @@ class AwsSpanAssertions { } static SpanDataAssert sns(SpanDataAssert span, String topicArn, String rpcMethod) { -- ++ SpanDataAssert spanAssert = ++ span.hasName("SNS." + rpcMethod).hasKind(SpanKind.CLIENT).hasNoParent(); + - return span.hasName("SNS." + rpcMethod) -+ SpanDataAssert spanAssert = span.hasName("SNS." + rpcMethod) - .hasKind(SpanKind.CLIENT) +- .hasKind(SpanKind.CLIENT) - .hasNoParent() - .hasAttributesSatisfyingExactly( -+ .hasNoParent(); -+ -+ // For CreateTopic, the topicArn parameter might be null but aws.sns.topic.arn +- equalTo(stringKey("aws.agent"), "java-aws-sdk"), +- equalTo(MESSAGING_DESTINATION_NAME, topicArn), +- satisfies(stringKey("aws.endpoint"), v -> v.isInstanceOf(String.class)), +- satisfies(AWS_REQUEST_ID, v -> v.isInstanceOf(String.class)), +- equalTo(RPC_METHOD, rpcMethod), +- equalTo(RPC_SYSTEM, "aws-api"), +- equalTo(RPC_SERVICE, "AmazonSNS"), +- equalTo(HTTP_REQUEST_METHOD, "POST"), +- equalTo(HTTP_RESPONSE_STATUS_CODE, 200), +- satisfies(URL_FULL, val -> val.startsWith("http://")), +- satisfies(SERVER_ADDRESS, v -> v.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), +- satisfies( +- SERVER_PORT, +- val -> +- val.satisfiesAnyOf( +- v -> assertThat(v).isNull(), +- v -> assertThat(v).isInstanceOf(Number.class)))); ++ // For CreateTopic, the topicArn parameter might be null but aws.sns.topic.arn + // will be set from the response + if ("CreateTopic".equals(rpcMethod)) { + return spanAssert.hasAttributesSatisfyingExactly( -+ equalTo(stringKey("aws.agent"), "java-aws-sdk"), -+ satisfies(stringKey("aws.endpoint"), v -> v.isInstanceOf(String.class)), -+ satisfies(AWS_REQUEST_ID, v -> v.isInstanceOf(String.class)), -+ equalTo(RPC_METHOD, rpcMethod), -+ equalTo(RPC_SYSTEM, "aws-api"), -+ equalTo(RPC_SERVICE, "AmazonSNS"), -+ equalTo(HTTP_REQUEST_METHOD, "POST"), -+ equalTo(HTTP_RESPONSE_STATUS_CODE, 200), -+ satisfies(URL_FULL, val -> val.startsWith("http://")), -+ satisfies(SERVER_ADDRESS, v -> v.isInstanceOf(String.class)), -+ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), -+ satisfies( -+ SERVER_PORT, -+ val -> -+ val.satisfiesAnyOf( -+ v -> assertThat(v).isNull(), -+ v -> assertThat(v).isInstanceOf(Number.class))), -+ satisfies(stringKey("aws.sns.topic.arn"), v -> v.isInstanceOf(String.class))); ++ equalTo(stringKey("aws.agent"), "java-aws-sdk"), ++ satisfies(stringKey("aws.endpoint"), v -> v.isInstanceOf(String.class)), ++ satisfies(AWS_REQUEST_ID, v -> v.isInstanceOf(String.class)), ++ equalTo(RPC_METHOD, rpcMethod), ++ equalTo(RPC_SYSTEM, "aws-api"), ++ equalTo(RPC_SERVICE, "AmazonSNS"), ++ equalTo(stringKey("aws.auth.account.access_key"), "test"), ++ equalTo(HTTP_REQUEST_METHOD, "POST"), ++ equalTo(HTTP_RESPONSE_STATUS_CODE, 200), ++ satisfies(URL_FULL, val -> val.startsWith("http://")), ++ satisfies(SERVER_ADDRESS, v -> v.isInstanceOf(String.class)), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ satisfies( ++ SERVER_PORT, ++ val -> ++ val.satisfiesAnyOf( ++ v -> assertThat(v).isNull(), v -> assertThat(v).isInstanceOf(Number.class))), ++ satisfies(stringKey("aws.sns.topic.arn"), v -> v.isInstanceOf(String.class))); + } + + return spanAssert.hasAttributesSatisfyingExactly( - equalTo(stringKey("aws.agent"), "java-aws-sdk"), - equalTo(MESSAGING_DESTINATION_NAME, topicArn), - satisfies(stringKey("aws.endpoint"), v -> v.isInstanceOf(String.class)), -@@ -107,6 +131,7 @@ class AwsSpanAssertions { - val -> - val.satisfiesAnyOf( - v -> assertThat(v).isNull(), -- v -> assertThat(v).isInstanceOf(Number.class)))); -+ v -> assertThat(v).isInstanceOf(Number.class))), -+ equalTo(stringKey("aws.sns.topic.arn"), topicArn)); ++ equalTo(stringKey("aws.agent"), "java-aws-sdk"), ++ equalTo(MESSAGING_DESTINATION_NAME, topicArn), ++ satisfies(stringKey("aws.endpoint"), v -> v.isInstanceOf(String.class)), ++ satisfies(AWS_REQUEST_ID, v -> v.isInstanceOf(String.class)), ++ equalTo(RPC_METHOD, rpcMethod), ++ equalTo(RPC_SYSTEM, "aws-api"), ++ equalTo(RPC_SERVICE, "AmazonSNS"), ++ equalTo(stringKey("aws.auth.account.access_key"), "test"), ++ equalTo(HTTP_REQUEST_METHOD, "POST"), ++ equalTo(HTTP_RESPONSE_STATUS_CODE, 200), ++ satisfies(URL_FULL, val -> val.startsWith("http://")), ++ satisfies(SERVER_ADDRESS, v -> v.isInstanceOf(String.class)), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ satisfies( ++ SERVER_PORT, ++ val -> ++ val.satisfiesAnyOf( ++ v -> assertThat(v).isNull(), v -> assertThat(v).isInstanceOf(Number.class))), ++ equalTo(stringKey("aws.sns.topic.arn"), topicArn)); } } +diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/S3TracingTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/S3TracingTest.java +index 56eca09f8c..82c3379840 100644 +--- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/S3TracingTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/S3TracingTest.java +@@ -105,6 +105,7 @@ class S3TracingTest { + equalTo(RPC_METHOD, "ReceiveMessage"), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "AmazonSQS"), ++ equalTo(stringKey("aws.auth.account.access_key"), "test"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + satisfies(URL_FULL, val -> val.startsWith("http://")), +@@ -198,6 +199,7 @@ class S3TracingTest { + equalTo(RPC_METHOD, "ReceiveMessage"), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "AmazonSQS"), ++ equalTo(stringKey("aws.auth.account.access_key"), "test"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + satisfies(URL_FULL, val -> val.startsWith("http://")), +diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SnsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SnsTracingTest.java +index 429ca07938..d21918bc70 100644 +--- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SnsTracingTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/SnsTracingTest.java +@@ -89,6 +89,7 @@ class SnsTracingTest { + equalTo(RPC_METHOD, "ReceiveMessage"), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "AmazonSQS"), ++ equalTo(stringKey("aws.auth.account.access_key"), "test"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + satisfies(URL_FULL, val -> val.startsWith("http://")), diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts index 6cf49a21c4..3705634153 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts @@ -287,13 +357,16 @@ index 0000000000..e890cb3c0f + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java -index 096c7826a1..a271b16da8 100644 +index 096c7826a1..27613c04f2 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java -@@ -17,6 +17,36 @@ final class AwsExperimentalAttributes { +@@ -16,7 +16,41 @@ final class AwsExperimentalAttributes { + static final AttributeKey AWS_QUEUE_URL = stringKey("aws.queue.url"); static final AttributeKey AWS_QUEUE_NAME = stringKey("aws.queue.name"); static final AttributeKey AWS_STREAM_NAME = stringKey("aws.stream.name"); ++ static final AttributeKey AWS_STREAM_ARN = stringKey("aws.stream.arn"); static final AttributeKey AWS_TABLE_NAME = stringKey("aws.table.name"); ++ static final AttributeKey AWS_TABLE_ARN = stringKey("aws.table.arn"); + static final AttributeKey AWS_AGENT_ID = stringKey("aws.bedrock.agent.id"); + static final AttributeKey AWS_KNOWLEDGE_BASE_ID = + stringKey("aws.bedrock.knowledge_base.id"); @@ -322,20 +395,23 @@ index 096c7826a1..a271b16da8 100644 + static final AttributeKey AWS_SNS_TOPIC_ARN = stringKey("aws.sns.topic.arn"); + static final AttributeKey AWS_SECRET_ARN = stringKey("aws.secretsmanager.secret.arn"); + static final AttributeKey AWS_LAMBDA_NAME = stringKey("aws.lambda.function.name"); ++ static final AttributeKey AWS_LAMBDA_ARN = stringKey("aws.lambda.function.arn"); + static final AttributeKey AWS_LAMBDA_RESOURCE_ID = + stringKey("aws.lambda.resource_mapping.id"); ++ static final AttributeKey AWS_AUTH_ACCESS_KEY = stringKey("aws.auth.account.access_key"); private AwsExperimentalAttributes() {} } diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java -index 541e67d23b..1abf8e9c28 100644 +index 541e67d23b..5a321f9cb1 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java -@@ -6,12 +6,30 @@ +@@ -6,25 +6,56 @@ package io.opentelemetry.instrumentation.awssdk.v1_11; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_AGENT; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_AGENT_ID; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_AUTH_ACCESS_KEY; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_BEDROCK_RUNTIME_MODEL_ID; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_BEDROCK_SYSTEM; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_BUCKET_NAME; @@ -343,6 +419,7 @@ index 541e67d23b..1abf8e9c28 100644 +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_GUARDRAIL_ARN; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_GUARDRAIL_ID; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_KNOWLEDGE_BASE_ID; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_LAMBDA_ARN; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_LAMBDA_NAME; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_LAMBDA_RESOURCE_ID; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_QUEUE_NAME; @@ -351,7 +428,9 @@ index 541e67d23b..1abf8e9c28 100644 +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_SNS_TOPIC_ARN; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_STATE_MACHINE_ARN; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_STREAM_ARN; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_STREAM_NAME; ++import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_TABLE_ARN; import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_TABLE_NAME; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_MAX_TOKENS; +import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_TEMPERATURE; @@ -362,7 +441,9 @@ index 541e67d23b..1abf8e9c28 100644 import com.amazonaws.Request; import com.amazonaws.Response; -@@ -19,12 +37,17 @@ import io.opentelemetry.api.common.AttributeKey; ++import com.amazonaws.auth.AWSCredentials; ++import com.amazonaws.handlers.HandlerContextKey; + import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.context.Context; import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; @@ -377,10 +458,12 @@ index 541e67d23b..1abf8e9c28 100644 + private static final String BEDROCK_AGENT_SERVICE = "AWSBedrockAgent"; + private static final String BEDROCK_AGENT_RUNTIME_SERVICE = "AWSBedrockAgentRuntime"; + private static final String BEDROCK_RUNTIME_SERVICE = "AmazonBedrockRuntime"; ++ private static final HandlerContextKey AWS_CREDENTIALS = ++ new HandlerContextKey("AWSCredentials"); @Override public void onStart(AttributesBuilder attributes, Context parentContext, Request request) { -@@ -32,14 +55,155 @@ class AwsSdkExperimentalAttributesExtractor +@@ -32,14 +63,165 @@ class AwsSdkExperimentalAttributesExtractor attributes.put(AWS_ENDPOINT, request.getEndpoint().toString()); Object originalRequest = request.getOriginalRequest(); @@ -390,10 +473,18 @@ index 541e67d23b..1abf8e9c28 100644 - setRequestAttribute(attributes, AWS_STREAM_NAME, originalRequest, RequestAccess::getStreamName); - setRequestAttribute(attributes, AWS_TABLE_NAME, originalRequest, RequestAccess::getTableName); + String requestClassName = originalRequest.getClass().getSimpleName(); ++ AWSCredentials credentials = request.getHandlerContext(AWS_CREDENTIALS); ++ if (credentials != null) { ++ String accessKeyId = credentials.getAWSAccessKeyId(); ++ if (accessKeyId != null) { ++ attributes.put(AWS_AUTH_ACCESS_KEY, accessKeyId); ++ } ++ } + setAttribute(attributes, AWS_BUCKET_NAME, originalRequest, RequestAccess::getBucketName); + setAttribute(attributes, AWS_QUEUE_URL, originalRequest, RequestAccess::getQueueUrl); + setAttribute(attributes, AWS_QUEUE_NAME, originalRequest, RequestAccess::getQueueName); + setAttribute(attributes, AWS_STREAM_NAME, originalRequest, RequestAccess::getStreamName); ++ setAttribute(attributes, AWS_STREAM_ARN, originalRequest, RequestAccess::getStreamArn); + setAttribute(attributes, AWS_TABLE_NAME, originalRequest, RequestAccess::getTableName); + setAttribute( + attributes, AWS_STATE_MACHINE_ARN, originalRequest, RequestAccess::getStateMachineArn); @@ -413,8 +504,9 @@ index 541e67d23b..1abf8e9c28 100644 + if (isBedrockService(serviceName)) { + bedrockOnStart(attributes, originalRequest, requestClassName, serviceName); + } -+ } -+ + } + +- private static void setRequestAttribute( + @Override + public void onEnd( + AttributesBuilder attributes, @@ -424,6 +516,8 @@ index 541e67d23b..1abf8e9c28 100644 + @Nullable Throwable error) { + if (response != null) { + Object awsResp = response.getAwsResponse(); ++ setAttribute(attributes, AWS_TABLE_ARN, awsResp, RequestAccess::getTableArn); ++ setAttribute(attributes, AWS_LAMBDA_ARN, awsResp, RequestAccess::getLambdaArn); + setAttribute(attributes, AWS_STATE_MACHINE_ARN, awsResp, RequestAccess::getStateMachineArn); + setAttribute( + attributes, @@ -535,14 +629,13 @@ index 541e67d23b..1abf8e9c28 100644 + || serviceName.equals(BEDROCK_AGENT_SERVICE) + || serviceName.equals(BEDROCK_AGENT_RUNTIME_SERVICE) + || serviceName.equals(BEDROCK_RUNTIME_SERVICE); - } - -- private static void setRequestAttribute( ++ } ++ + private static void setAttribute( AttributesBuilder attributes, AttributeKey key, Object request, -@@ -49,12 +213,4 @@ class AwsSdkExperimentalAttributesExtractor +@@ -49,12 +231,4 @@ class AwsSdkExperimentalAttributesExtractor attributes.put(key, value); } } @@ -829,7 +922,7 @@ index 0000000000..d1acc5768a + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java -index c212a69678..3101685194 100644 +index c212a69678..82a7185abe 100644 --- a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java +++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java @@ -8,6 +8,12 @@ package io.opentelemetry.instrumentation.awssdk.v1_11; @@ -845,7 +938,7 @@ index c212a69678..3101685194 100644 import javax.annotation.Nullable; final class RequestAccess { -@@ -20,48 +26,392 @@ final class RequestAccess { +@@ -20,48 +26,417 @@ final class RequestAccess { } }; @@ -1075,6 +1168,14 @@ index c212a69678..3101685194 100644 + } + + @Nullable ++ static String getLambdaArn(Object request) { ++ if (request == null) { ++ return null; ++ } ++ return findNestedAccessorOrNull(request, "getConfiguration", "getFunctionArn"); ++ } ++ ++ @Nullable + static String getLambdaResourceId(Object request) { + if (request == null) { + return null; @@ -1164,6 +1265,23 @@ index c212a69678..3101685194 100644 return invokeOrNull(access.getTableName, request); } ++ @Nullable ++ static String getTableArn(Object request) { ++ if (request == null) { ++ return null; ++ } ++ return findNestedAccessorOrNull(request, "getTable", "getTableArn"); ++ } ++ ++ @Nullable ++ static String getStreamArn(Object request) { ++ if (request == null) { ++ return null; ++ } ++ RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); ++ return invokeOrNull(access.getStreamArn, request); ++ } ++ @Nullable static String getTopicArn(Object request) { + if (request == null) { @@ -1238,7 +1356,7 @@ index c212a69678..3101685194 100644 @Nullable private static String invokeOrNull(@Nullable MethodHandle method, Object obj) { if (method == null) { -@@ -74,6 +424,19 @@ final class RequestAccess { +@@ -74,31 +449,88 @@ final class RequestAccess { } } @@ -1258,7 +1376,8 @@ index c212a69678..3101685194 100644 @Nullable private final MethodHandle getBucketName; @Nullable private final MethodHandle getQueueUrl; @Nullable private final MethodHandle getQueueName; -@@ -81,24 +444,66 @@ final class RequestAccess { + @Nullable private final MethodHandle getStreamName; ++ @Nullable private final MethodHandle getStreamArn; @Nullable private final MethodHandle getTableName; @Nullable private final MethodHandle getTopicArn; @Nullable private final MethodHandle getTargetArn; @@ -1287,6 +1406,7 @@ index c212a69678..3101685194 100644 + getQueueUrl = findAccessorOrNull(clz, "getQueueUrl", String.class); + getQueueName = findAccessorOrNull(clz, "getQueueName", String.class); + getStreamName = findAccessorOrNull(clz, "getStreamName", String.class); ++ getStreamArn = findAccessorOrNull(clz, "getStreamARN", String.class); + getTableName = findAccessorOrNull(clz, "getTableName", String.class); + getTopicArn = findAccessorOrNull(clz, "getTopicArn", String.class); + getTargetArn = findAccessorOrNull(clz, "getTargetArn", String.class); @@ -1793,6 +1913,126 @@ index 0000000000..98a5873614 + }); + } +} +diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java +index 441a4a3a0b..529e317a65 100644 +--- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractDynamoDbClientTest.java +@@ -11,10 +11,12 @@ import static io.opentelemetry.semconv.incubating.AwsIncubatingAttributes.AWS_DY + import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; + import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DbSystemIncubatingValues.DYNAMODB; + import static java.util.Collections.singletonList; ++import static org.junit.Assert.assertEquals; + + import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; + import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; + import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; ++import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; + import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; + import io.opentelemetry.testing.internal.armeria.common.HttpResponse; + import io.opentelemetry.testing.internal.armeria.common.HttpStatus; +@@ -53,4 +55,39 @@ public abstract class AbstractDynamoDbClientTest extends AbstractBaseAwsClientTe + assertRequestWithMockedResponse( + response, client, "DynamoDBv2", "CreateTable", "POST", additionalAttributes); + } ++ ++ @Test ++ public void testGetTableArnWithMockedResponse() { ++ AmazonDynamoDBClientBuilder clientBuilder = AmazonDynamoDBClientBuilder.standard(); ++ AmazonDynamoDB client = ++ configureClient(clientBuilder) ++ .withEndpointConfiguration(endpoint) ++ .withCredentials(credentialsProvider) ++ .build(); ++ ++ String tableName = "MockTable"; ++ String expectedArn = "arn:aws:dynamodb:us-west-2:123456789012:table/" + tableName; ++ ++ String body = ++ "{\n" ++ + "\"Table\": {\n" ++ + "\"TableName\": \"" ++ + tableName ++ + "\",\n" ++ + "\"TableArn\": \"" ++ + expectedArn ++ + "\"\n" ++ + "}\n" ++ + "}"; ++ ++ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, body)); ++ ++ String actualArn = ++ client ++ .describeTable(new DescribeTableRequest().withTableName(tableName)) ++ .getTable() ++ .getTableArn(); ++ ++ assertEquals("Table ARN should match expected value", expectedArn, actualArn); ++ } + } +diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractKinesisClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractKinesisClientTest.java +index ee6d1b7501..a21b1ebefa 100644 +--- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractKinesisClientTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractKinesisClientTest.java +@@ -12,13 +12,16 @@ import static java.util.Collections.singletonList; + import com.amazonaws.services.kinesis.AmazonKinesis; + import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder; + import com.amazonaws.services.kinesis.model.DeleteStreamRequest; ++import com.amazonaws.services.kinesis.model.DescribeStreamRequest; + import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; + import io.opentelemetry.testing.internal.armeria.common.HttpResponse; + import io.opentelemetry.testing.internal.armeria.common.HttpStatus; + import io.opentelemetry.testing.internal.armeria.common.MediaType; ++import java.util.Arrays; + import java.util.List; + import java.util.function.Function; + import java.util.stream.Stream; ++import org.junit.Test; + import org.junit.jupiter.params.ParameterizedTest; + import org.junit.jupiter.params.provider.Arguments; + import org.junit.jupiter.params.provider.MethodSource; +@@ -54,6 +57,41 @@ public abstract class AbstractKinesisClientTest extends AbstractBaseAwsClientTes + response, client, "Kinesis", operation, "POST", additionalAttributes); + } + ++ @Test ++ public void sendRequestWithStreamArnMockedResponse() throws Exception { ++ AmazonKinesisClientBuilder clientBuilder = AmazonKinesisClientBuilder.standard(); ++ AmazonKinesis client = ++ configureClient(clientBuilder) ++ .withEndpointConfiguration(endpoint) ++ .withCredentials(credentialsProvider) ++ .build(); ++ ++ String body = ++ "{\n" ++ + "\"StreamDescription\": {\n" ++ + "\"StreamARN\": \"arn:aws:kinesis:us-east-1:123456789012:stream/somestream\",\n" ++ + "\"StreamName\": \"somestream\",\n" ++ + "\"StreamStatus\": \"ACTIVE\",\n" ++ + "\"Shards\": []\n" ++ + "}\n" ++ + "}"; ++ ++ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, body)); ++ ++ List additionalAttributes = ++ Arrays.asList( ++ equalTo(stringKey("aws.stream.name"), "somestream"), ++ equalTo( ++ stringKey("aws.stream.arn"), ++ "arn:aws:kinesis:us-east-1:123456789012:stream/somestream")); ++ ++ Object response = ++ client.describeStream(new DescribeStreamRequest().withStreamName("somestream")); ++ ++ assertRequestWithMockedResponse( ++ response, client, "Kinesis", "DescribeStream", "POST", additionalAttributes); ++ } ++ + private static Stream provideArguments() { + return Stream.of( + Arguments.of( diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractLambdaClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractLambdaClientTest.java new file mode 100644 index 0000000000..9f5a245ee7 @@ -1871,6 +2111,18 @@ index 0000000000..9f5a245ee7 + c -> c.getFunction(new GetFunctionRequest().withFunctionName("functionName")))); + } +} +diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractS3ClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractS3ClientTest.java +index 574165992f..5248d050b6 100644 +--- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractS3ClientTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractS3ClientTest.java +@@ -175,6 +175,7 @@ public abstract class AbstractS3ClientTest extends AbstractBaseAwsClientTest { + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "Amazon S3"), + equalTo(RPC_METHOD, "GetObject"), ++ equalTo(stringKey("aws.auth.account.access_key"), "my-access-key"), + equalTo(stringKey("aws.endpoint"), server.httpUri().toString()), + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.bucket.name"), "someBucket"), diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSecretsManagerClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSecretsManagerClientTest.java new file mode 100644 index 0000000000..03de6fce3f @@ -2038,6 +2290,174 @@ index 3f272ba477..bea20f3d86 100644 + response, client, "SNS", "Publish", "POST", additionalAttributes); } } +diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsSuppressReceiveSpansTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsSuppressReceiveSpansTest.java +index c0b4b13a17..4cfaf469d9 100644 +--- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsSuppressReceiveSpansTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsSuppressReceiveSpansTest.java +@@ -116,7 +116,8 @@ public abstract class AbstractSqsSuppressReceiveSpansTest { + equalTo(URL_FULL, "http://localhost:" + sqsPort), + equalTo(SERVER_ADDRESS, "localhost"), + equalTo(SERVER_PORT, sqsPort), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"))), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> +@@ -146,7 +147,8 @@ public abstract class AbstractSqsSuppressReceiveSpansTest { + equalTo(MESSAGING_OPERATION, "publish"), + satisfies( + MESSAGING_MESSAGE_ID, val -> val.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1")), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x")), + span -> + span.hasName("testSdkSqs process") + .hasKind(SpanKind.CONSUMER) +@@ -174,7 +176,8 @@ public abstract class AbstractSqsSuppressReceiveSpansTest { + equalTo(MESSAGING_OPERATION, "process"), + satisfies( + MESSAGING_MESSAGE_ID, val -> val.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1")), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x")), + span -> + span.hasName("process child") + .hasParent(trace.getSpan(1)) +@@ -222,7 +225,8 @@ public abstract class AbstractSqsSuppressReceiveSpansTest { + equalTo(URL_FULL, "http://localhost:" + sqsPort), + equalTo(SERVER_ADDRESS, "localhost"), + equalTo(SERVER_PORT, sqsPort), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"))), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> +@@ -252,7 +256,8 @@ public abstract class AbstractSqsSuppressReceiveSpansTest { + equalTo(MESSAGING_OPERATION, "publish"), + satisfies( + MESSAGING_MESSAGE_ID, val -> val.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1")), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x")), + span -> + span.hasName("testSdkSqs process") + .hasKind(SpanKind.CONSUMER) +@@ -280,7 +285,8 @@ public abstract class AbstractSqsSuppressReceiveSpansTest { + equalTo(MESSAGING_OPERATION, "process"), + satisfies( + MESSAGING_MESSAGE_ID, val -> val.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1")), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x")), + span -> + span.hasName("process child") + .hasParent(trace.getSpan(1)) +@@ -311,7 +317,8 @@ public abstract class AbstractSqsSuppressReceiveSpansTest { + equalTo(URL_FULL, "http://localhost:" + sqsPort), + equalTo(SERVER_ADDRESS, "localhost"), + equalTo(SERVER_PORT, sqsPort), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1")))); ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x")))); + } + + @Test +diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.java +index f1bfa126ca..dfb5b96550 100644 +--- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSqsTracingTest.java +@@ -150,7 +150,8 @@ public abstract class AbstractSqsTracingTest { + equalTo(URL_FULL, "http://localhost:" + sqsPort), + equalTo(SERVER_ADDRESS, "localhost"), + equalTo(SERVER_PORT, sqsPort), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"))), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> { +@@ -179,7 +180,8 @@ public abstract class AbstractSqsTracingTest { + equalTo(MESSAGING_OPERATION, "publish"), + satisfies( + MESSAGING_MESSAGE_ID, val -> val.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"))); ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))); + + if (testCaptureHeaders) { + attributes.add( +@@ -220,7 +222,8 @@ public abstract class AbstractSqsTracingTest { + equalTo(MESSAGING_DESTINATION_NAME, "testSdkSqs"), + equalTo(MESSAGING_OPERATION, "receive"), + equalTo(MESSAGING_BATCH_MESSAGE_COUNT, 1), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"))); ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))); + + if (testCaptureHeaders) { + attributes.add( +@@ -260,7 +263,8 @@ public abstract class AbstractSqsTracingTest { + equalTo(MESSAGING_OPERATION, "process"), + satisfies( + MESSAGING_MESSAGE_ID, val -> val.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"))); ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))); + + if (testCaptureHeaders) { + attributes.add( +@@ -320,7 +324,8 @@ public abstract class AbstractSqsTracingTest { + equalTo(URL_FULL, "http://localhost:" + sqsPort), + equalTo(SERVER_ADDRESS, "localhost"), + equalTo(SERVER_PORT, sqsPort), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"))), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))), + trace -> + trace.hasSpansSatisfyingExactly( + span -> +@@ -350,7 +355,8 @@ public abstract class AbstractSqsTracingTest { + equalTo(MESSAGING_OPERATION, "publish"), + satisfies( + MESSAGING_MESSAGE_ID, val -> val.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1"))), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))), + trace -> { + AtomicReference receiveSpan = new AtomicReference<>(); + AtomicReference processSpan = new AtomicReference<>(); +@@ -385,7 +391,8 @@ public abstract class AbstractSqsTracingTest { + equalTo(URL_FULL, "http://localhost:" + sqsPort), + equalTo(SERVER_ADDRESS, "localhost"), + equalTo(SERVER_PORT, sqsPort), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1")), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x")), + span -> + span.hasName("testSdkSqs receive") + .hasKind(SpanKind.CONSUMER) +@@ -419,7 +426,8 @@ public abstract class AbstractSqsTracingTest { + MessagingIncubatingAttributes + .MESSAGING_BATCH_MESSAGE_COUNT, + 1), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1")), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x")), + span -> + span.hasName("testSdkSqs process") + .hasKind(SpanKind.CONSUMER) +@@ -452,7 +460,8 @@ public abstract class AbstractSqsTracingTest { + satisfies( + MESSAGING_MESSAGE_ID, + val -> val.isInstanceOf(String.class)), +- equalTo(NETWORK_PROTOCOL_VERSION, "1.1")), ++ equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x")), + span -> + span.hasName("process child") + .hasParent(processSpan.get()) diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractStepFunctionsClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractStepFunctionsClientTest.java new file mode 100644 index 0000000000..fc58ec3c9b @@ -2166,10 +2586,10 @@ index 3b7381a8ba..6f77951710 100644 testing { diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsExperimentalAttributes.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsExperimentalAttributes.java new file mode 100644 -index 0000000000..4aed4a58c0 +index 0000000000..fd951ffe37 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsExperimentalAttributes.java -@@ -0,0 +1,73 @@ +@@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 @@ -2186,6 +2606,7 @@ index 0000000000..4aed4a58c0 + static final AttributeKey AWS_QUEUE_URL = stringKey("aws.queue.url"); + static final AttributeKey AWS_QUEUE_NAME = stringKey("aws.queue.name"); + static final AttributeKey AWS_STREAM_NAME = stringKey("aws.stream.name"); ++ static final AttributeKey AWS_STREAM_ARN = stringKey("aws.stream.arn"); + static final AttributeKey AWS_TABLE_NAME = stringKey("aws.table.name"); + static final AttributeKey AWS_GUARDRAIL_ID = stringKey("aws.bedrock.guardrail.id"); + static final AttributeKey AWS_GUARDRAIL_ARN = stringKey("aws.bedrock.guardrail.arn"); @@ -2232,6 +2653,12 @@ index 0000000000..4aed4a58c0 + static final AttributeKey AWS_LAMBDA_RESOURCE_ID = + stringKey("aws.lambda.resource_mapping.id"); + ++ static final AttributeKey AWS_TABLE_ARN = stringKey("aws.table.arn"); ++ ++ static final AttributeKey AWS_AUTH_ACCESS_KEY = stringKey("aws.auth.account.access_key"); ++ ++ static final AttributeKey AWS_AUTH_REGION = stringKey("aws.auth.region"); ++ + static boolean isGenAiAttribute(String attributeKey) { + return attributeKey.equals(GEN_AI_REQUEST_MAX_TOKENS.getKey()) + || attributeKey.equals(GEN_AI_REQUEST_TEMPERATURE.getKey()) @@ -2322,10 +2749,10 @@ index 02d92ca070..aa98cd62c7 100644 BatchGetItem( DYNAMODB, diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequestType.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequestType.java -index 274ec27194..83d9353c3b 100644 +index 274ec27194..d8dba6cf5c 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequestType.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequestType.java -@@ -5,7 +5,32 @@ +@@ -5,7 +5,34 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.internal; @@ -2344,7 +2771,9 @@ index 274ec27194..83d9353c3b 100644 +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_SNS_TOPIC_ARN; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_STATE_MACHINE_ARN; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_STREAM_ARN; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_STREAM_NAME; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_TABLE_ARN; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_TABLE_NAME; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.GEN_AI_MODEL; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.GEN_AI_REQUEST_MAX_TOKENS; @@ -2358,7 +2787,7 @@ index 274ec27194..83d9353c3b 100644 import io.opentelemetry.api.common.AttributeKey; import java.util.Collections; -@@ -13,16 +38,60 @@ import java.util.List; +@@ -13,16 +40,64 @@ import java.util.List; import java.util.Map; enum AwsSdkRequestType { @@ -2370,9 +2799,13 @@ index 274ec27194..83d9353c3b 100644 + + SQS(request(AWS_QUEUE_URL.getKey(), "QueueUrl"), request(AWS_QUEUE_NAME.getKey(), "QueueName")), + -+ KINESIS(request(AWS_STREAM_NAME.getKey(), "StreamName")), ++ KINESIS( ++ request(AWS_STREAM_NAME.getKey(), "StreamName"), ++ request(AWS_STREAM_ARN.getKey(), "StreamARN")), + -+ DYNAMODB(request(AWS_TABLE_NAME.getKey(), "TableName")), ++ DYNAMODB( ++ request(AWS_TABLE_NAME.getKey(), "TableName"), ++ response(AWS_TABLE_ARN.getKey(), "Table.TableArn")), + SNS( /* @@ -2962,19 +3395,37 @@ index 7ae1590152..5b7a188914 100644 + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java -index 94243d0b11..7b15a1c84b 100644 +index 94243d0b11..06d8a9141b 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java -@@ -5,6 +5,8 @@ +@@ -5,6 +5,10 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.internal; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_AUTH_ACCESS_KEY; ++import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_AUTH_REGION; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.GEN_AI_SYSTEM; +import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.BEDROCKRUNTIME; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.DYNAMODB; import io.opentelemetry.api.common.AttributeKey; -@@ -48,6 +50,7 @@ import software.amazon.awssdk.http.SdkHttpResponse; +@@ -28,6 +32,7 @@ import java.time.Instant; + import java.util.Optional; + import java.util.stream.Collectors; + import javax.annotation.Nullable; ++import software.amazon.awssdk.auth.credentials.AwsCredentials; + import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; + import software.amazon.awssdk.awscore.AwsResponse; + import software.amazon.awssdk.core.ClientType; +@@ -40,6 +45,7 @@ import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; + import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; + import software.amazon.awssdk.http.SdkHttpRequest; + import software.amazon.awssdk.http.SdkHttpResponse; ++import software.amazon.awssdk.regions.Region; + + /** + * AWS request execution interceptor. +@@ -48,6 +54,7 @@ import software.amazon.awssdk.http.SdkHttpResponse; * at any time. */ public final class TracingExecutionInterceptor implements ExecutionInterceptor { @@ -2982,7 +3433,34 @@ index 94243d0b11..7b15a1c84b 100644 // copied from DbIncubatingAttributes private static final AttributeKey DB_OPERATION = AttributeKey.stringKey("db.operation"); -@@ -342,6 +345,10 @@ public final class TracingExecutionInterceptor implements ExecutionInterceptor { +@@ -261,6 +268,26 @@ public final class TracingExecutionInterceptor implements ExecutionInterceptor { + SdkHttpRequest httpRequest = context.httpRequest(); + executionAttributes.putAttribute(SDK_HTTP_REQUEST_ATTRIBUTE, httpRequest); + ++ if (captureExperimentalSpanAttributes) { ++ AwsCredentials credentials = ++ executionAttributes.getAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS); ++ Region signingRegion = ++ executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION); ++ Span span = Span.fromContext(otelContext); ++ ++ if (credentials != null) { ++ String accessKeyId = credentials.accessKeyId(); ++ if (accessKeyId != null) { ++ span.setAttribute(AWS_AUTH_ACCESS_KEY, accessKeyId); ++ } ++ } ++ ++ if (signingRegion != null) { ++ String region = signingRegion.toString(); ++ span.setAttribute(AWS_AUTH_REGION, region); ++ } ++ } ++ + // We ought to pass the parent of otelContext here, but we didn't store it, and it shouldn't + // make a difference (unless we start supporting the http.resend_count attribute in this + // instrumentation, which, logically, we can't on this level of abstraction) +@@ -342,6 +369,10 @@ public final class TracingExecutionInterceptor implements ExecutionInterceptor { } } } @@ -3120,8 +3598,39 @@ index 08b000a05c..de0fe82638 100644 // needed for SQS - using emq directly as localstack references emq v0.15.7 ie WITHOUT AWS trace header propagation implementation("org.elasticmq:elasticmq-rest-sqs_2.13") +diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy +index 9aaacb3abe..198990a509 100644 +--- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy ++++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy +@@ -146,6 +146,8 @@ abstract class AbstractAws2ClientCoreTest extends InstrumentationSpecification { + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "DynamoDb" + "$RpcIncubatingAttributes.RPC_METHOD" "CreateTable" ++ "aws.auth.account.access_key" "my-access-key" ++ "aws.auth.region" "ap-northeast-1" + "aws.agent" "java-aws-sdk" + "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" + "aws.table.name" "sometable" +@@ -179,6 +181,8 @@ abstract class AbstractAws2ClientCoreTest extends InstrumentationSpecification { + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "DynamoDb" + "$RpcIncubatingAttributes.RPC_METHOD" "Query" ++ "aws.auth.account.access_key" "my-access-key" ++ "aws.auth.region" "ap-northeast-1" + "aws.agent" "java-aws-sdk" + "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" + "aws.table.name" "sometable" +@@ -211,6 +215,8 @@ abstract class AbstractAws2ClientCoreTest extends InstrumentationSpecification { + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "$service" + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" ++ "aws.auth.account.access_key" "my-access-key" ++ "aws.auth.region" "ap-northeast-1" + "aws.agent" "java-aws-sdk" + "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" + "aws.table.name" "sometable" diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy -index c571c0aa9c..1a4a00d95e 100644 +index c571c0aa9c..a6fbdab597 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy @@ -37,10 +37,19 @@ import software.amazon.awssdk.services.s3.model.GetObjectRequest @@ -3144,7 +3653,16 @@ index c571c0aa9c..1a4a00d95e 100644 import spock.lang.Unroll import java.nio.charset.StandardCharsets -@@ -148,8 +157,32 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { +@@ -134,6 +143,8 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "$service" + "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" ++ "aws.auth.account.access_key" "my-access-key" ++ "aws.auth.region" "ap-northeast-1" + "aws.agent" "java-aws-sdk" + "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" + if (service == "S3") { +@@ -148,8 +159,32 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemIncubatingValues.AWS_SQS } else if (service == "Kinesis") { "aws.stream.name" "somestream" @@ -3179,7 +3697,7 @@ index c571c0aa9c..1a4a00d95e 100644 } } } -@@ -164,7 +197,7 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { +@@ -164,7 +199,7 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { "S3" | "CreateBucket" | "PUT" | "UNKNOWN" | s3ClientBuilder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" "S3" | "GetObject" | "GET" | "UNKNOWN" | s3ClientBuilder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) } | "" "Kinesis" | "DeleteStream" | "POST" | "UNKNOWN" | KinesisClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" @@ -3188,7 +3706,7 @@ index c571c0aa9c..1a4a00d95e 100644 567910cd-659e-55d4-8ccb-5aaf14679dc0 -@@ -174,15 +207,15 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { +@@ -174,15 +209,15 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { """ @@ -3211,7 +3729,7 @@ index c571c0aa9c..1a4a00d95e 100644 """ "Sqs" | "CreateQueue" | "POST" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | { if (!Boolean.getBoolean("testLatestDeps")) { -@@ -244,170 +277,193 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { +@@ -244,170 +279,193 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 """ @@ -3566,6 +4084,101 @@ index c571c0aa9c..1a4a00d95e 100644 // TODO: Without AOP instrumentation of the HTTP client, we cannot model retries as // spans because of https://github.com/aws/aws-sdk-java-v2/issues/1741. We should at least tweak // the instrumentation to add Events for retries instead. +@@ -457,6 +515,8 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { + "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" + "$RpcIncubatingAttributes.RPC_SERVICE" "S3" + "$RpcIncubatingAttributes.RPC_METHOD" "GetObject" ++ "aws.auth.account.access_key" "my-access-key" ++ "aws.auth.region" "ap-northeast-1" + "aws.agent" "java-aws-sdk" + "aws.bucket.name" "somebucket" + } +diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java +index 73d2a0ba82..f46361a078 100644 +--- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientRecordHttpErrorTest.java +@@ -172,6 +172,8 @@ public abstract class AbstractAws2ClientRecordHttpErrorTest { + span.hasKind(SpanKind.CLIENT); + span.hasNoParent(); + span.hasAttributesSatisfyingExactly( ++ equalTo(stringKey("aws.auth.account.access_key"), "my-access-key"), ++ equalTo(stringKey("aws.auth.region"), "ap-northeast-1"), + equalTo(SERVER_ADDRESS, "127.0.0.1"), + equalTo(SERVER_PORT, server.httpPort()), + equalTo(HTTP_REQUEST_METHOD, method), +diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsBaseTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsBaseTest.java +index 902bfdc0d4..756968776e 100644 +--- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsBaseTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsBaseTest.java +@@ -214,6 +214,8 @@ public abstract class AbstractAws2SqsBaseTest { + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "Sqs"), + equalTo(RPC_METHOD, "CreateQueue"), ++ equalTo(stringKey("aws.auth.account.access_key"), "my-access-key"), ++ equalTo(stringKey("aws.auth.region"), "ap-northeast-1"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + satisfies(URL_FULL, v -> v.startsWith("http://localhost:" + sqsPort)), +@@ -257,6 +259,8 @@ public abstract class AbstractAws2SqsBaseTest { + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "Sqs"), + equalTo(RPC_METHOD, rcpMethod), ++ equalTo(stringKey("aws.auth.account.access_key"), "my-access-key"), ++ equalTo(stringKey("aws.auth.region"), "ap-northeast-1"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + satisfies(URL_FULL, v -> v.startsWith("http://localhost:" + sqsPort)), +diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsSuppressReceiveSpansTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsSuppressReceiveSpansTest.java +index 4d0a9be89c..382c035bf5 100644 +--- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsSuppressReceiveSpansTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsSuppressReceiveSpansTest.java +@@ -84,6 +84,8 @@ public abstract class AbstractAws2SqsSuppressReceiveSpansTest extends AbstractAw + equalTo(RPC_METHOD, "ReceiveMessage"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), ++ equalTo(stringKey("aws.auth.account.access_key"), "my-access-key"), ++ equalTo(stringKey("aws.auth.region"), "ap-northeast-1"), + satisfies(URL_FULL, v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(SERVER_ADDRESS, "localhost"), + equalTo(SERVER_PORT, sqsPort)))); +diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.java +index 6fa897d462..f7ac28762c 100644 +--- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.java ++++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2SqsTracingTest.java +@@ -80,6 +80,9 @@ public abstract class AbstractAws2SqsTracingTest extends AbstractAws2SqsBaseTest + equalTo(RPC_METHOD, "SendMessage"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), ++ equalTo( ++ stringKey("aws.auth.account.access_key"), "my-access-key"), ++ equalTo(stringKey("aws.auth.region"), "ap-northeast-1"), + satisfies( + URL_FULL, v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(SERVER_ADDRESS, "localhost"), +@@ -133,6 +136,9 @@ public abstract class AbstractAws2SqsTracingTest extends AbstractAws2SqsBaseTest + equalTo(RPC_METHOD, "ReceiveMessage"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), ++ equalTo( ++ stringKey("aws.auth.account.access_key"), "my-access-key"), ++ equalTo(stringKey("aws.auth.region"), "ap-northeast-1"), + satisfies( + URL_FULL, v -> v.startsWith("http://localhost:" + sqsPort)), + equalTo(SERVER_ADDRESS, "localhost"), +diff --git a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpanAssertions.java b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpanAssertions.java +index 8731717005..0d59b40f5e 100644 +--- a/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpanAssertions.java ++++ b/instrumentation/camel-2.20/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/apachecamel/aws/AwsSpanAssertions.java +@@ -94,7 +94,8 @@ class AwsSpanAssertions { + equalTo(NETWORK_PROTOCOL_VERSION, "1.1"), + equalTo(RPC_SYSTEM, "aws-api"), + satisfies(RPC_METHOD, stringAssert -> stringAssert.isEqualTo(rpcMethod)), +- equalTo(RPC_SERVICE, "AmazonSQS"))); ++ equalTo(RPC_SERVICE, "AmazonSQS"), ++ equalTo(stringKey("aws.auth.account.access_key"), "x"))); + + if (spanName.endsWith("receive") + || spanName.endsWith("process") diff --git a/version.gradle.kts b/version.gradle.kts index a1cae43b4b..c1520e9947 100644 --- a/version.gradle.kts @@ -3573,8 +4186,8 @@ index a1cae43b4b..c1520e9947 100644 @@ -1,5 +1,5 @@ -val stableVersion = "2.11.0" -val alphaVersion = "2.11.0-alpha" -+val stableVersion = "2.11.0-adot2" -+val alphaVersion = "2.11.0-adot2-alpha" ++val stableVersion = "2.11.0-adot3" ++val alphaVersion = "2.11.0-adot3-alpha" allprojects { if (findProperty("otel.stable") != "true") { diff --git a/.github/workflows/application-signals-e2e-test.yml b/.github/workflows/application-signals-e2e-test.yml index d3ca04b77d..8d1bad5981 100644 --- a/.github/workflows/application-signals-e2e-test.yml +++ b/.github/workflows/application-signals-e2e-test.yml @@ -245,3 +245,19 @@ jobs: with: aws-region: us-east-1 caller-workflow-name: 'main-build' + + # This validation is to ensure that all test workflows relevant to this repo are actually + # being used in this repo, which is referring to all the other jobs in this file. + # + # If this starts failing, then it most likely means that new e2e test workflow was + # added to `aws-observability/aws-application-signals-test-framework`, but was not + # added to this file. It could also mean that a test in this file has been removed. + # + # If a particular test file is intended to not be tested in this repo and should not + # be failing this particular validation, then choose one of the following options: + # - Add the test file to the exclusions input (CSV format) to the workflow + # (see: https://github.com/aws-observability/aws-application-signals-test-framework/blob/main/.github/workflows/validate-e2e-tests-are-accounted-for.yml#L1) + # - Update the `validate-e2e-tests-are-accounted-for` job to change which "workflow files are expected to be used by this repo" + # (see: https://github.com/aws-observability/aws-application-signals-test-framework/blob/main/.github/workflows/validate-e2e-tests-are-accounted-for.yml) + validate-all-tests-are-accounted-for: + uses: aws-observability/aws-application-signals-test-framework/.github/workflows/validate-e2e-tests-are-accounted-for.yml@main diff --git a/.github/workflows/e2e-tests-app-with-java-agent.yml b/.github/workflows/e2e-tests-app-with-java-agent.yml index b2c4d744bf..d09283cb8f 100644 --- a/.github/workflows/e2e-tests-app-with-java-agent.yml +++ b/.github/workflows/e2e-tests-app-with-java-agent.yml @@ -167,18 +167,18 @@ jobs: VALIDATOR_COMMAND: -c spark-otel-trace-metric-validation.yml --endpoint http://app:4567 --metric-namespace aws-otel-integ-test -t ${{ github.run_id }}-${{ github.run_number }} # publish status - publish-build-status: - needs: [ test_Spring_App_With_Java_Agent, test_Spark_App_With_Java_Agent, test_Spark_AWS_SDK_V1_App_With_Java_Agent ] - if: ${{ always() }} - uses: ./.github/workflows/publish-status.yml - with: - namespace: 'ADOT/GitHubActions' - repository: ${{ github.repository }} - branch: ${{ github.ref_name }} - workflow: ${{ inputs.caller-workflow-name }} - success: ${{ needs.test_Spring_App_With_Java_Agent.result == 'success' && - needs.test_Spark_App_With_Java_Agent.result == 'success' && - needs.test_Spark_AWS_SDK_V1_App_With_Java_Agent.result == 'success' }} - region: us-east-1 - secrets: - roleArn: ${{ secrets.METRICS_ROLE_ARN }} + # publish-build-status: + # needs: [ test_Spring_App_With_Java_Agent, test_Spark_App_With_Java_Agent, test_Spark_AWS_SDK_V1_App_With_Java_Agent ] + # if: ${{ always() }} + # uses: ./.github/workflows/publish-status.yml + # with: + # namespace: 'ADOT/GitHubActions' + # repository: ${{ github.repository }} + # branch: ${{ github.ref_name }} + # workflow: ${{ inputs.caller-workflow-name }} + # success: ${{ needs.test_Spring_App_With_Java_Agent.result == 'success' && + # needs.test_Spark_App_With_Java_Agent.result == 'success' && + # needs.test_Spark_AWS_SDK_V1_App_With_Java_Agent.result == 'success' }} + # region: us-east-1 + # secrets: + # roleArn: ${{ secrets.METRICS_ROLE_ARN }} diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml index 2255a97aca..30c74f3c87 100644 --- a/.github/workflows/main-build.yml +++ b/.github/workflows/main-build.yml @@ -263,16 +263,21 @@ jobs: adot-image-name: ${{ needs.build.outputs.staging-image }} publish-build-status: - needs: [ build, contract-tests ] - if: ${{ always() }} - uses: ./.github/workflows/publish-status.yml - with: - namespace: 'ADOT/GitHubActions' - repository: ${{ github.repository }} - branch: ${{ github.ref_name }} - workflow: main-build - success: ${{ needs.build.result == 'success' && - needs.contract-tests.result == 'success' }} - region: us-east-1 - secrets: - roleArn: ${{ secrets.METRICS_ROLE_ARN }} + name: "Publish Main Build Status" + needs: [ build, e2e-test, contract-tests, application-signals-lambda-layer-build, application-signals-e2e-test ] + runs-on: ubuntu-latest + if: always() + steps: + - name: Configure AWS Credentials for emitting metrics + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.METRICS_ROLE_ARN }} + aws-region: us-east-1 + + - name: Publish main build status + run: | + value="${{ needs.build.result == 'success' && needs.e2e-test.result == 'success' && needs.contract-tests.result == 'success' && needs.application-signals-lambda-layer-build.result == 'success' && needs.application-signals-e2e-test.result == 'success' && '0.0' || '1.0' }}" + aws cloudwatch put-metric-data --namespace 'ADOT/GitHubActions' \ + --metric-name Failure \ + --dimensions repository=${{ github.repository }},branch=${{ github.ref_name }},workflow=main_build \ + --value $value diff --git a/.github/workflows/owasp.yml b/.github/workflows/owasp.yml index ecf34587f2..54ce812326 100644 --- a/.github/workflows/owasp.yml +++ b/.github/workflows/owasp.yml @@ -97,7 +97,7 @@ jobs: id: high_scan_v2 uses: ./.github/actions/image_scan with: - image-ref: "public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v2.11.0" + image-ref: "public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v2.11.1" severity: 'CRITICAL,HIGH' - name: Perform low image scan on v2 @@ -105,7 +105,7 @@ jobs: id: low_scan_v2 uses: ./.github/actions/image_scan with: - image-ref: "public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v2.11.0" + image-ref: "public.ecr.aws/aws-observability/adot-autoinstrumentation-java:v2.11.1" severity: 'MEDIUM,LOW,UNKNOWN' - name: Configure AWS Credentials for emitting metrics diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 8696c3d915..ce9d29ddc8 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -21,6 +21,7 @@ permissions: jobs: build: + environment: Release runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-lambda.yml b/.github/workflows/release-lambda.yml index 87336ab4b9..2c1b2f037c 100644 --- a/.github/workflows/release-lambda.yml +++ b/.github/workflows/release-lambda.yml @@ -9,10 +9,10 @@ on: aws_region: description: 'Deploy to aws regions' required: true - default: 'us-east-1, us-east-2, us-west-1, us-west-2, ap-south-1, ap-northeast-3, ap-northeast-2, ap-southeast-1, ap-southeast-2, ap-northeast-1, ca-central-1, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-north-1, sa-east-1, af-south-1, ap-east-1, ap-south-2, ap-southeast-3, ap-southeast-4, eu-central-2, eu-south-1, eu-south-2, il-central-1, me-central-1, me-south-1' + default: 'us-east-1, us-east-2, us-west-1, us-west-2, ap-south-1, ap-northeast-3, ap-northeast-2, ap-southeast-1, ap-southeast-2, ap-northeast-1, ca-central-1, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-north-1, sa-east-1, af-south-1, ap-east-1, ap-south-2, ap-southeast-3, ap-southeast-4, eu-central-2, eu-south-1, eu-south-2, il-central-1, me-central-1, me-south-1, ap-southeast-5, ap-southeast-7, mx-central-1, ca-west-1, cn-north-1, cn-northwest-1' env: - COMMERCIAL_REGIONS: us-east-1, us-east-2, us-west-1, us-west-2, ap-south-1, ap-northeast-3, ap-northeast-2, ap-southeast-1, ap-southeast-2, ap-northeast-1, ca-central-1, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-north-1, sa-east-1 + COMMERCIAL_REGIONS: us-east-1, us-east-2, us-west-1, us-west-2, ap-south-1, ap-northeast-3, ap-northeast-2, ap-southeast-1, ap-southeast-2, ap-northeast-1, ca-central-1, eu-central-1, eu-west-1, eu-west-2, eu-west-3, eu-north-1, sa-east-1, ap-southeast-5, ap-southeast-7, mx-central-1, ca-west-1, cn-north-1, cn-northwest-1 LAYER_NAME: AWSOpenTelemetryDistroJava permissions: @@ -21,6 +21,7 @@ permissions: jobs: build-layer: + environment: Release runs-on: ubuntu-latest outputs: aws_regions_json: ${{ steps.set-matrix.outputs.aws_regions_json }} diff --git a/.github/workflows/release-udp-exporter.yml b/.github/workflows/release-udp-exporter.yml index 50b4d67065..e200a7c3a9 100644 --- a/.github/workflows/release-udp-exporter.yml +++ b/.github/workflows/release-udp-exporter.yml @@ -21,6 +21,7 @@ jobs: id-token: write release-udp-exporter: + environment: Release runs-on: ubuntu-latest needs: validate-udp-exporter-e2e-test steps: diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java index 08036c7d9b..5af9cae391 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/base/AwsSdkBaseTest.java @@ -54,6 +54,7 @@ public abstract class AwsSdkBaseTest extends ContractTestBase { "localstack", "s3.localstack", "create-bucket.s3.localstack", + "cross-account-bucket.s3.localstack", "put-object.s3.localstack", "get-object.s3.localstack"); @@ -258,6 +259,12 @@ private ThrowingConsumer assertKeyIsPresent(String key) { }; } + private ThrowingConsumer assertKeyIsNotPresent(String key) { + return (attribute) -> { + assertThat(attribute.getKey()).isNotEqualTo(key); + }; + } + /** All the spans of the AWS SDK Should have a RPC properties. */ private void assertSemanticConventionsAttributes( List attributesList, @@ -308,6 +315,9 @@ private void assertSpanClientAttributes( String type, String identifier, String cloudformationIdentifier, + String remoteAccountId, + String remoteAccessKey, + String remoteRegion, String address, int port, String url, @@ -327,6 +337,9 @@ private void assertSpanClientAttributes( type, identifier, cloudformationIdentifier, + remoteAccountId, + remoteAccessKey, + remoteRegion, address, port, url, @@ -345,6 +358,9 @@ private void assertSpanProducerAttributes( String type, String identifier, String cloudformationIdentifier, + String remoteAccountId, + String remoteAccessKey, + String remoteRegion, String address, int port, String url, @@ -363,6 +379,9 @@ private void assertSpanProducerAttributes( type, identifier, cloudformationIdentifier, + remoteAccountId, + remoteAccessKey, + remoteRegion, address, port, url, @@ -412,6 +431,9 @@ private void assertSpanAttributes( String type, String identifier, String cloudformationIdentifier, + String remoteAccountId, + String remoteAccessKey, + String remoteRegion, String address, int port, String url, @@ -436,6 +458,9 @@ private void assertSpanAttributes( type, identifier, cloudformationIdentifier, + remoteAccountId, + remoteAccessKey, + remoteRegion, awsSpanKind); for (var assertion : extraAssertions) { assertThat(spanAttributes).satisfiesOnlyOnce(assertion); @@ -452,6 +477,9 @@ private void assertAwsAttributes( String type, String identifier, String clouformationIdentifier, + String remoteAccountId, + String remoteAccessKey, + String remoteRegion, String spanKind) { var assertions = @@ -471,6 +499,27 @@ private void assertAwsAttributes( assertAttribute( AppSignalsConstants.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, clouformationIdentifier)); } + + if (remoteAccountId != null) { + assertions.satisfiesOnlyOnce( + assertKeyIsPresent(AppSignalsConstants.AWS_REMOTE_RESOURCE_IDENTIFIER)); + assertions.satisfiesOnlyOnce( + assertAttribute(AppSignalsConstants.AWS_REMOTE_RESOURCE_ACCOUNT_ID, remoteAccountId)); + assertKeyIsNotPresent(AppSignalsConstants.AWS_REMOTE_RESOURCE_ACCESS_KEY); + } + + if (remoteAccessKey != null) { + assertions.satisfiesOnlyOnce( + assertKeyIsPresent(AppSignalsConstants.AWS_REMOTE_RESOURCE_IDENTIFIER)); + assertions.satisfiesOnlyOnce( + assertAttribute(AppSignalsConstants.AWS_REMOTE_RESOURCE_ACCESS_KEY, remoteAccessKey)); + assertKeyIsNotPresent(AppSignalsConstants.AWS_REMOTE_RESOURCE_ACCOUNT_ID); + } + + if (remoteRegion != null) { + assertions.satisfiesOnlyOnce( + assertAttribute(AppSignalsConstants.AWS_REMOTE_RESOURCE_REGION, remoteRegion)); + } } private void assertSqsConsumerAwsAttributes(List attributesList, String operation) { @@ -495,6 +544,9 @@ protected void assertMetricClientAttributes( String type, String identifier, String cloudformationIdentifier, + String remoteAccountId, + String remoteAccessKey, + String remoteRegion, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -507,6 +559,9 @@ protected void assertMetricClientAttributes( type, identifier, cloudformationIdentifier, + remoteAccountId, + remoteAccessKey, + remoteRegion, expectedSum); } @@ -520,6 +575,9 @@ protected void assertMetricProducerAttributes( String type, String identifier, String cloudformationIdentifier, + String remoteAccountId, + String remoteAccessKey, + String remoteRegion, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -532,6 +590,9 @@ protected void assertMetricProducerAttributes( type, identifier, cloudformationIdentifier, + remoteAccountId, + remoteAccessKey, + remoteRegion, expectedSum); } @@ -545,6 +606,9 @@ protected void assertMetricConsumerAttributes( String type, String identifier, String cloudformationIdentifier, + String remoteAccountId, + String remoteAccessKey, + String remoteRegion, Double expectedSum) { assertMetricAttributes( resourceScopeMetrics, @@ -557,6 +621,9 @@ protected void assertMetricConsumerAttributes( type, identifier, cloudformationIdentifier, + remoteAccountId, + remoteAccessKey, + remoteRegion, expectedSum); } @@ -571,6 +638,9 @@ protected void assertMetricAttributes( String type, String identifier, String cloudformationIdentifier, + String remoteAccountId, + String remoteAccessKey, + String remoteRegion, Double expectedSum) { assertThat(resourceScopeMetrics) .anySatisfy( @@ -591,6 +661,9 @@ protected void assertMetricAttributes( type, identifier, cloudformationIdentifier, + remoteAccountId, + remoteAccessKey, + remoteRegion, spanKind); if (expectedSum != null) { double actualSum = dataPoint.getSum(); @@ -634,6 +707,9 @@ protected void doTestS3CreateBucket() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "create-bucket.s3.localstack", 4566, "http://create-bucket.s3.localstack:4566", @@ -649,6 +725,9 @@ protected void doTestS3CreateBucket() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -660,6 +739,9 @@ protected void doTestS3CreateBucket() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -671,6 +753,9 @@ protected void doTestS3CreateBucket() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -702,6 +787,9 @@ protected void doTestS3CreateObject() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "put-object.s3.localstack", 4566, "http://put-object.s3.localstack:4566", @@ -717,6 +805,9 @@ protected void doTestS3CreateObject() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -728,6 +819,9 @@ protected void doTestS3CreateObject() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -739,6 +833,9 @@ protected void doTestS3CreateObject() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -769,6 +866,9 @@ protected void doTestS3GetObject() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "get-object.s3.localstack", 4566, "http://get-object.s3.localstack:4566", @@ -784,6 +884,9 @@ protected void doTestS3GetObject() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -795,6 +898,9 @@ protected void doTestS3GetObject() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -806,6 +912,9 @@ protected void doTestS3GetObject() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -836,6 +945,9 @@ protected void doTestS3Error() { type, identifier, cloudformationIdentifier, + null, + null, + null, "error-bucket.s3.test", 8080, "http://error-bucket.s3.test:8080", @@ -851,6 +963,9 @@ protected void doTestS3Error() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -862,6 +977,9 @@ protected void doTestS3Error() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -873,6 +991,9 @@ protected void doTestS3Error() { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); } @@ -903,6 +1024,9 @@ protected void doTestS3Fault() { type, identifier, cloudformationIdentifier, + null, + null, + null, "fault-bucket.s3.test", 8080, "http://fault-bucket.s3.test:8080", @@ -918,6 +1042,9 @@ protected void doTestS3Fault() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -929,6 +1056,9 @@ protected void doTestS3Fault() { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); assertMetricClientAttributes( metrics, @@ -940,6 +1070,9 @@ protected void doTestS3Fault() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -978,6 +1111,9 @@ protected void doTestDynamoDbCreateTable() { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -993,6 +1129,9 @@ protected void doTestDynamoDbCreateTable() { type, identifier, cloudformationIdentifier, + null, + null, + null, 20000.0); assertMetricClientAttributes( metrics, @@ -1004,6 +1143,9 @@ protected void doTestDynamoDbCreateTable() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1015,6 +1157,9 @@ protected void doTestDynamoDbCreateTable() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1045,6 +1190,9 @@ protected void doTestDynamoDbPutItem() { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -1060,6 +1208,9 @@ protected void doTestDynamoDbPutItem() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1071,6 +1222,9 @@ protected void doTestDynamoDbPutItem() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1082,6 +1236,91 @@ protected void doTestDynamoDbPutItem() { type, identifier, cloudformationIdentifier, + null, + null, + null, + 0.0); + } + + protected void doTestDynamoDbDescribeTable() { + appClient.get("/ddb/describetable/test-table").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /ddb/describetable/:tablename"; + var type = "AWS::DynamoDB::Table"; + var identifier = "test-table"; + var cloudformationIdentifier = "test-table"; + var remoteAccountId = "000000000000"; + var remoteRegion = "us-west-2"; + + assertSpanClientAttributes( + traces, + dynamoDbSpanName("DescribeTable"), + getDynamoDbRpcServiceName(), + localService, + localOperation, + getDynamoDbServiceName(), + "DescribeTable", + type, + identifier, + cloudformationIdentifier, + remoteAccountId, + null, + remoteRegion, + "localstack", + 4566, + "http://localstack:4566", + 200, + dynamoDbAttributes("DescribeTable", "test-table")); + + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getDynamoDbServiceName(), + "DescribeTable", + type, + identifier, + cloudformationIdentifier, + remoteAccountId, + null, + remoteRegion, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getDynamoDbServiceName(), + "DescribeTable", + type, + identifier, + cloudformationIdentifier, + remoteAccountId, + null, + remoteRegion, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getDynamoDbServiceName(), + "DescribeTable", + type, + identifier, + cloudformationIdentifier, + remoteAccountId, + null, + remoteRegion, 0.0); } @@ -1112,6 +1351,9 @@ protected void doTestDynamoDbError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "error.test", 8080, "http://error.test:8080", @@ -1127,6 +1369,9 @@ protected void doTestDynamoDbError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1138,6 +1383,9 @@ protected void doTestDynamoDbError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1149,6 +1397,9 @@ protected void doTestDynamoDbError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); } @@ -1185,6 +1436,9 @@ protected void doTestDynamoDbFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "fault.test", 8080, "http://fault.test:8080", @@ -1200,6 +1454,9 @@ protected void doTestDynamoDbFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 20000.0); assertMetricClientAttributes( metrics, @@ -1211,6 +1468,9 @@ protected void doTestDynamoDbFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); assertMetricClientAttributes( metrics, @@ -1222,6 +1482,9 @@ protected void doTestDynamoDbFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1252,6 +1515,9 @@ protected void doTestSQSCreateQueue() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -1267,6 +1533,9 @@ protected void doTestSQSCreateQueue() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1278,6 +1547,9 @@ protected void doTestSQSCreateQueue() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1289,6 +1561,9 @@ protected void doTestSQSCreateQueue() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1320,6 +1595,9 @@ protected void doTestSQSSendMessage() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -1336,6 +1614,9 @@ protected void doTestSQSSendMessage() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricProducerAttributes( metrics, @@ -1347,6 +1628,9 @@ protected void doTestSQSSendMessage() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricProducerAttributes( metrics, @@ -1358,6 +1642,9 @@ protected void doTestSQSSendMessage() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1406,6 +1693,9 @@ protected void doTestSQSReceiveMessage() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricConsumerAttributes( metrics, @@ -1417,6 +1707,9 @@ protected void doTestSQSReceiveMessage() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1448,6 +1741,9 @@ protected void doTestSQSError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "error.test", 8080, "http://error.test:8080", @@ -1464,6 +1760,9 @@ protected void doTestSQSError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricProducerAttributes( metrics, @@ -1475,6 +1774,9 @@ protected void doTestSQSError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricProducerAttributes( metrics, @@ -1486,6 +1788,9 @@ protected void doTestSQSError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); } @@ -1517,6 +1822,9 @@ protected void doTestSQSFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "fault.test", 8080, "http://fault.test:8080", @@ -1533,6 +1841,9 @@ protected void doTestSQSFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricProducerAttributes( metrics, @@ -1544,6 +1855,9 @@ protected void doTestSQSFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); assertMetricProducerAttributes( metrics, @@ -1555,6 +1869,9 @@ protected void doTestSQSFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1585,6 +1902,9 @@ protected void doTestKinesisPutRecord() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -1600,6 +1920,9 @@ protected void doTestKinesisPutRecord() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1611,6 +1934,9 @@ protected void doTestKinesisPutRecord() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1622,11 +1948,14 @@ protected void doTestKinesisPutRecord() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } - protected void doTestKinesisError() throws Exception { - appClient.get("/kinesis/error").aggregate().join(); + protected void doTestKinesisDescribeStream() { + appClient.get("/kinesis/describestream/test-stream").aggregate().join(); var traces = mockCollectorClient.getTraces(); var metrics = mockCollectorClient.getMetrics( @@ -1636,14 +1965,98 @@ protected void doTestKinesisError() throws Exception { AppSignalsConstants.LATENCY_METRIC)); var localService = getApplicationOtelServiceName(); - var localOperation = "GET /kinesis/error"; + var localOperation = "GET /kinesis/describestream/:streamname"; var type = "AWS::Kinesis::Stream"; - var identifier = "nonexistantstream"; - var cloudformationIdentifier = "nonexistantstream"; + var identifier = "test-stream"; + var cloudformationIdentifier = "test-stream"; + var remoteAccountId = "000000000000"; + var remoteRegion = "us-west-2"; assertSpanClientAttributes( traces, - kinesisSpanName("PutRecord"), + kinesisSpanName("DescribeStream"), + getKinesisRpcServiceName(), + localService, + localOperation, + getKinesisServiceName(), + "DescribeStream", + type, + identifier, + cloudformationIdentifier, + remoteAccountId, + null, + remoteRegion, + "localstack", + 4566, + "http://localstack:4566", + 200, + List.of( + assertAttribute( + SemanticConventionsConstants.AWS_STREAM_ARN, + "arn:aws:kinesis:us-west-2:000000000000:stream/test-stream"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getKinesisServiceName(), + "DescribeStream", + type, + identifier, + cloudformationIdentifier, + remoteAccountId, + null, + remoteRegion, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getKinesisServiceName(), + "DescribeStream", + type, + identifier, + cloudformationIdentifier, + remoteAccountId, + null, + remoteRegion, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getKinesisServiceName(), + "DescribeStream", + type, + identifier, + cloudformationIdentifier, + remoteAccountId, + null, + remoteRegion, + 0.0); + } + + protected void doTestKinesisError() throws Exception { + appClient.get("/kinesis/error").aggregate().join(); + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /kinesis/error"; + var type = "AWS::Kinesis::Stream"; + var identifier = "nonexistantstream"; + var cloudformationIdentifier = "nonexistantstream"; + + assertSpanClientAttributes( + traces, + kinesisSpanName("PutRecord"), getKinesisRpcServiceName(), localService, localOperation, @@ -1652,6 +2065,9 @@ protected void doTestKinesisError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "error.test", 8080, "http://error.test:8080", @@ -1668,6 +2084,9 @@ protected void doTestKinesisError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1679,6 +2098,9 @@ protected void doTestKinesisError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1690,6 +2112,9 @@ protected void doTestKinesisError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); } @@ -1720,6 +2145,9 @@ protected void doTestKinesisFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "fault.test", 8080, "http://fault.test:8080", @@ -1735,6 +2163,9 @@ protected void doTestKinesisFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1746,6 +2177,9 @@ protected void doTestKinesisFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); assertMetricClientAttributes( metrics, @@ -1757,6 +2191,9 @@ protected void doTestKinesisFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1787,6 +2224,9 @@ protected void doTestBedrockAgentKnowledgeBaseId() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1804,6 +2244,9 @@ protected void doTestBedrockAgentKnowledgeBaseId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1815,6 +2258,9 @@ protected void doTestBedrockAgentKnowledgeBaseId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1826,6 +2272,9 @@ protected void doTestBedrockAgentKnowledgeBaseId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1855,6 +2304,9 @@ protected void doTestBedrockAgentAgentId() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1870,6 +2322,9 @@ protected void doTestBedrockAgentAgentId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1881,6 +2336,9 @@ protected void doTestBedrockAgentAgentId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1892,6 +2350,9 @@ protected void doTestBedrockAgentAgentId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1921,6 +2382,9 @@ protected void doTestBedrockAgentDataSourceId() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -1938,6 +2402,9 @@ protected void doTestBedrockAgentDataSourceId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -1949,6 +2416,9 @@ protected void doTestBedrockAgentDataSourceId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -1960,6 +2430,9 @@ protected void doTestBedrockAgentDataSourceId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -1989,6 +2462,9 @@ protected void doTestBedrockRuntimeAi21Jamba() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2012,6 +2488,9 @@ protected void doTestBedrockRuntimeAi21Jamba() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2023,6 +2502,9 @@ protected void doTestBedrockRuntimeAi21Jamba() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2034,6 +2516,9 @@ protected void doTestBedrockRuntimeAi21Jamba() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2063,6 +2548,9 @@ protected void doTestBedrockRuntimeAmazonTitan() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2089,6 +2577,9 @@ protected void doTestBedrockRuntimeAmazonTitan() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2100,6 +2591,9 @@ protected void doTestBedrockRuntimeAmazonTitan() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2111,6 +2605,9 @@ protected void doTestBedrockRuntimeAmazonTitan() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2142,6 +2639,9 @@ protected void doTestBedrockRuntimeAnthropicClaude() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2168,6 +2668,9 @@ protected void doTestBedrockRuntimeAnthropicClaude() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2179,6 +2682,9 @@ protected void doTestBedrockRuntimeAnthropicClaude() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2190,6 +2696,9 @@ protected void doTestBedrockRuntimeAnthropicClaude() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2221,6 +2730,9 @@ protected void doTestBedrockRuntimeCohereCommandR() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2246,6 +2758,9 @@ protected void doTestBedrockRuntimeCohereCommandR() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2257,6 +2772,9 @@ protected void doTestBedrockRuntimeCohereCommandR() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2268,6 +2786,9 @@ protected void doTestBedrockRuntimeCohereCommandR() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2299,6 +2820,9 @@ protected void doTestBedrockRuntimeMetaLlama() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2323,6 +2847,9 @@ protected void doTestBedrockRuntimeMetaLlama() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2334,6 +2861,9 @@ protected void doTestBedrockRuntimeMetaLlama() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2345,6 +2875,9 @@ protected void doTestBedrockRuntimeMetaLlama() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2376,6 +2909,9 @@ protected void doTestBedrockRuntimeMistral() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2401,6 +2937,9 @@ protected void doTestBedrockRuntimeMistral() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2412,6 +2951,9 @@ protected void doTestBedrockRuntimeMistral() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2423,6 +2965,9 @@ protected void doTestBedrockRuntimeMistral() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2453,6 +2998,9 @@ protected void doTestBedrockGuardrailId() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2473,6 +3021,9 @@ protected void doTestBedrockGuardrailId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2484,6 +3035,9 @@ protected void doTestBedrockGuardrailId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2495,6 +3049,9 @@ protected void doTestBedrockGuardrailId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2524,6 +3081,9 @@ protected void doTestBedrockAgentRuntimeAgentId() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2539,6 +3099,9 @@ protected void doTestBedrockAgentRuntimeAgentId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2550,6 +3113,9 @@ protected void doTestBedrockAgentRuntimeAgentId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2561,6 +3127,9 @@ protected void doTestBedrockAgentRuntimeAgentId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2591,6 +3160,9 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { type, identifier, cloudformationIdentifier, + null, + null, + null, "bedrock.test", 8080, "http://bedrock.test:8080", @@ -2608,6 +3180,9 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2619,6 +3194,9 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2630,6 +3208,9 @@ protected void doTestBedrockAgentRuntimeKnowledgeBaseId() { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2659,6 +3240,9 @@ protected void doTestSecretsManagerDescribeSecret() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -2677,6 +3261,9 @@ protected void doTestSecretsManagerDescribeSecret() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2688,6 +3275,9 @@ protected void doTestSecretsManagerDescribeSecret() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2699,6 +3289,9 @@ protected void doTestSecretsManagerDescribeSecret() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2724,6 +3317,9 @@ protected void doTestSecretsManagerError() throws Exception { null, null, null, + null, + null, + null, "error.test", 8080, "http://error.test:8080", @@ -2739,6 +3335,9 @@ protected void doTestSecretsManagerError() throws Exception { null, null, null, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2750,6 +3349,9 @@ protected void doTestSecretsManagerError() throws Exception { null, null, null, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2761,6 +3363,9 @@ protected void doTestSecretsManagerError() throws Exception { null, null, null, + null, + null, + null, 1.0); } @@ -2787,6 +3392,9 @@ protected void doTestSecretsManagerFault() throws Exception { null, null, null, + null, + null, + null, "fault.test", 8080, "http://fault.test:8080", @@ -2802,6 +3410,9 @@ protected void doTestSecretsManagerFault() throws Exception { null, null, null, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2813,6 +3424,9 @@ protected void doTestSecretsManagerFault() throws Exception { null, null, null, + null, + null, + null, 1.0); assertMetricClientAttributes( metrics, @@ -2824,6 +3438,9 @@ protected void doTestSecretsManagerFault() throws Exception { null, null, null, + null, + null, + null, 0.0); } @@ -2854,6 +3471,9 @@ protected void doTestStepFunctionsDescribeStateMachine() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -2872,6 +3492,9 @@ protected void doTestStepFunctionsDescribeStateMachine() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2883,6 +3506,9 @@ protected void doTestStepFunctionsDescribeStateMachine() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2894,6 +3520,9 @@ protected void doTestStepFunctionsDescribeStateMachine() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2923,6 +3552,9 @@ protected void doTestStepFunctionsDescribeActivity() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -2941,6 +3573,9 @@ protected void doTestStepFunctionsDescribeActivity() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -2952,6 +3587,9 @@ protected void doTestStepFunctionsDescribeActivity() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -2963,6 +3601,9 @@ protected void doTestStepFunctionsDescribeActivity() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -2994,6 +3635,9 @@ protected void doTestStepFunctionsError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "error.test", 8080, "http://error.test:8080", @@ -3012,6 +3656,9 @@ protected void doTestStepFunctionsError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -3023,6 +3670,9 @@ protected void doTestStepFunctionsError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); assertMetricClientAttributes( metrics, @@ -3034,6 +3684,9 @@ protected void doTestStepFunctionsError() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); } @@ -3064,6 +3717,9 @@ protected void doTestStepFunctionsFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "fault.test", 8080, "http://fault.test:8080", @@ -3082,6 +3738,9 @@ protected void doTestStepFunctionsFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 5000.0); assertMetricClientAttributes( metrics, @@ -3093,6 +3752,9 @@ protected void doTestStepFunctionsFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 1.0); assertMetricClientAttributes( metrics, @@ -3104,6 +3766,9 @@ protected void doTestStepFunctionsFault() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, 0.0); } @@ -3134,6 +3799,9 @@ protected void doTestSnsGetTopicAttributes() throws Exception { type, identifier, cloudformationIdentifier, + null, + null, + null, "localstack", 4566, "http://localstack:4566", @@ -3167,6 +3835,9 @@ protected void doTestSnsError() throws Exception { null, null, null, + null, + null, + null, "error.test", 8080, "http://error.test:8080", @@ -3183,6 +3854,9 @@ protected void doTestSnsError() throws Exception { null, null, null, + null, + null, + null, 5000.0); assertMetricClientAttributes( @@ -3195,6 +3869,9 @@ protected void doTestSnsError() throws Exception { null, null, null, + null, + null, + null, 0.0); assertMetricClientAttributes( @@ -3207,6 +3884,9 @@ protected void doTestSnsError() throws Exception { null, null, null, + null, + null, + null, 1.0); } @@ -3233,6 +3913,9 @@ protected void doTestSnsFault() throws Exception { null, null, null, + null, + null, + null, "fault.test", 8080, "http://fault.test:8080", @@ -3249,6 +3932,9 @@ protected void doTestSnsFault() throws Exception { null, null, null, + null, + null, + null, 5000.0); assertMetricClientAttributes( @@ -3261,6 +3947,9 @@ protected void doTestSnsFault() throws Exception { null, null, null, + null, + null, + null, 1.0); assertMetricClientAttributes( @@ -3273,6 +3962,91 @@ protected void doTestSnsFault() throws Exception { null, null, null, + null, + null, + null, + 0.0); + } + + protected void doTestCrossAccount(String remoteRegion) throws Exception { + appClient.get("/crossaccount/createbucket/accountb").aggregate().join(); + + var traces = mockCollectorClient.getTraces(); + var metrics = + mockCollectorClient.getMetrics( + Set.of( + AppSignalsConstants.ERROR_METRIC, + AppSignalsConstants.FAULT_METRIC, + AppSignalsConstants.LATENCY_METRIC)); + + var localService = getApplicationOtelServiceName(); + var localOperation = "GET /crossaccount/createbucket/accountb"; + var type = "AWS::S3::Bucket"; + var identifier = "cross-account-bucket"; + var cloudformationIdentifier = "cross-account-bucket"; + var remoteAccessKey = "account_b_access_key_id"; + + assertSpanClientAttributes( + traces, + s3SpanName("CreateBucket"), + getS3RpcServiceName(), + localService, + localOperation, + getS3ServiceName(), + "CreateBucket", + type, + identifier, + cloudformationIdentifier, + null, + remoteAccessKey, + remoteRegion, + "cross-account-bucket.s3.localstack", + 4566, + "http://cross-account-bucket.s3.localstack:4566", + 200, + List.of( + assertAttribute(SemanticConventionsConstants.AWS_BUCKET_NAME, "cross-account-bucket"))); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.LATENCY_METRIC, + localService, + localOperation, + getS3ServiceName(), + "CreateBucket", + type, + identifier, + cloudformationIdentifier, + null, + remoteAccessKey, + remoteRegion, + 5000.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.FAULT_METRIC, + localService, + localOperation, + getS3ServiceName(), + "CreateBucket", + type, + identifier, + cloudformationIdentifier, + null, + remoteAccessKey, + remoteRegion, + 0.0); + assertMetricClientAttributes( + metrics, + AppSignalsConstants.ERROR_METRIC, + localService, + localOperation, + getS3ServiceName(), + "CreateBucket", + type, + identifier, + cloudformationIdentifier, + null, + remoteAccessKey, + remoteRegion, 0.0); } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java index 1395bc3ac8..6389fdcf5a 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v1/AwsSdkV1Test.java @@ -181,6 +181,11 @@ void testDynamoDbPutItem() { doTestDynamoDbPutItem(); } + @Test + void testDynamoDbDescribeTable() { + doTestDynamoDbDescribeTable(); + } + @Test void testDynamoDbError() throws Exception { doTestDynamoDbError(); @@ -221,6 +226,11 @@ void testKinesisPutRecord() throws Exception { doTestKinesisPutRecord(); } + @Test + void testKinesisDescribeStream() { + doTestKinesisDescribeStream(); + } + @Test void testKinsesisError() throws Exception { doTestKinesisError(); @@ -340,4 +350,9 @@ void testSnsError() throws Exception { void testSnsFault() throws Exception { doTestStepFunctionsFault(); } + + @Test + void testCrossAccount() throws Exception { + doTestCrossAccount(null); + } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java index 744a3be251..536c1e6b2a 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/awssdk/v2/AwsSdkV2Test.java @@ -179,6 +179,11 @@ void testDynamoDbPutItem() { doTestDynamoDbPutItem(); } + @Test + void testDynamoDbDescribeTable() { + doTestDynamoDbDescribeTable(); + } + @Test void testDynamoDbError() throws Exception { doTestDynamoDbError(); @@ -224,6 +229,11 @@ void testKinesisPutRecord() throws Exception { doTestKinesisPutRecord(); } + @Test + void testKinesisDescribeStream() { + doTestKinesisDescribeStream(); + } + @Test void testKinesisError() throws Exception { doTestKinesisError(); @@ -343,4 +353,9 @@ void testSnsError() throws Exception { void testSnsFault() throws Exception { doTestStepFunctionsFault(); } + + @Test + void testCrossAccount() throws Exception { + doTestCrossAccount("eu-central-1"); + } } diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java index 0ff11305c2..d4bac11941 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/AppSignalsConstants.java @@ -35,7 +35,10 @@ public class AppSignalsConstants { "aws.remote.resource.cfn.primary.identifier"; public static final String AWS_SPAN_KIND = "aws.span.kind"; public static final String AWS_REMOTE_DB_USER = "aws.remote.db.user"; - + public static final String AWS_REMOTE_RESOURCE_ACCESS_KEY = + "aws.remote.resource.account.access_key"; + public static final String AWS_REMOTE_RESOURCE_ACCOUNT_ID = "aws.remote.resource.account.id"; + public static final String AWS_REMOTE_RESOURCE_REGION = "aws.remote.resource.region"; // JVM Metrics public static final String JVM_GC_DURATION = "jvm.gc.collections.elapsed"; public static final String JVM_GC_COUNT = "jvm.gc.collections.count"; diff --git a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java index dee50bb8ea..12c116d3c0 100644 --- a/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java +++ b/appsignals-tests/contract-tests/src/test/java/software/amazon/opentelemetry/appsignals/test/utils/SemanticConventionsConstants.java @@ -56,6 +56,7 @@ public class SemanticConventionsConstants { public static final String AWS_TABLE_NAME = "aws.table.name"; public static final String AWS_QUEUE_URL = "aws.queue.url"; public static final String AWS_QUEUE_NAME = "aws.queue.name"; + public static final String AWS_STREAM_ARN = "aws.stream.arn"; public static final String AWS_STREAM_NAME = "aws.stream.name"; public static final String AWS_KNOWLEDGE_BASE_ID = "aws.bedrock.knowledge_base.id"; public static final String AWS_DATA_SOURCE_ID = "aws.bedrock.data_source.id"; diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java index 6b39559b0d..7f8f331d66 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v1/src/main/java/com/amazon/sampleapp/App.java @@ -21,8 +21,11 @@ import static spark.Spark.port; import static spark.Spark.post; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.auth.BasicSessionCredentials; import com.amazonaws.client.builder.AwsClientBuilder.EndpointConfiguration; import com.amazonaws.regions.Regions; import com.amazonaws.services.bedrock.AmazonBedrockClient; @@ -49,6 +52,7 @@ import com.amazonaws.services.identitymanagement.model.PutRolePolicyRequest; import com.amazonaws.services.kinesis.AmazonKinesisClient; import com.amazonaws.services.kinesis.model.CreateStreamRequest; +import com.amazonaws.services.kinesis.model.DescribeStreamRequest; import com.amazonaws.services.kinesis.model.PutRecordRequest; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.CreateBucketRequest; @@ -152,6 +156,7 @@ public static void main(String[] args) throws IOException, InterruptedException setupStepFunctions(); setupSns(); setupBedrock(); + setupCrossAccount(); // Add this log line so that we only start testing after all routes are configured. awaitInitialization(); @@ -320,6 +325,33 @@ private static void setupDynamoDb() { dynamoDbClient.putItem(putItemRequest); return ""; }); + + get( + "/ddb/describetable/:tablename", + (req, res) -> { + var tableName = req.params(":tablename"); + + var createTableRequest = + new CreateTableRequest() + .withTableName(tableName) + .withAttributeDefinitions( + new AttributeDefinition() + .withAttributeName("partitionKey") + .withAttributeType("S")) + .withKeySchema( + new KeySchemaElement() + .withAttributeName("partitionKey") + .withKeyType(KeyType.HASH)) + .withProvisionedThroughput( + new ProvisionedThroughput() + .withReadCapacityUnits(1L) + .withWriteCapacityUnits(1L)); + dynamoDbClient.createTable(createTableRequest); + + dynamoDbClient.describeTable(tableName); + return ""; + }); + get( "/ddb/error", (req, res) -> { @@ -405,6 +437,30 @@ private static void setupKinesis() { return ""; }); + get( + "/kinesis/describestream/:streamname", + (req, res) -> { + var streamName = req.params(":streamname"); + + var kinesisClient = + AmazonKinesisClient.builder() + .withEndpointConfiguration(endpointConfiguration) + .withCredentials(CREDENTIALS_PROVIDER) + .build(); + + var createStreamRequest = new CreateStreamRequest(); + createStreamRequest.setStreamName(streamName); + + kinesisClient.createStream(createStreamRequest); + + // Describe stream using ARN + var streamArn = "arn:aws:kinesis:us-west-2:000000000000:stream/" + streamName; + DescribeStreamRequest describeStreamRequest = + new DescribeStreamRequest().withStreamARN(streamArn); + kinesisClient.describeStream(describeStreamRequest); + return ""; + }); + get( "/kinesis/error", (req, res) -> { @@ -1191,4 +1247,30 @@ private static void setupBedrock() { return ""; }); } + + private static void setupCrossAccount() { + // Create credentials provider with temporary credentials + AWSCredentials sessionCredentials = + new BasicSessionCredentials( + "account_b_access_key_id", "account_b_secret_access_key", "account_b_token"); + AWSCredentialsProvider sessionCredentialsProvider = + new AWSStaticCredentialsProvider(sessionCredentials); + + // Create S3 client with temporary credentials + var crossAccountS3Client = + AmazonS3Client.builder() + .withCredentials(sessionCredentialsProvider) + .withEndpointConfiguration( + new EndpointConfiguration(s3Endpoint, Regions.EU_CENTRAL_1.getName())) + .build(); + + get( + "/crossaccount/createbucket/accountb", + (req, res) -> { + CreateBucketRequest createBucketRequest = + new CreateBucketRequest("cross-account-bucket", Region.EU_Frankfurt); + crossAccountS3Client.createBucket(createBucketRequest); + return ""; + }); + } } diff --git a/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java b/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java index c96b762dfd..dc4233b107 100644 --- a/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java +++ b/appsignals-tests/images/aws-sdk/aws-sdk-v2/src/main/java/com/amazon/sampleapp/App.java @@ -35,6 +35,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; @@ -68,6 +69,7 @@ import software.amazon.awssdk.services.kinesis.model.DescribeStreamRequest; import software.amazon.awssdk.services.kinesis.model.PutRecordRequest; import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.CreateBucketConfiguration; import software.amazon.awssdk.services.s3.model.CreateBucketRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; @@ -148,6 +150,7 @@ public static void main(String[] args) throws IOException, InterruptedException setupSfn(); setupBedrock(); setupSns(); + setupCrossAccount(); // Add this log line so that we only start testing after all routes are configured. awaitInitialization(); logger.info("All routes initialized"); @@ -328,6 +331,36 @@ private static void setupDynamoDb() { dynamoDbClient.putItem(putItemRequest); return ""; }); + + get( + "/ddb/describetable/:tablename", + (req, res) -> { + var tableName = req.params(":tablename"); + + var createTableRequest = + CreateTableRequest.builder() + .tableName(tableName) + .attributeDefinitions( + AttributeDefinition.builder() + .attributeName("partitionKey") + .attributeType("S") + .build()) + .keySchema( + KeySchemaElement.builder() + .attributeName("partitionKey") + .keyType(KeyType.HASH) + .build()) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(1L) + .writeCapacityUnits(1L) + .build()) + .build(); + dynamoDbClient.createTable(createTableRequest); + dynamoDbClient.describeTable(r -> r.tableName(tableName)); + return ""; + }); + get( "/ddb/error", (req, res) -> { @@ -413,6 +446,26 @@ private static void setupKinesis() { return ""; }); + get( + "/kinesis/describestream/:streamname", + (req, res) -> { + var streamName = req.params(":streamname"); + + var kinesisClient = + KinesisClient.builder() + .endpointOverride(endpoint) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + kinesisClient.createStream(CreateStreamRequest.builder().streamName(streamName).build()); + + // Describe stream using ARN + var streamArn = "arn:aws:kinesis:us-west-2:000000000000:stream/" + streamName; + var describeStreamRequest = DescribeStreamRequest.builder().streamARN(streamArn).build(); + kinesisClient.describeStream(describeStreamRequest); + return ""; + }); + get( "/kinesis/error", (req, res) -> { @@ -1209,4 +1262,36 @@ private static void setupBedrock() { return ""; }); } + + private static void setupCrossAccount() { + // Create credentials provider with temporary credentials + AwsSessionCredentials sessionCredentials = + AwsSessionCredentials.create( + "account_b_access_key_id", "account_b_secret_access_key", "account_b_token"); + StaticCredentialsProvider sessionCredentialsProvider = + StaticCredentialsProvider.create(sessionCredentials); + + // Create S3 client with temporary credentials + var crossAccountS3Client = + S3Client.builder() + .credentialsProvider(sessionCredentialsProvider) + .endpointOverride(s3Endpoint) + .region(Region.EU_CENTRAL_1) + .build(); + + get( + "/crossaccount/createbucket/accountb", + (req, res) -> { + CreateBucketRequest createBucketRequest = + CreateBucketRequest.builder() + .bucket("cross-account-bucket") + .createBucketConfiguration( + CreateBucketConfiguration.builder() + .locationConstraint(Region.EU_CENTRAL_1.id()) + .build()) + .build(); + crossAccountS3Client.createBucket(createBucketRequest); + return ""; + }); + } } diff --git a/awsagentprovider/build.gradle.kts b/awsagentprovider/build.gradle.kts index 37f6bf3020..1abe269cc0 100644 --- a/awsagentprovider/build.gradle.kts +++ b/awsagentprovider/build.gradle.kts @@ -45,7 +45,8 @@ dependencies { // For Udp emitter compileOnly("io.opentelemetry:opentelemetry-exporter-otlp-common") - // For OtlpAwsSpanExporter SigV4 Authentication + // For OtlpAwsExporter SigV4 Authentication + runtimeOnly("software.amazon.awssdk:sts") implementation("software.amazon.awssdk:auth") implementation("software.amazon.awssdk:http-auth-aws") diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java index 8cdd55a881..13cb4ddd81 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsApplicationSignalsCustomizerProvider.java @@ -135,6 +135,12 @@ public final class AwsApplicationSignalsCustomizerProvider private static final String OTEL_TRACES_SAMPLER = "otel.traces.sampler"; private static final String OTEL_TRACES_SAMPLER_ARG = "otel.traces.sampler.arg"; static final String OTEL_EXPORTER_OTLP_LOGS_HEADERS = "otel.exporter.otlp.logs.headers"; + private static final String OTEL_EXPORTER_OTLP_COMPRESSION_CONFIG = + "otel.exporter.otlp.compression"; + private static final String OTEL_EXPORTER_OTLP_TRACES_COMPRESSION_CONFIG = + "otel.exporter.otlp.traces.compression"; + private static final String OTEL_EXPORTER_OTLP_LOGS_COMPRESSION_CONFIG = + "otel.exporter.otlp.logs.compression"; // UDP packet can be upto 64KB. To limit the packet size, we limit the exported batch size. // This is a bit of a magic number, as there is no simple way to tell how many spans can make a @@ -394,11 +400,18 @@ SpanExporter customizeSpanExporter(SpanExporter spanExporter, ConfigProperties c // and OTEL_EXPORTER_OTLP_TRACES_PROTOCOL is http/protobuf // so the given spanExporter will be an instance of OtlpHttpSpanExporter + // get compression method from environment + String compression = + configProps.getString( + OTEL_EXPORTER_OTLP_TRACES_COMPRESSION_CONFIG, + configProps.getString(OTEL_EXPORTER_OTLP_COMPRESSION_CONFIG, "none")); + try { spanExporter = OtlpAwsSpanExporterBuilder.create( (OtlpHttpSpanExporter) spanExporter, configProps.getString(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)) + .setCompression(compression) .build(); } catch (Exception e) { // This technically should never happen as the validator checks for the correct env @@ -430,10 +443,17 @@ LogRecordExporter customizeLogsExporter( // OTEL_EXPORTER_OTLP_LOGS_PROTOCOL is http/protobuf // so the given logsExporter will be an instance of OtlpHttpLogRecorderExporter + // get compression method from environment + String compression = + configProps.getString( + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION_CONFIG, + configProps.getString(OTEL_EXPORTER_OTLP_COMPRESSION_CONFIG, "none")); + try { return OtlpAwsLogsExporterBuilder.create( (OtlpHttpLogRecordExporter) logsExporter, configProps.getString(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT)) + .setCompression(compression) .build(); } catch (Exception e) { // This technically should never happen as the validator checks for the correct env diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java index 726979dbf2..65b36ef765 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsAttributeKeys.java @@ -22,6 +22,11 @@ final class AwsAttributeKeys { private AwsAttributeKeys() {} + static final AttributeKey AWS_AUTH_ACCESS_KEY = + AttributeKey.stringKey("aws.auth.account.access_key"); + + static final AttributeKey AWS_AUTH_REGION = AttributeKey.stringKey("aws.auth.region"); + static final AttributeKey AWS_SPAN_KIND = AttributeKey.stringKey("aws.span.kind"); static final AttributeKey AWS_LOCAL_SERVICE = AttributeKey.stringKey("aws.local.service"); @@ -29,6 +34,19 @@ private AwsAttributeKeys() {} static final AttributeKey AWS_LOCAL_OPERATION = AttributeKey.stringKey("aws.local.operation"); + /* + * By default the local operation of a Lambda span is hard-coded to "/FunctionHandler". + * To dynamically override this at runtime—such as when running a custom server inside your Lambda— + * you can set the span attribute "aws.lambda.local.operation.override" before ending the span. For example: + * + * // Obtain the current Span and override its operation name + * Span.current().setAttribute( + * "aws.lambda.local.operation.override", + * "MyService/handleRequest"); + */ + static final AttributeKey AWS_LAMBDA_LOCAL_OPERATION_OVERRIDE = + AttributeKey.stringKey("aws.lambda.local.operation.override"); + static final AttributeKey AWS_REMOTE_SERVICE = AttributeKey.stringKey("aws.remote.service"); @@ -38,6 +56,15 @@ private AwsAttributeKeys() {} static final AttributeKey AWS_REMOTE_OPERATION = AttributeKey.stringKey("aws.remote.operation"); + static final AttributeKey AWS_REMOTE_RESOURCE_ACCESS_KEY = + AttributeKey.stringKey("aws.remote.resource.account.access_key"); + + static final AttributeKey AWS_REMOTE_RESOURCE_ACCOUNT_ID = + AttributeKey.stringKey("aws.remote.resource.account.id"); + + static final AttributeKey AWS_REMOTE_RESOURCE_REGION = + AttributeKey.stringKey("aws.remote.resource.region"); + static final AttributeKey AWS_REMOTE_RESOURCE_IDENTIFIER = AttributeKey.stringKey("aws.remote.resource.identifier"); @@ -70,7 +97,7 @@ private AwsAttributeKeys() {} static final AttributeKey AWS_LAMBDA_NAME = AttributeKey.stringKey("aws.lambda.function.name"); - static final AttributeKey AWS_LAMBDA_ARN = + static final AttributeKey AWS_LAMBDA_FUNCTION_ARN = AttributeKey.stringKey("aws.lambda.function.arn"); static final AttributeKey AWS_LAMBDA_RESOURCE_ID = @@ -87,7 +114,9 @@ private AwsAttributeKeys() {} static final AttributeKey AWS_QUEUE_URL = AttributeKey.stringKey("aws.queue.url"); static final AttributeKey AWS_QUEUE_NAME = AttributeKey.stringKey("aws.queue.name"); static final AttributeKey AWS_STREAM_NAME = AttributeKey.stringKey("aws.stream.name"); + static final AttributeKey AWS_STREAM_ARN = AttributeKey.stringKey("aws.stream.arn"); static final AttributeKey AWS_TABLE_NAME = AttributeKey.stringKey("aws.table.name"); + static final AttributeKey AWS_TABLE_ARN = AttributeKey.stringKey("aws.table.arn"); static final AttributeKey AWS_AGENT_ID = AttributeKey.stringKey("aws.bedrock.agent.id"); static final AttributeKey AWS_KNOWLEDGE_BASE_ID = AttributeKey.stringKey("aws.bedrock.knowledge_base.id"); diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java index 2e138f6e35..a1349f06b5 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGenerator.java @@ -47,13 +47,15 @@ import static io.opentelemetry.semconv.SemanticAttributes.URL_FULL; import static software.amazon.opentelemetry.javaagent.providers.AwsApplicationSignalsCustomizerProvider.LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_AGENT_ID; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_AUTH_ACCESS_KEY; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_AUTH_REGION; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_BUCKET_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_DATA_SOURCE_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_GUARDRAIL_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_GUARDRAIL_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_KNOWLEDGE_BASE_ID; -import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_ARN; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_FUNCTION_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_RESOURCE_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_OPERATION; @@ -63,7 +65,10 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_DB_USER; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_ENVIRONMENT; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_OPERATION; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_ACCESS_KEY; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_ACCOUNT_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_IDENTIFIER; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_REGION; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_TYPE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_SERVICE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SECRET_ARN; @@ -71,7 +76,9 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SPAN_KIND; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STATE_MACHINE_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STREAM_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STREAM_NAME; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_TABLE_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_TABLE_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.MAX_KEYWORD_LENGTH; @@ -99,7 +106,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.logging.Level; @@ -175,7 +184,13 @@ private Attributes generateDependencyMetricAttributes(SpanData span, Resource re setEgressOperation(span, builder); setRemoteServiceAndOperation(span, builder); setRemoteEnvironment(span, builder); - setRemoteResourceTypeAndIdentifier(span, builder); + boolean isRemoteResourceIdentifierPresent = setRemoteResourceTypeAndIdentifier(span, builder); + if (isRemoteResourceIdentifierPresent) { + boolean isAccountIdAndRegionPresent = setRemoteResourceAccountIdAndRegion(span, builder); + if (!isAccountIdAndRegionPresent) { + setRemoteResourceAccessKeyAndRegion(span, builder); + } + } setSpanKindForDependency(span, builder); setHttpStatus(span, builder); setRemoteDbUser(span, builder); @@ -489,7 +504,8 @@ private static String normalizeRemoteServiceName(SpanData span, String serviceNa * href="https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html">AWS * Cloud Control resource format. */ - private static void setRemoteResourceTypeAndIdentifier(SpanData span, AttributesBuilder builder) { + private static boolean setRemoteResourceTypeAndIdentifier( + SpanData span, AttributesBuilder builder) { Optional remoteResourceType = Optional.empty(); Optional remoteResourceIdentifier = Optional.empty(); Optional cloudformationPrimaryIdentifier = Optional.empty(); @@ -499,10 +515,20 @@ private static void setRemoteResourceTypeAndIdentifier(SpanData span, Attributes remoteResourceType = Optional.of(NORMALIZED_DYNAMO_DB_SERVICE_NAME + "::Table"); remoteResourceIdentifier = Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_TABLE_NAME))); + } else if (isKeyPresent(span, AWS_TABLE_ARN)) { + remoteResourceType = Optional.of(NORMALIZED_DYNAMO_DB_SERVICE_NAME + "::Table"); + remoteResourceIdentifier = + getDynamodbTableNameFromArn( + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_TABLE_ARN)))); } else if (isKeyPresent(span, AWS_STREAM_NAME)) { remoteResourceType = Optional.of(NORMALIZED_KINESIS_SERVICE_NAME + "::Stream"); remoteResourceIdentifier = Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_STREAM_NAME))); + } else if (isKeyPresent(span, AWS_STREAM_ARN)) { + remoteResourceType = Optional.of(NORMALIZED_KINESIS_SERVICE_NAME + "::Stream"); + remoteResourceIdentifier = + getKinesisStreamNameFromArn( + Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_STREAM_ARN)))); } else if (isKeyPresent(span, AWS_BUCKET_NAME)) { remoteResourceType = Optional.of(NORMALIZED_S3_SERVICE_NAME + "::Bucket"); remoteResourceIdentifier = @@ -579,7 +605,8 @@ private static void setRemoteResourceTypeAndIdentifier(SpanData span, Attributes getLambdaFunctionNameFromArn( Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_LAMBDA_NAME)))); cloudformationPrimaryIdentifier = - Optional.ofNullable(escapeDelimiters(span.getAttributes().get(AWS_LAMBDA_ARN))); + Optional.ofNullable( + escapeDelimiters(span.getAttributes().get(AWS_LAMBDA_FUNCTION_ARN))); } } else if (isKeyPresent(span, AWS_LAMBDA_RESOURCE_ID)) { remoteResourceType = Optional.of(NORMALIZED_LAMBDA_SERVICE_NAME + "::EventSourceMapping"); @@ -599,30 +626,137 @@ private static void setRemoteResourceTypeAndIdentifier(SpanData span, Attributes builder.put(AWS_REMOTE_RESOURCE_TYPE, remoteResourceType.get()); builder.put(AWS_REMOTE_RESOURCE_IDENTIFIER, remoteResourceIdentifier.get()); builder.put(AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER, cloudformationPrimaryIdentifier.get()); + return true; } + return false; } - private static Optional getLambdaFunctionNameFromArn(Optional stringArn) { - if (stringArn.isPresent() && stringArn.get().startsWith("arn:aws:lambda:")) { + private static void setRemoteResourceAccessKeyAndRegion( + SpanData span, AttributesBuilder builder) { + if (isKeyPresent(span, AWS_AUTH_ACCESS_KEY)) { + String remoteResourceAccessKey = span.getAttributes().get(AWS_AUTH_ACCESS_KEY); + builder.put(AWS_REMOTE_RESOURCE_ACCESS_KEY, remoteResourceAccessKey); + } + + if (isKeyPresent(span, AWS_AUTH_REGION)) { + String remoteResourceRegion = span.getAttributes().get(AWS_AUTH_REGION); + builder.put(AWS_REMOTE_RESOURCE_REGION, remoteResourceRegion); + } + } + + private static boolean setRemoteResourceAccountIdAndRegion( + SpanData span, AttributesBuilder builder) { + Optional remoteResourceAccountId = Optional.empty(); + Optional remoteResourceRegion = Optional.empty(); + List> ARN_ATTRIBUTES = + Arrays.asList( + AWS_TABLE_ARN, + AWS_STREAM_ARN, + AWS_SNS_TOPIC_ARN, + AWS_SECRET_ARN, + AWS_STEP_FUNCTIONS_ACTIVITY_ARN, + AWS_STATE_MACHINE_ARN, + AWS_GUARDRAIL_ARN, + AWS_LAMBDA_FUNCTION_ARN); + + if (isKeyPresent(span, AWS_QUEUE_URL)) { + String url = escapeDelimiters(span.getAttributes().get(AWS_QUEUE_URL)); + remoteResourceAccountId = SqsUrlParser.getAccountId(url); + remoteResourceRegion = SqsUrlParser.getRegion(url); + } else { + for (AttributeKey attributeKey : ARN_ATTRIBUTES) { + if (isKeyPresent(span, attributeKey)) { + String stringArn = escapeDelimiters(span.getAttributes().get(attributeKey)); + try { + Arn resourceArn = Arn.fromString(stringArn); + remoteResourceAccountId = Optional.of(resourceArn.getAccountId()); + remoteResourceRegion = Optional.of(resourceArn.getRegion()); + } catch (IllegalArgumentException e) { + logger.log( + Level.FINE, + String.format( + "Could not parse ARN to extract cross-account information: %s", stringArn)); + } + } + } + } + + if (remoteResourceAccountId.isPresent() && remoteResourceRegion.isPresent()) { + builder.put(AWS_REMOTE_RESOURCE_ACCOUNT_ID, remoteResourceAccountId.get()); + builder.put(AWS_REMOTE_RESOURCE_REGION, remoteResourceRegion.get()); + return true; + } + return false; + } + + private static Optional getKinesisStreamNameFromArn(Optional stringArn) { + try { Arn resourceArn = Arn.fromString(stringArn.get()); return Optional.of(resourceArn.getResource().toString().split(":")[1]); + } catch (IllegalArgumentException e) { + logger.log( + Level.FINE, String.format("Could not parse Kinesis stream name from ARN: %s", stringArn)); + } + return Optional.empty(); + } + + private static Optional getDynamodbTableNameFromArn(Optional stringArn) { + try { + Arn resourceArn = Arn.fromString(stringArn.get()); + return Optional.of(resourceArn.getResource().toString().split(":")[1]); + } catch (IllegalArgumentException e) { + logger.log( + Level.FINE, String.format("Could not parse DynamoDB table name from ARN: %s", stringArn)); + } + return Optional.empty(); + } + + private static Optional getLambdaFunctionNameFromArn(Optional stringArn) { + try { + if (stringArn.isPresent() && stringArn.get().startsWith("arn:aws:lambda:")) { + Arn resourceArn = Arn.fromString(stringArn.get()); + return Optional.of(resourceArn.getResource().toString().split(":")[1]); + } + } catch (IllegalArgumentException e) { + logger.log( + Level.FINE, + String.format("Could not parse Lambda resource name from ARN: %s", stringArn)); } return stringArn; } private static Optional getSecretsManagerResourceNameFromArn(Optional stringArn) { - Arn resourceArn = Arn.fromString(stringArn.get()); - return Optional.of(resourceArn.getResource().toString().split(":")[1]); + try { + Arn resourceArn = Arn.fromString(stringArn.get()); + return Optional.of(resourceArn.getResource().toString().split(":")[1]); + } catch (IllegalArgumentException e) { + logger.log( + Level.FINE, + String.format("Could not parse Secrets Manager resource name from ARN: %s", stringArn)); + } + return Optional.empty(); } private static Optional getSfnResourceNameFromArn(Optional stringArn) { - Arn resourceArn = Arn.fromString(stringArn.get()); - return Optional.of(resourceArn.getResource().toString().split(":")[1]); + try { + Arn resourceArn = Arn.fromString(stringArn.get()); + return Optional.of(resourceArn.getResource().toString().split(":")[1]); + } catch (IllegalArgumentException e) { + logger.log( + Level.FINE, String.format("Could not parse Sfn resource name from ARN: %s", stringArn)); + } + return Optional.empty(); } private static Optional getSnsResourceNameFromArn(Optional stringArn) { - Arn resourceArn = Arn.fromString(stringArn.get()); - return Optional.of(resourceArn.getResource().toString()); + try { + Arn resourceArn = Arn.fromString(stringArn.get()); + return Optional.of(resourceArn.getResource().toString()); + } catch (IllegalArgumentException e) { + logger.log( + Level.FINE, String.format("Could not parse Sfn resource name from ARN: %s", stringArn)); + } + return Optional.empty(); } /** diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java index b1f0e01b1b..539c863e23 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtil.java @@ -27,6 +27,7 @@ import static io.opentelemetry.semconv.SemanticAttributes.URL_PATH; import static software.amazon.opentelemetry.javaagent.providers.AwsApplicationSignalsCustomizerProvider.AWS_LAMBDA_FUNCTION_NAME_CONFIG; import static software.amazon.opentelemetry.javaagent.providers.AwsApplicationSignalsCustomizerProvider.isLambdaEnvironment; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_LOCAL_OPERATION_OVERRIDE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_OPERATION; import com.fasterxml.jackson.core.type.TypeReference; @@ -96,11 +97,23 @@ static List getDialectKeywords() { */ static String getIngressOperation(SpanData span) { if (isLambdaEnvironment()) { - String op = generateIngressOperation(span); - if (!op.equals(UNKNOWN_OPERATION)) { - return op; + /* + * By default the local operation of a Lambda span is hard-coded to "/FunctionHandler". + * To dynamically override this at runtime—such as when running a custom server inside your Lambda— + * you can set the span attribute "aws.lambda.local.operation.override" before ending the span. For example: + * + * // Obtain the current Span and override its operation name + * Span.current().setAttribute( + * "aws.lambda.local.operation.override", + * "MyServiceOperation"); + * + * The code below will detect that override and use it instead of the default. + */ + String operationOverride = span.getAttributes().get(AWS_LAMBDA_LOCAL_OPERATION_OVERRIDE); + if (operationOverride != null) { + return operationOverride; } - return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG) + "/FunctionHandler"; + return getFunctionNameFromEnv() + "/FunctionHandler"; } String operation = span.getName(); if (shouldUseInternalOperation(span)) { @@ -111,6 +124,11 @@ static String getIngressOperation(SpanData span) { return operation; } + // define a function so that we can mock it in unit test + static String getFunctionNameFromEnv() { + return System.getenv(AWS_LAMBDA_FUNCTION_NAME_CONFIG); + } + static String getEgressOperation(SpanData span) { if (shouldUseInternalOperation(span)) { return INTERNAL_OPERATION; diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/SqsUrlParser.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/SqsUrlParser.java index e69ccb091a..0517f9f516 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/SqsUrlParser.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/SqsUrlParser.java @@ -32,16 +32,54 @@ public static Optional getQueueName(String url) { if (url == null) { return Optional.empty(); } - url = url.replace(HTTP_SCHEMA, "").replace(HTTPS_SCHEMA, ""); - String[] splitUrl = url.split("/"); + String urlWithoutProtocol = url.replace(HTTP_SCHEMA, "").replace(HTTPS_SCHEMA, ""); + String[] splitUrl = urlWithoutProtocol.split("/"); if (splitUrl.length == 3 && isAccountId(splitUrl[1]) && isValidQueueName(splitUrl[2])) { return Optional.of(splitUrl[2]); } return Optional.empty(); } + /** Extracts the account ID from an SQS URL. */ + public static Optional getAccountId(String url) { + ParsedUrl parsed = parseUrl(url); + return Optional.ofNullable(parsed.accountId); + } + + /** Extracts the region from an SQS URL. */ + public static Optional getRegion(String url) { + ParsedUrl parsed = parseUrl(url); + return Optional.ofNullable(parsed.region); + } + + /** + * Parses an SQS URL and extracts its components. URL Format: + * https://sqs..amazonaws.com// + */ + private static ParsedUrl parseUrl(String url) { + if (url == null) { + return new ParsedUrl(null, null, null); + } + + String urlWithoutProtocol = url.replace(HTTP_SCHEMA, "").replace(HTTPS_SCHEMA, ""); + String[] splitUrl = urlWithoutProtocol.split("/"); + + if (splitUrl.length != 3 + || !isAccountId(splitUrl[1]) + || !isValidQueueName(splitUrl[2]) + || !splitUrl[0].toLowerCase().startsWith("sqs")) { + return new ParsedUrl(null, null, null); + } + + String domain = splitUrl[0]; + String[] domainParts = domain.split("\\."); + + String region = domainParts.length == 4 ? domainParts[1] : null; + return new ParsedUrl(splitUrl[2], splitUrl[1], region); + } + private static boolean isAccountId(String input) { - if (input == null || input.length() != 12) { + if (input == null) { return false; } @@ -67,4 +105,16 @@ private static boolean isValidQueueName(String input) { return true; } + + private static class ParsedUrl { + private final String queueName; + private final String accountId; + private final String region; + + private ParsedUrl(String queueName, String accountId, String region) { + this.queueName = queueName; + this.accountId = accountId; + this.region = region; + } + } } diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/SigV4AuthHeaderSupplier.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/AwsAuthHeaderSupplier.java similarity index 71% rename from awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/SigV4AuthHeaderSupplier.java rename to awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/AwsAuthHeaderSupplier.java index ca34d829ec..b47e0916ae 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/SigV4AuthHeaderSupplier.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/AwsAuthHeaderSupplier.java @@ -16,11 +16,14 @@ package software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.net.URI; import java.util.*; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.GZIPOutputStream; import software.amazon.awssdk.auth.credentials.AwsCredentials; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.http.SdkHttpFullRequest; @@ -29,11 +32,11 @@ import software.amazon.awssdk.http.auth.aws.signer.AwsV4HttpSigner; import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; -final class SigV4AuthHeaderSupplier implements Supplier> { +final class AwsAuthHeaderSupplier implements Supplier> { BaseOtlpAwsExporter exporter; Logger logger; - public SigV4AuthHeaderSupplier(BaseOtlpAwsExporter exporter) { + public AwsAuthHeaderSupplier(BaseOtlpAwsExporter exporter) { this.exporter = exporter; this.logger = Logger.getLogger(exporter.getClass().getName()); } @@ -41,7 +44,7 @@ public SigV4AuthHeaderSupplier(BaseOtlpAwsExporter exporter) { @Override public Map get() { try { - byte[] data = exporter.data.get(); + ByteArrayOutputStream data = exporter.data.get(); SdkHttpRequest httpRequest = SdkHttpFullRequest.builder() @@ -50,6 +53,14 @@ public Map get() { .putHeader("Content-Type", "application/x-protobuf") .build(); + // Compress the data before signing with gzip + ByteArrayOutputStream compressedData; + if (exporter.getCompression().equals(CompressionMethod.GZIP)) { + compressedData = compressWithGzip(data); + } else { + compressedData = data; + } + AwsCredentials credentials = DefaultCredentialsProvider.create().resolveCredentials(); SignedRequest signedRequest = @@ -60,7 +71,7 @@ public Map get() { .request(httpRequest) .putProperty(AwsV4HttpSigner.SERVICE_SIGNING_NAME, exporter.serviceName()) .putProperty(AwsV4HttpSigner.REGION_NAME, exporter.awsRegion) - .payload(() -> new ByteArrayInputStream(data))); + .payload(() -> new ByteArrayInputStream(compressedData.toByteArray()))); Map result = new HashMap<>(); @@ -84,4 +95,21 @@ public Map get() { return Collections.emptyMap(); } } + + /** + * Compresses the given byte array using GZIP compression. + * + * @param data the byte array stream to compress + * @return the compressed byte as a ByteArrayOutputStream + * @throws IOException if compression fails + */ + private ByteArrayOutputStream compressWithGzip(ByteArrayOutputStream data) throws IOException { + ByteArrayOutputStream compressedData = new ByteArrayOutputStream(); + + try (GZIPOutputStream gzipOut = new GZIPOutputStream(compressedData)) { + data.writeTo(gzipOut); + } + + return compressedData; + } } diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/BaseOtlpAwsExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/BaseOtlpAwsExporter.java index 864f4c0a82..08ae2cc618 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/BaseOtlpAwsExporter.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/BaseOtlpAwsExporter.java @@ -15,6 +15,7 @@ package software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common; +import java.io.ByteArrayOutputStream; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -27,15 +28,21 @@ public abstract class BaseOtlpAwsExporter { protected final String awsRegion; protected final String endpoint; - protected final AtomicReference data; + protected final AtomicReference data; protected final Supplier> headerSupplier; + protected final CompressionMethod compression; - protected BaseOtlpAwsExporter(String endpoint) { + protected BaseOtlpAwsExporter(String endpoint, CompressionMethod compression) { this.endpoint = endpoint.toLowerCase(); + this.compression = compression; this.awsRegion = endpoint.split("\\.")[1]; this.data = new AtomicReference<>(); - this.headerSupplier = new SigV4AuthHeaderSupplier(this); + this.headerSupplier = new AwsAuthHeaderSupplier(this); } public abstract String serviceName(); + + public CompressionMethod getCompression() { + return this.compression; + } } diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/CompressionMethod.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/CompressionMethod.java new file mode 100644 index 0000000000..ae63dd12ff --- /dev/null +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/common/CompressionMethod.java @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common; + +public enum CompressionMethod { + NONE, + GZIP +} diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporter.java index 1f1bd2a006..f93b1f1c9a 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporter.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporter.java @@ -28,6 +28,7 @@ import java.util.StringJoiner; import javax.annotation.Nonnull; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.BaseOtlpAwsExporter; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.CompressionMethod; /** * This exporter extends the functionality of the OtlpHttpLogsRecordExporter to allow logs to be @@ -42,19 +43,18 @@ public final class OtlpAwsLogsExporter extends BaseOtlpAwsExporter implements Lo private final OtlpHttpLogRecordExporter parentExporter; static OtlpAwsLogsExporter getDefault(String endpoint) { - return new OtlpAwsLogsExporter(endpoint); + return new OtlpAwsLogsExporter( + OtlpHttpLogRecordExporter.getDefault(), endpoint, CompressionMethod.NONE); } - static OtlpAwsLogsExporter create(OtlpHttpLogRecordExporter parent, String endpoint) { - return new OtlpAwsLogsExporter(parent, endpoint); + static OtlpAwsLogsExporter create( + OtlpHttpLogRecordExporter parent, String endpoint, CompressionMethod compression) { + return new OtlpAwsLogsExporter(parent, endpoint, compression); } - private OtlpAwsLogsExporter(String endpoint) { - this(OtlpHttpLogRecordExporter.getDefault(), endpoint); - } - - private OtlpAwsLogsExporter(OtlpHttpLogRecordExporter parentExporter, String endpoint) { - super(endpoint); + private OtlpAwsLogsExporter( + OtlpHttpLogRecordExporter parentExporter, String endpoint, CompressionMethod compression) { + super(endpoint, compression); this.parentExporterBuilder = parentExporter.toBuilder() @@ -75,7 +75,7 @@ public CompletableResultCode export(@Nonnull Collection logs) { try { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); LogsRequestMarshaler.create(logs).writeBinaryTo(buffer); - this.data.set(buffer.toByteArray()); + this.data.set(buffer); return this.parentExporter.export(logs); } catch (IOException e) { return CompletableResultCode.ofFailure(); diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporterBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporterBuilder.java index 440dce6d79..bf91bd6d4e 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporterBuilder.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/logs/OtlpAwsLogsExporterBuilder.java @@ -18,10 +18,12 @@ import static java.util.Objects.requireNonNull; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.CompressionMethod; public class OtlpAwsLogsExporterBuilder { private final OtlpHttpLogRecordExporter parentExporter; private final String endpoint; + private String compression; public static OtlpAwsLogsExporterBuilder create( OtlpHttpLogRecordExporter parentExporter, String endpoint) { @@ -32,8 +34,18 @@ public static OtlpAwsLogsExporter getDefault(String endpoint) { return OtlpAwsLogsExporter.getDefault(endpoint); } + public OtlpAwsLogsExporterBuilder setCompression(String compression) { + this.compression = compression; + return this; + } + public OtlpAwsLogsExporter build() { - return OtlpAwsLogsExporter.create(this.parentExporter, this.endpoint); + CompressionMethod compression = CompressionMethod.NONE; + if (this.compression != null && "gzip".equalsIgnoreCase(this.compression)) { + compression = CompressionMethod.GZIP; + } + + return OtlpAwsLogsExporter.create(this.parentExporter, this.endpoint, compression); } private OtlpAwsLogsExporterBuilder(OtlpHttpLogRecordExporter parentExporter, String endpoint) { diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/traces/OtlpAwsSpanExporter.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/traces/OtlpAwsSpanExporter.java index d2feba84a1..ff0dcf4cb3 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/traces/OtlpAwsSpanExporter.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/traces/OtlpAwsSpanExporter.java @@ -28,6 +28,7 @@ import java.util.StringJoiner; import javax.annotation.Nonnull; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.BaseOtlpAwsExporter; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.CompressionMethod; /** * This exporter extends the functionality of the OtlpHttpSpanExporter to allow spans to be exported @@ -40,19 +41,18 @@ public final class OtlpAwsSpanExporter extends BaseOtlpAwsExporter implements Sp private final OtlpHttpSpanExporter parentExporter; static OtlpAwsSpanExporter getDefault(String endpoint) { - return new OtlpAwsSpanExporter(endpoint); + return new OtlpAwsSpanExporter( + OtlpHttpSpanExporter.getDefault(), endpoint, CompressionMethod.NONE); } - static OtlpAwsSpanExporter create(OtlpHttpSpanExporter parent, String endpoint) { - return new OtlpAwsSpanExporter(parent, endpoint); + static OtlpAwsSpanExporter create( + OtlpHttpSpanExporter parent, String endpoint, CompressionMethod compression) { + return new OtlpAwsSpanExporter(parent, endpoint, compression); } - private OtlpAwsSpanExporter(String endpoint) { - this(OtlpHttpSpanExporter.getDefault(), endpoint); - } - - private OtlpAwsSpanExporter(OtlpHttpSpanExporter parentExporter, String endpoint) { - super(endpoint); + private OtlpAwsSpanExporter( + OtlpHttpSpanExporter parentExporter, String endpoint, CompressionMethod compression) { + super(endpoint, compression); this.parentExporterBuilder = parentExporter.toBuilder() @@ -73,7 +73,7 @@ public CompletableResultCode export(@Nonnull Collection spans) { try { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); TraceRequestMarshaler.create(spans).writeBinaryTo(buffer); - this.data.set(buffer.toByteArray()); + this.data.set(buffer); return this.parentExporter.export(spans); } catch (IOException e) { return CompletableResultCode.ofFailure(); diff --git a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/traces/OtlpAwsSpanExporterBuilder.java b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/traces/OtlpAwsSpanExporterBuilder.java index 1b0c725136..bef2d5a589 100644 --- a/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/traces/OtlpAwsSpanExporterBuilder.java +++ b/awsagentprovider/src/main/java/software/amazon/opentelemetry/javaagent/providers/exporter/otlp/aws/traces/OtlpAwsSpanExporterBuilder.java @@ -18,10 +18,12 @@ import static java.util.Objects.requireNonNull; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.CompressionMethod; public class OtlpAwsSpanExporterBuilder { private final OtlpHttpSpanExporter parentExporter; private final String endpoint; + private String compression; public static OtlpAwsSpanExporterBuilder create( OtlpHttpSpanExporter parentExporter, String endpoint) { @@ -32,12 +34,22 @@ public static OtlpAwsSpanExporter getDefault(String endpoint) { return OtlpAwsSpanExporter.getDefault(endpoint); } + public OtlpAwsSpanExporterBuilder setCompression(String compression) { + this.compression = compression; + return this; + } + private OtlpAwsSpanExporterBuilder(OtlpHttpSpanExporter parentExporter, String endpoint) { this.parentExporter = requireNonNull(parentExporter, "Must set a parentExporter"); this.endpoint = requireNonNull(endpoint, "Must set an endpoint"); } public OtlpAwsSpanExporter build() { - return OtlpAwsSpanExporter.create(this.parentExporter, this.endpoint); + CompressionMethod compression = CompressionMethod.NONE; + if (this.compression != null && "gzip".equalsIgnoreCase(this.compression)) { + compression = CompressionMethod.GZIP; + } + + return OtlpAwsSpanExporter.create(this.parentExporter, this.endpoint, compression); } } diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGeneratorTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGeneratorTest.java index 2811bec965..8b362193df 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGeneratorTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsMetricAttributeGeneratorTest.java @@ -22,13 +22,15 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_AGENT_ID; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_AUTH_ACCESS_KEY; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_AUTH_REGION; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_BUCKET_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_DATA_SOURCE_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_GUARDRAIL_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_GUARDRAIL_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_KNOWLEDGE_BASE_ID; -import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_ARN; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_FUNCTION_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_RESOURCE_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_OPERATION; @@ -38,7 +40,10 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_DB_USER; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_ENVIRONMENT; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_OPERATION; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_ACCESS_KEY; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_ACCOUNT_ID; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_IDENTIFIER; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_REGION; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_RESOURCE_TYPE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_REMOTE_SERVICE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SECRET_ARN; @@ -46,7 +51,9 @@ import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_SPAN_KIND; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STATE_MACHINE_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STREAM_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_STREAM_NAME; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_TABLE_ARN; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_TABLE_NAME; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.GEN_AI_REQUEST_MODEL; import static software.amazon.opentelemetry.javaagent.providers.MetricAttributeGenerator.DEPENDENCY_METRIC; @@ -65,6 +72,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -85,6 +93,8 @@ class AwsMetricAttributeGeneratorTest { private static final String UNKNOWN_REMOTE_OPERATION = "UnknownRemoteOperation"; private static final String INTERNAL_OPERATION = "InternalOperation"; private static final String LOCAL_ROOT = "LOCAL_ROOT"; + private static final String MOCK_ACCESS_KEY = "MockAccessKey"; + private static final String MOCK_REGION = "us-east-1"; private Attributes attributesMock; private SpanData spanDataMock; @@ -758,14 +768,20 @@ public void testPeerServiceDoesNotOverrideAwsRemoteService() { @Test public void testSdkClientSpanWithRemoteResourceAttributes() { mockAttribute(RPC_SYSTEM, "aws-api"); + mockAttribute(AWS_AUTH_ACCESS_KEY, MOCK_ACCESS_KEY); + mockAttribute(AWS_AUTH_REGION, MOCK_REGION); // Validate behaviour of aws bucket name attribute, then remove it. mockAttribute(AWS_BUCKET_NAME, "aws_s3_bucket_name"); validateRemoteResourceAttributes("AWS::S3::Bucket", "aws_s3_bucket_name"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_BUCKET_NAME, null); // Validate behaviour of AWS_QUEUE_NAME attribute, then remove it. mockAttribute(AWS_QUEUE_NAME, "aws_queue_name"); validateRemoteResourceAttributes("AWS::SQS::Queue", "aws_queue_name"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_QUEUE_NAME, null); // Validate behaviour of having both AWS_QUEUE_NAME and AWS_QUEUE_URL attribute, then remove @@ -773,6 +789,8 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { mockAttribute(AWS_QUEUE_URL, "https://sqs.us-east-2.amazonaws.com/123456789012/Queue"); mockAttribute(AWS_QUEUE_NAME, "aws_queue_name"); validateRemoteResourceAttributes("AWS::SQS::Queue", "aws_queue_name"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-2")); mockAttribute(AWS_QUEUE_URL, null); mockAttribute(AWS_QUEUE_NAME, null); @@ -780,63 +798,98 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { mockAttribute(AWS_QUEUE_URL, "invalidUrl"); mockAttribute(AWS_QUEUE_NAME, "aws_queue_name"); validateRemoteResourceAttributes("AWS::SQS::Queue", "aws_queue_name"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_QUEUE_URL, null); mockAttribute(AWS_QUEUE_NAME, null); // Validate behaviour of AWS_STREAM_NAME attribute, then remove it. mockAttribute(AWS_STREAM_NAME, "aws_stream_name"); validateRemoteResourceAttributes("AWS::Kinesis::Stream", "aws_stream_name"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_STREAM_NAME, null); + // Validate behaviour of AWS_STREAM_ARN attribute, then remove it. + mockAttribute(AWS_STREAM_ARN, "arn:aws:kinesis:us-east-1:123456789012:stream/test_stream"); + validateRemoteResourceAttributes("AWS::Kinesis::Stream", "test_stream"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); + mockAttribute(AWS_STREAM_ARN, null); + // Validate behaviour of AWS_TABLE_NAME attribute, then remove it. mockAttribute(AWS_TABLE_NAME, "aws_table_name"); validateRemoteResourceAttributes("AWS::DynamoDB::Table", "aws_table_name"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_TABLE_NAME, null); // Validate behaviour of AWS_TABLE_NAME attribute with special chars(|), then remove it. mockAttribute(AWS_TABLE_NAME, "aws_table|name"); validateRemoteResourceAttributes("AWS::DynamoDB::Table", "aws_table^|name"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_TABLE_NAME, null); // Validate behaviour of AWS_TABLE_NAME attribute with special chars(^), then remove it. mockAttribute(AWS_TABLE_NAME, "aws_table^name"); validateRemoteResourceAttributes("AWS::DynamoDB::Table", "aws_table^^name"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_TABLE_NAME, null); + // Validate behaviour of AWS_TABLE_ARN attribute, then remove it. + mockAttribute(AWS_TABLE_ARN, "arn:aws:dynamodb:us-east-1:123456789012:table/test_table"); + validateRemoteResourceAttributes("AWS::DynamoDB::Table", "test_table"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); + mockAttribute(AWS_TABLE_ARN, null); + // Validate behaviour of AWS_BEDROCK_AGENT_ID attribute, then remove it. mockAttribute(AWS_AGENT_ID, "test_agent_id"); validateRemoteResourceAttributes("AWS::Bedrock::Agent", "test_agent_id"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_AGENT_ID, null); // Validate behaviour of AWS_BEDROCK_AGENT_ID attribute with special chars(^), then remove it. mockAttribute(AWS_AGENT_ID, "test_agent_^id"); validateRemoteResourceAttributes("AWS::Bedrock::Agent", "test_agent_^^id"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_AGENT_ID, null); // Validate behaviour of AWS_KNOWLEDGE_BASE_ID attribute, then remove it. mockAttribute(AWS_KNOWLEDGE_BASE_ID, "test_knowledgeBase_id"); validateRemoteResourceAttributes("AWS::Bedrock::KnowledgeBase", "test_knowledgeBase_id"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_KNOWLEDGE_BASE_ID, null); // Validate behaviour of AWS_KNOWLEDGE_BASE_ID attribute with special chars(^), then remove it. mockAttribute(AWS_KNOWLEDGE_BASE_ID, "test_knowledgeBase_^id"); validateRemoteResourceAttributes("AWS::Bedrock::KnowledgeBase", "test_knowledgeBase_^^id"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_KNOWLEDGE_BASE_ID, null); // Validate behaviour of AWS_DATA_SOURCE_ID attribute, then remove it. mockAttribute(AWS_DATA_SOURCE_ID, "test_datasource_id"); validateRemoteResourceAttributes("AWS::Bedrock::DataSource", "test_datasource_id"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_DATA_SOURCE_ID, null); // Validate behaviour of AWS_DATA_SOURCE_ID attribute with special chars(^), then remove // it. mockAttribute(AWS_DATA_SOURCE_ID, "test_datasource_^id"); validateRemoteResourceAttributes("AWS::Bedrock::DataSource", "test_datasource_^^id"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_DATA_SOURCE_ID, null); // Validate behaviour of AWS_GUARDRAIL_ID attribute, then remove it. mockAttribute(AWS_GUARDRAIL_ID, "test_guardrail_id"); - validateRemoteResourceAttributes("AWS::Bedrock::Guardrail", "test_guardrail_id"); // Also test with ARN to verify cloudformationPrimaryIdentifier uses ARN mockAttribute( AWS_GUARDRAIL_ARN, "arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_id"); @@ -844,12 +897,13 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { "AWS::Bedrock::Guardrail", "test_guardrail_id", "arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_id"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); mockAttribute(AWS_GUARDRAIL_ID, null); mockAttribute(AWS_GUARDRAIL_ARN, null); // Validate behaviour of AWS_GUARDRAIL_ID attribute with special chars(^), then remove it. mockAttribute(AWS_GUARDRAIL_ID, "test_guardrail_^id"); - validateRemoteResourceAttributes("AWS::Bedrock::Guardrail", "test_guardrail_^^id"); // Also test with ARN containing special chars to verify delimiter escaping in // cloudformationPrimaryIdentifier mockAttribute( @@ -858,24 +912,32 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { "AWS::Bedrock::Guardrail", "test_guardrail_^^id", "arn:aws:bedrock:us-east-1:123456789012:guardrail/test_guardrail_^^id"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); mockAttribute(AWS_GUARDRAIL_ID, null); mockAttribute(AWS_GUARDRAIL_ARN, null); // Validate behaviour of GEN_AI_REQUEST_MODEL attribute, then remove it. mockAttribute(GEN_AI_REQUEST_MODEL, "test.service_id"); validateRemoteResourceAttributes("AWS::Bedrock::Model", "test.service_id"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(GEN_AI_REQUEST_MODEL, null); // Validate behaviour of GEN_AI_REQUEST_MODEL attribute with special chars(^), then // remove it. mockAttribute(GEN_AI_REQUEST_MODEL, "test.service_^id"); validateRemoteResourceAttributes("AWS::Bedrock::Model", "test.service_^^id"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(GEN_AI_REQUEST_MODEL, null); // Validate behaviour of AWS_STATE_MACHINE_ARN attribute, then remove it. mockAttribute( AWS_STATE_MACHINE_ARN, "arn:aws:states:us-east-1:123456789012:stateMachine:test_state_machine"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); validateRemoteResourceAttributes( "AWS::StepFunctions::StateMachine", "test_state_machine", @@ -885,15 +947,21 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { // Validate behaviour of AWS_STEPFUNCTIONS_ACTIVITY_ARN, then remove it. mockAttribute( AWS_STEP_FUNCTIONS_ACTIVITY_ARN, - "arn:aws:states:us-east-1:007003123456789012:activity:testActivity"); + "arn:aws:states:us-east-1:123456789012:activity:testActivity"); + mockAttribute(AWS_AUTH_ACCESS_KEY, MOCK_ACCESS_KEY); + mockAttribute(AWS_AUTH_REGION, MOCK_REGION); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); validateRemoteResourceAttributes( "AWS::StepFunctions::Activity", "testActivity", - "arn:aws:states:us-east-1:007003123456789012:activity:testActivity"); + "arn:aws:states:us-east-1:123456789012:activity:testActivity"); mockAttribute(AWS_STEP_FUNCTIONS_ACTIVITY_ARN, null); // Validate behaviour of AWS_SNS_TOPIC_ARN, then remove it. mockAttribute(AWS_SNS_TOPIC_ARN, "arn:aws:sns:us-west-2:012345678901:testTopic"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("012345678901"), Optional.empty(), Optional.of("us-west-2")); validateRemoteResourceAttributes( "AWS::SNS::Topic", "testTopic", "arn:aws:sns:us-west-2:012345678901:testTopic"); mockAttribute(AWS_SNS_TOPIC_ARN, null); @@ -901,6 +969,8 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { // Validate behaviour of AWS_SECRET_ARN, then remove it. mockAttribute( AWS_SECRET_ARN, "arn:aws:secretsmanager:us-east-1:123456789012:secret:secretName"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); validateRemoteResourceAttributes( "AWS::SecretsManager::Secret", "secretName", @@ -911,29 +981,35 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { mockAttribute(RPC_SERVICE, "Lambda"); mockAttribute(RPC_METHOD, "GetFunction"); mockAttribute(AWS_LAMBDA_NAME, "testLambdaName"); - mockAttribute(AWS_LAMBDA_ARN, "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName"); + mockAttribute( + AWS_LAMBDA_FUNCTION_ARN, "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName"); validateRemoteResourceAttributes( "AWS::Lambda::Function", "testLambdaName", "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); mockAttribute(RPC_SERVICE, null); mockAttribute(RPC_METHOD, null); mockAttribute(AWS_LAMBDA_NAME, null); - mockAttribute(AWS_LAMBDA_ARN, null); + mockAttribute(AWS_LAMBDA_FUNCTION_ARN, null); // Validate behaviour of AWS_LAMBDA_NAME containing ARN for non-Invoke operations mockAttribute(RPC_SERVICE, "Lambda"); mockAttribute(RPC_METHOD, "ListFunctions"); mockAttribute(AWS_LAMBDA_NAME, "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName"); - mockAttribute(AWS_LAMBDA_ARN, "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName"); + mockAttribute( + AWS_LAMBDA_FUNCTION_ARN, "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName"); validateRemoteResourceAttributes( "AWS::Lambda::Function", "testLambdaName", "arn:aws:lambda:us-east-1:123456789012:function:testLambdaName"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); mockAttribute(RPC_SERVICE, null); mockAttribute(RPC_METHOD, null); mockAttribute(AWS_LAMBDA_NAME, null); - mockAttribute(AWS_LAMBDA_ARN, null); + mockAttribute(AWS_LAMBDA_FUNCTION_ARN, null); // Validate that Lambda Invoke with function name treats Lambda as a service, not a resource mockAttribute(RPC_SERVICE, "Lambda"); @@ -956,8 +1032,64 @@ public void testSdkClientSpanWithRemoteResourceAttributes() { // Validate behaviour of AWS_LAMBDA_RESOURCE_ID mockAttribute(AWS_LAMBDA_RESOURCE_ID, "eventSourceId"); validateRemoteResourceAttributes("AWS::Lambda::EventSourceMapping", "eventSourceId"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); mockAttribute(AWS_LAMBDA_RESOURCE_ID, null); + // Validate behaviour of AWS_LAMBDA_FUNCTION_NAME + mockAttribute(AWS_LAMBDA_RESOURCE_ID, "eventSourceId"); + validateRemoteResourceAttributes("AWS::Lambda::EventSourceMapping", "eventSourceId"); + validateRemoteResourceAccountIdAndRegion( + Optional.empty(), Optional.of(MOCK_ACCESS_KEY), Optional.of(MOCK_REGION)); + mockAttribute(AWS_LAMBDA_RESOURCE_ID, null); + + // Cross account support + // Invalid arn but account access key is available + mockAttribute(AWS_SECRET_ARN, "invalid_arn"); + validateRemoteResourceAccountIdAndRegion(Optional.empty(), Optional.empty(), Optional.empty()); + mockAttribute(AWS_SECRET_ARN, null); + + // Both account access key and account id are not available + mockAttribute(AWS_AUTH_REGION, null); + mockAttribute(AWS_AUTH_ACCESS_KEY, null); + mockAttribute(AWS_BUCKET_NAME, "aws_s3_bucket_name"); + validateRemoteResourceAttributes("AWS::S3::Bucket", "aws_s3_bucket_name"); + validateRemoteResourceAccountIdAndRegion(Optional.empty(), Optional.empty(), Optional.empty()); + mockAttribute(AWS_BUCKET_NAME, null); + + // Account access key is not available + mockAttribute( + AWS_SECRET_ARN, "arn:aws:secretsmanager:us-east-1:123456789012:secret:secretName"); + validateRemoteResourceAttributes( + "AWS::SecretsManager::Secret", + "secretName", + "arn:aws:secretsmanager:us-east-1:123456789012:secret:secretName"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("us-east-1")); + mockAttribute(AWS_SECRET_ARN, null); + + // Arn with invalid account id + mockAttribute( + AWS_SECRET_ARN, "arn:aws:secretsmanager:us-east-1:invalid_account_id:secret:secretName"); + validateRemoteResourceAttributes( + "AWS::SecretsManager::Secret", + "secretName", + "arn:aws:secretsmanager:us-east-1:invalid_account_id:secret:secretName"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("invalid_account_id"), Optional.empty(), Optional.of("us-east-1")); + mockAttribute(AWS_SECRET_ARN, null); + + // Arn with invalid region + mockAttribute( + AWS_SECRET_ARN, "arn:aws:secretsmanager:invalid_region:123456789012:secret:secretName"); + validateRemoteResourceAttributes( + "AWS::SecretsManager::Secret", + "secretName", + "arn:aws:secretsmanager:invalid_region:123456789012:secret:secretName"); + validateRemoteResourceAccountIdAndRegion( + Optional.of("123456789012"), Optional.empty(), Optional.of("invalid_region")); + mockAttribute(AWS_SECRET_ARN, null); + mockAttribute(RPC_SYSTEM, "null"); } @@ -1284,6 +1416,47 @@ private void validatePeerServiceDoesOverride(AttributeKey remoteServiceK mockAttribute(PEER_SERVICE, null); } + private void validateRemoteResourceAccountIdAndRegion( + Optional accountId, Optional accessKey, Optional region) { + SpanKind[] spanKinds = {SpanKind.CLIENT, SpanKind.PRODUCER, SpanKind.CONSUMER}; + + for (SpanKind spanKind : spanKinds) { + when(spanDataMock.getKind()).thenReturn(spanKind); + Attributes actualAttributes = + GENERATOR + .generateMetricAttributeMapFromSpan(spanDataMock, resource) + .get(DEPENDENCY_METRIC); + + if (region.isPresent()) { + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_REGION)).isEqualTo(region.get()); + } else { + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_REGION)).isEqualTo(null); + } + + if (accountId.isPresent()) { + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)).isEqualTo(accountId.get()); + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)).isEqualTo(null); + } else { + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)).isEqualTo(null); + } + + if (accessKey.isPresent()) { + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)).isEqualTo(accessKey.get()); + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)).isEqualTo(null); + } else { + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)).isEqualTo(null); + } + } + + // Server span should not generate remote resource attributes + when(spanDataMock.getKind()).thenReturn(SpanKind.SERVER); + Attributes actualAttributes = + GENERATOR.generateMetricAttributeMapFromSpan(spanDataMock, resource).get(SERVICE_METRIC); + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_ACCESS_KEY)).isNull(); + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_ACCOUNT_ID)).isNull(); + assertThat(actualAttributes.get(AWS_REMOTE_RESOURCE_REGION)).isNull(); + } + private void validateRemoteResourceAttributes(String type, String identifier) { validateRemoteResourceAttributes(type, identifier, identifier); } diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java index f3500a2aa6..ea576a7303 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/AwsSpanProcessingUtilTest.java @@ -21,8 +21,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Answers.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; +import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LAMBDA_LOCAL_OPERATION_OVERRIDE; import static software.amazon.opentelemetry.javaagent.providers.AwsAttributeKeys.AWS_LOCAL_OPERATION; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.MAX_KEYWORD_LENGTH; import static software.amazon.opentelemetry.javaagent.providers.AwsSpanProcessingUtil.getDialectKeywords; @@ -36,6 +40,7 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; public class AwsSpanProcessingUtilTest { private static final String DEFAULT_PATH_VALUE = "/"; @@ -123,6 +128,49 @@ public void testGetIngressOperationInvalidNameAndValidTargetAndMethod() { assertThat(actualOperation).isEqualTo(validMethod + " " + validTarget); } + @Test + public void testGetIngressOperationLambdaOverride() { + try (MockedStatic providerStatic = + mockStatic( + AwsApplicationSignalsCustomizerProvider.class, + withSettings().defaultAnswer(CALLS_REAL_METHODS))) { + // Force Lambda environment branch + providerStatic + .when(AwsApplicationSignalsCustomizerProvider::isLambdaEnvironment) + .thenReturn(true); + // Simulate an override attribute on the span + when(attributesMock.get(AWS_LAMBDA_LOCAL_OPERATION_OVERRIDE)).thenReturn("MyOverrideOp"); + + String actualOperation = AwsSpanProcessingUtil.getIngressOperation(spanDataMock); + assertThat(actualOperation).isEqualTo("MyOverrideOp"); + } + } + + @Test + public void testGetIngressOperationLambdaDefault() throws Exception { + try ( + // Mock the AWS environment check + MockedStatic providerStatic = + mockStatic( + AwsApplicationSignalsCustomizerProvider.class, + withSettings().defaultAnswer(CALLS_REAL_METHODS)); + // Mock only getFunctionNameFromEnv, leave all other util logic untouched + MockedStatic utilStatic = + mockStatic( + AwsSpanProcessingUtil.class, withSettings().defaultAnswer(CALLS_REAL_METHODS))) { + // force lambda branch and no override attribute + providerStatic + .when(AwsApplicationSignalsCustomizerProvider::isLambdaEnvironment) + .thenReturn(true); + when(attributesMock.get(AWS_LAMBDA_LOCAL_OPERATION_OVERRIDE)).thenReturn(null); + // Provide a deterministic function name + utilStatic.when(AwsSpanProcessingUtil::getFunctionNameFromEnv).thenReturn("MockFunction"); + + String actual = AwsSpanProcessingUtil.getIngressOperation(spanDataMock); + assertThat(actual).isEqualTo("MockFunction/FunctionHandler"); + } + } + @Test public void testGetEgressOperationUseInternalOperation() { String invalidName = null; diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/OtlpAwsExporterTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/OtlpAwsExporterTest.java index 69f6ab029c..0af51a345a 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/OtlpAwsExporterTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/OtlpAwsExporterTest.java @@ -50,7 +50,10 @@ import software.amazon.awssdk.http.auth.spi.signer.SignRequest.Builder; import software.amazon.awssdk.http.auth.spi.signer.SignedRequest; import software.amazon.awssdk.identity.spi.AwsCredentialsIdentity; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.common.CompressionMethod; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogsExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.logs.OtlpAwsLogsExporterBuilder; +import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporter; import software.amazon.opentelemetry.javaagent.providers.exporter.otlp.aws.traces.OtlpAwsSpanExporterBuilder; interface OtlpAwsExporterTest { @@ -215,15 +218,51 @@ static class OtlpAwsSpanExporterTest extends AbstractOtlpAwsExporterTest { @BeforeEach @Override void setup() { - when(this.mockExporter.toBuilder()).thenReturn(mockBuilder); - when(this.mockBuilder.setEndpoint(any())).thenReturn(mockBuilder); - when(this.mockBuilder.setMemoryMode(any())).thenReturn(this.mockBuilder); - when(this.mockBuilder.setHeaders(this.headersCaptor.capture())).thenReturn(mockBuilder); - when(this.mockBuilder.build()).thenReturn(this.mockExporter); + lenient().when(this.mockExporter.toBuilder()).thenReturn(mockBuilder); + lenient().when(this.mockBuilder.setEndpoint(any())).thenReturn(mockBuilder); + lenient().when(this.mockBuilder.setMemoryMode(any())).thenReturn(this.mockBuilder); + lenient() + .when(this.mockBuilder.setHeaders(this.headersCaptor.capture())) + .thenReturn(mockBuilder); + lenient().when(this.mockBuilder.build()).thenReturn(this.mockExporter); OtlpAwsExporterTest tester = new MockOtlpAwsSpanExporterWrapper(this.mockExporter); this.init(XRAY_OTLP_ENDPOINT, tester); super.setup(); - when(this.mockExporter.export(any())).thenReturn(CompletableResultCode.ofSuccess()); + lenient().when(this.mockExporter.export(any())).thenReturn(CompletableResultCode.ofSuccess()); + } + + @Test + void testSpanExporterCompressionDefaultsToNone() { + OtlpAwsSpanExporter exporter = + OtlpAwsSpanExporterBuilder.create(this.mockExporter, XRAY_OTLP_ENDPOINT).build(); + assertEquals(CompressionMethod.NONE, exporter.getCompression()); + } + + @Test + void testSpanExporterCompressionCanBeSetToGzip() { + OtlpAwsSpanExporter exporter = + OtlpAwsSpanExporterBuilder.create(this.mockExporter, XRAY_OTLP_ENDPOINT) + .setCompression("gzip") + .build(); + assertEquals(CompressionMethod.GZIP, exporter.getCompression()); + } + + @Test + void testSpanExporterCompressionIgnoresCaseForGzip() { + OtlpAwsSpanExporter exporter = + OtlpAwsSpanExporterBuilder.create(this.mockExporter, XRAY_OTLP_ENDPOINT) + .setCompression("GZIP") + .build(); + assertEquals(CompressionMethod.GZIP, exporter.getCompression()); + } + + @Test + void testSpanExporterCompressionDefaultsToNoneForUnknownValue() { + OtlpAwsSpanExporter exporter = + OtlpAwsSpanExporterBuilder.create(this.mockExporter, XRAY_OTLP_ENDPOINT) + .setCompression("unknown") + .build(); + assertEquals(CompressionMethod.NONE, exporter.getCompression()); } private static final class MockOtlpAwsSpanExporterWrapper implements OtlpAwsExporterTest { @@ -252,15 +291,51 @@ static class OtlpAwsLogsExporterTest extends AbstractOtlpAwsExporterTest { @BeforeEach @Override void setup() { - when(this.mockExporter.toBuilder()).thenReturn(mockBuilder); - when(this.mockBuilder.setEndpoint(any())).thenReturn(mockBuilder); - when(this.mockBuilder.setMemoryMode(any())).thenReturn(this.mockBuilder); - when(this.mockBuilder.setHeaders(this.headersCaptor.capture())).thenReturn(mockBuilder); - when(this.mockBuilder.build()).thenReturn(this.mockExporter); + lenient().when(this.mockExporter.toBuilder()).thenReturn(mockBuilder); + lenient().when(this.mockBuilder.setEndpoint(any())).thenReturn(mockBuilder); + lenient().when(this.mockBuilder.setMemoryMode(any())).thenReturn(this.mockBuilder); + lenient() + .when(this.mockBuilder.setHeaders(this.headersCaptor.capture())) + .thenReturn(mockBuilder); + lenient().when(this.mockBuilder.build()).thenReturn(this.mockExporter); OtlpAwsExporterTest mocker = new MockOtlpAwsLogsExporterWrapper(this.mockExporter); this.init(LOGS_OTLP_ENDPOINT, mocker); super.setup(); - when(this.mockExporter.export(any())).thenReturn(CompletableResultCode.ofSuccess()); + lenient().when(this.mockExporter.export(any())).thenReturn(CompletableResultCode.ofSuccess()); + } + + @Test + void testLogsExporterCompressionDefaultsToNone() { + OtlpAwsLogsExporter exporter = + OtlpAwsLogsExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT).build(); + assertEquals(CompressionMethod.NONE, exporter.getCompression()); + } + + @Test + void testLogsExporterCompressionCanBeSetToGzip() { + OtlpAwsLogsExporter exporter = + OtlpAwsLogsExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) + .setCompression("gzip") + .build(); + assertEquals(CompressionMethod.GZIP, exporter.getCompression()); + } + + @Test + void testLogsExporterCompressionIgnoresCaseForGzip() { + OtlpAwsLogsExporter exporter = + OtlpAwsLogsExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) + .setCompression("GZIP") + .build(); + assertEquals(CompressionMethod.GZIP, exporter.getCompression()); + } + + @Test + void testLogsExporterCompressionDefaultsToNoneForUnknownValue() { + OtlpAwsLogsExporter exporter = + OtlpAwsLogsExporterBuilder.create(this.mockExporter, LOGS_OTLP_ENDPOINT) + .setCompression("unknown") + .build(); + assertEquals(CompressionMethod.NONE, exporter.getCompression()); } private static final class MockOtlpAwsLogsExporterWrapper implements OtlpAwsExporterTest { diff --git a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/SqsUrlParserTest.java b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/SqsUrlParserTest.java index 551d02b7b5..d5aaff3122 100644 --- a/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/SqsUrlParserTest.java +++ b/awsagentprovider/src/test/java/software/amazon/opentelemetry/javaagent/providers/SqsUrlParserTest.java @@ -24,59 +24,105 @@ public class SqsUrlParserTest { @Test public void testSqsClientSpanBasicUrls() { - validate("https://sqs.us-east-1.amazonaws.com/123412341234/Q_Name-5", "Q_Name-5"); - validate("https://sqs.af-south-1.amazonaws.com/999999999999/-_ThisIsValid", "-_ThisIsValid"); - validate("http://sqs.eu-west-3.amazonaws.com/000000000000/FirstQueue", "FirstQueue"); - validate("sqs.sa-east-1.amazonaws.com/123456781234/SecondQueue", "SecondQueue"); + validateGetQueueName("https://sqs.us-east-1.amazonaws.com/123412341234/Q_Name-5", "Q_Name-5"); + validateGetQueueName( + "https://sqs.af-south-1.amazonaws.com/999999999999/-_ThisIsValid", "-_ThisIsValid"); + validateGetQueueName( + "http://sqs.eu-west-3.amazonaws.com/000000000000/FirstQueue", "FirstQueue"); + validateGetQueueName("sqs.sa-east-1.amazonaws.com/123456781234/SecondQueue", "SecondQueue"); } @Test public void testSqsClientSpanLegacyFormatUrls() { - validate("https://ap-northeast-2.queue.amazonaws.com/123456789012/MyQueue", "MyQueue"); - validate("http://cn-northwest-1.queue.amazonaws.com/123456789012/MyQueue", "MyQueue"); - validate("http://cn-north-1.queue.amazonaws.com/123456789012/MyQueue", "MyQueue"); - validate( + validateGetQueueName( + "https://ap-northeast-2.queue.amazonaws.com/123456789012/MyQueue", "MyQueue"); + validateGetQueueName( + "http://cn-northwest-1.queue.amazonaws.com/123456789012/MyQueue", "MyQueue"); + validateGetQueueName("http://cn-north-1.queue.amazonaws.com/123456789012/MyQueue", "MyQueue"); + validateGetQueueName( "ap-south-1.queue.amazonaws.com/123412341234/MyLongerQueueNameHere", "MyLongerQueueNameHere"); - validate("https://queue.amazonaws.com/123456789012/MyQueue", "MyQueue"); + validateGetQueueName("https://queue.amazonaws.com/123456789012/MyQueue", "MyQueue"); } @Test public void testSqsClientSpanCustomUrls() { - validate("http://127.0.0.1:1212/123456789012/MyQueue", "MyQueue"); - validate("https://127.0.0.1:1212/123412341234/RRR", "RRR"); - validate("127.0.0.1:1212/123412341234/QQ", "QQ"); - validate("https://amazon.com/123412341234/BB", "BB"); + validateGetQueueName("http://127.0.0.1:1212/123456789012/MyQueue", "MyQueue"); + validateGetQueueName("https://127.0.0.1:1212/123412341234/RRR", "RRR"); + validateGetQueueName("127.0.0.1:1212/123412341234/QQ", "QQ"); + validateGetQueueName("https://amazon.com/123412341234/BB", "BB"); } @Test public void testSqsClientSpanLongUrls() { String queueName = "a".repeat(80); - validate("http://127.0.0.1:1212/123456789012/" + queueName, queueName); + validateGetQueueName("http://127.0.0.1:1212/123456789012/" + queueName, queueName); String queueNameTooLong = "a".repeat(81); - validate("http://127.0.0.1:1212/123456789012/" + queueNameTooLong, null); + validateGetQueueName("http://127.0.0.1:1212/123456789012/" + queueNameTooLong, null); } @Test public void testClientSpanSqsInvalidOrEmptyUrls() { - validate(null, null); - validate("", null); - validate(" ", null); - validate("/", null); - validate("//", null); - validate("///", null); - validate("//asdf", null); - validate("/123412341234/as&df", null); - validate("invalidUrl", null); - validate("https://www.amazon.com", null); - validate("https://sqs.us-east-1.amazonaws.com/123412341234/.", null); - validate("https://sqs.us-east-1.amazonaws.com/12/Queue", null); - validate("https://sqs.us-east-1.amazonaws.com/A/A", null); - validate("https://sqs.us-east-1.amazonaws.com/123412341234/A/ThisShouldNotBeHere", null); + validateGetQueueName(null, null); + validateGetQueueName("", null); + validateGetQueueName(" ", null); + validateGetQueueName("/", null); + validateGetQueueName("//", null); + validateGetQueueName("///", null); + validateGetQueueName("//asdf", null); + validateGetQueueName("/123412341234/as&df", null); + validateGetQueueName("invalidUrl", null); + validateGetQueueName("https://www.amazon.com", null); + validateGetQueueName("https://sqs.us-east-1.amazonaws.com/123412341234/.", null); + validateGetQueueName("https://sqs.us-east-1.amazonaws.com/12xxxxxxxxxx/Queue", null); + validateGetQueueName("https://sqs.us-east-1.amazonaws.com/A/A", null); + validateGetQueueName( + "https://sqs.us-east-1.amazonaws.com/123412341234/A/ThisShouldNotBeHere", null); } - private void validate(String url, String expectedName) { + @Test + public void testClientSpanSqsAccountId() { + validateGetAccountId(null, null); + validateGetAccountId("", null); + validateGetAccountId(" ", null); + validateGetAccountId("/", null); + validateGetAccountId("//", null); + validateGetAccountId("///", null); + validateGetAccountId("//asdf", null); + validateGetAccountId("/123412341234/as&df", null); + validateGetAccountId("invalidUrl", null); + validateGetAccountId("https://www.amazon.com", null); + validateGetAccountId("https://sqs.us-east-1.amazonaws.com/123412341234/Queue", "123412341234"); + validateGetAccountId("https://sqs.us-east-1.amazonaws.com/12341234/Queue", "12341234"); + validateGetAccountId("https://sqs.us-east-1.amazonaws.com/1234123412xx/Queue", null); + validateGetAccountId("https://sqs.us-east-1.amazonaws.com/1234123412xx", null); + } + + @Test + public void testClientSpanSqsRegion() { + validateGetRegion(null, null); + validateGetRegion("", null); + validateGetRegion(" ", null); + validateGetRegion("/", null); + validateGetRegion("//", null); + validateGetRegion("///", null); + validateGetRegion("//asdf", null); + validateGetRegion("/123412341234/as&df", null); + validateGetRegion("invalidUrl", null); + validateGetRegion("https://www.amazon.com", null); + validateGetRegion("https://sqs.us-east-1.amazonaws.com/123412341234/Queue", "us-east-1"); + } + + private void validateGetRegion(String url, String expectedRegion) { + assertThat(SqsUrlParser.getRegion(url)).isEqualTo(Optional.ofNullable(expectedRegion)); + } + + private void validateGetAccountId(String url, String expectedAccountId) { + assertThat(SqsUrlParser.getAccountId(url)).isEqualTo(Optional.ofNullable(expectedAccountId)); + } + + private void validateGetQueueName(String url, String expectedName) { assertThat(SqsUrlParser.getQueueName(url)).isEqualTo(Optional.ofNullable(expectedName)); } } diff --git a/build.gradle.kts b/build.gradle.kts index 973d7d73ba..843124bd80 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,8 +40,8 @@ nebulaRelease { nexusPublishing { repositories { sonatype { - nexusUrl.set(uri("https://aws.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://aws.oss.sonatype.org/content/repositories/snapshots/")) + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) username.set(System.getenv("PUBLISH_TOKEN_USERNAME")) password.set(System.getenv("PUBLISH_TOKEN_PASSWORD")) } diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 00aa199d82..55ed00a16c 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -27,7 +27,7 @@ data class DependencySet(val group: String, val version: String, val modules: Li val testSnapshots = rootProject.findProperty("testUpstreamSnapshots") == "true" // This is the version of the upstream instrumentation BOM -val otelVersion = "2.11.0-adot2" +val otelVersion = "2.11.0-adot3" val otelSnapshotVersion = "2.12.0" val otelAlphaVersion = if (!testSnapshots) "$otelVersion-alpha" else "$otelSnapshotVersion-alpha-SNAPSHOT" val otelJavaAgentVersion = if (!testSnapshots) otelVersion else "$otelSnapshotVersion-SNAPSHOT" @@ -45,7 +45,7 @@ val dependencyBoms = listOf( "org.junit:junit-bom:5.10.1", "org.springframework.boot:spring-boot-dependencies:2.7.17", "org.testcontainers:testcontainers-bom:1.19.3", - "software.amazon.awssdk:bom:2.21.33", + "software.amazon.awssdk:bom:2.30.17", ) val dependencySets = listOf( diff --git a/exporters/aws-distro-opentelemetry-xray-udp-span-exporter/build.gradle.kts b/exporters/aws-distro-opentelemetry-xray-udp-span-exporter/build.gradle.kts index 9aec0ccbed..f638f1965b 100644 --- a/exporters/aws-distro-opentelemetry-xray-udp-span-exporter/build.gradle.kts +++ b/exporters/aws-distro-opentelemetry-xray-udp-span-exporter/build.gradle.kts @@ -101,8 +101,8 @@ tasks.create("printVersion") { nexusPublishing { repositories { sonatype { - nexusUrl.set(uri("https://aws.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://aws.oss.sonatype.org/content/repositories/snapshots/")) + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) username.set(System.getenv("PUBLISH_TOKEN_USERNAME")) password.set(System.getenv("PUBLISH_TOKEN_PASSWORD")) } diff --git a/exporters/aws-distro-opentelemetry-xray-udp-span-exporter/settings.gradle.kts b/exporters/aws-distro-opentelemetry-xray-udp-span-exporter/settings.gradle.kts index 33986beb6b..9cb79500f1 100644 --- a/exporters/aws-distro-opentelemetry-xray-udp-span-exporter/settings.gradle.kts +++ b/exporters/aws-distro-opentelemetry-xray-udp-span-exporter/settings.gradle.kts @@ -6,7 +6,7 @@ dependencyResolutionManagement { mavenLocal() maven { - setUrl("https://oss.sonatype.org/content/repositories/snapshots") + setUrl("https://central.sonatype.com/repository/maven-snapshots/") } } } @@ -16,4 +16,4 @@ pluginManagement { id("io.github.gradle-nexus.publish-plugin") version "2.0.0" id("nebula.release") version "18.0.6" } -} \ No newline at end of file +} diff --git a/instrumentation/aws-sdk/README.md b/instrumentation/aws-sdk/README.md new file mode 100644 index 0000000000..1b4d677d3e --- /dev/null +++ b/instrumentation/aws-sdk/README.md @@ -0,0 +1,163 @@ +## ADOT AWS SDK Instrumentation + +### Overview +The aws-sdk instrumentation is an SPI-based implementation that extends the upstream OpenTelemetry AWS Java SDK instrumentation. + +##### _v1.11 Initialization Workflow_ +1. OpenTelemetry Agent Starts + - Loads default instrumentations + - Loads aws-sdk v1.11 instrumentations + - Injects **TracingRequestHandler** into constructor +2. Scans for other SPI implementations + - Finds ADOT’s **AdotAwsSdkInstrumentationModule** + - Injects code that: + - Checks for TracingRequestHandler + - If present, adds **AdotAwsSdkTracingRequestHandler** +3. AWS SDK Client Created + - Constructor runs with injected code: + [AWS Handlers] → TracingRequestHandler → AdotAwsSdkTracingRequestHandler + +##### _v2.2 Initialization Workflow_ + +1. OpenTelemetry Agent starts + - Loads default instrumentations + - Loads aws-sdk instrumentation from opentelemetry-java-instrumentation + - Registers **TracingExecutionInterceptor** (order = 0) +2. Scans for other SPI implementations + - Finds ADOT’s **AdotAwsSdkInstrumentationModule** + - Registers **AdotAwsSdkTracingExecutionInterceptor** (order > 0) + +#### _Note on Attribute Collection:_ +AWS SDK v1.11 and v2.2 handle attribute collection differently: + +**V1.11:** +- Maintains a separate AttributesBuilder during request/response lifecycle +- Collects ADOT-specific attributes alongside upstream processing without interference +- Injects collected attributes into span at the end of the request and response lifecycle hooks + + +**V2.2:** +- FieldMapper directly modifies spans during request/response processing +- Attributes are added to spans immediately when discovered +- Direct integration with span lifecycle + +This architectural difference exists due to upstream AWS SDK injecting attributes into spans differently for v1.11 and v2.2 + +### AWS SDK v1 Instrumentation Summary +The AdotAwsSdkInstrumentationModule uses the instrumentation (specified in AdotAwsClientInstrumentation) to register the AdotAwsSdkTracingRequestHandler through `typeInstrumentations`. + +Key aspects of handler registration: +- `order` method ensures ADOT instrumentation runs after OpenTelemetry's base instrumentation. It is set to the max integer value, as precaution, in case upstream aws-sdk registers more handlers. +- `AdotAwsSdkClientInstrumentation` class adds ADOT handler to list of request handlers + +**AdotAwsSdkClientInstrumentation** + +AWS SDK v1.11 instrumentation requires ByteBuddy because, unlike v2.2, it doesn't provide an SPI for adding request handlers. While v2.2 uses the ExecutionInterceptor interface and Java's ServiceLoader mechanism, v1.11 maintains a direct list of handlers that can't be modified through a public API. Therefore, we use ByteBuddy to modify the AWS client constructor and inject our handler directly into the requestHandler2s list. + + - `AdotAwsSdkClientAdvice` registers our handler only if the upstream aws-sdk span is enabled (i.e. it checks if the upstream handler is present when an AWS SDK client is + initialized). + - Ensures the OpenTelemetry handler is registered first. + +**AdotAwsSdkTracingRequestHandler** + +The AdotAwsSdkTracingRequestHandler hooks onto OpenTelemetry's spans during specific phases of the SDK request and response life cycle. These hooks are strategically chosen to ensure proper ordering of attribute injection. + +1. `beforeRequest`: the latest point where the SDK request can be obtained after it is modified by the upstream aws-sdk v1.11 handler +2. `afterAttempt`: the latest point to access the SDK response before the span closes in the upstream afterResponse/afterError methods + - _NOTE:_ We use afterAttempt not because it's ideal, but because it our last chance to add attributes, even though this means our logic runs multiple times during retries. + - This is a trade-off: + - We get to add our attributes before span closure + - But our code runs redundantly on each retry attempt + - We're constrained by when upstream closes the span + +All the span lifecycle hooks provided by AWS SDK RequestHandler2 can be found [here.](https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/handlers/RequestHandler2.html#beforeMarshalling-com.amazonaws.AmazonWebServiceRequest) + +_**Important Notes:**_ +- The upstream interceptor's last point of request modification occurs in [beforeRequest](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java#L58). +- The upstream interceptor closes the span in [afterResponse](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java#L116) and/or [afterError](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java#L131). These hooks are inaccessible for span modification. + `afterAttempt` is our final hook point, giving us access to both the fully processed response and active span. + +**High-Level Sequence Diagram:** +![img.png](sequence-diagram-1.11.png) + +_Class Functionalities:_ +- `AdotAwsSdkTracingRequestHandler` + - Hooks into AWS SDK request/response lifecycle + - Adds ADOT-specific attributes to spans extracted by AwsSdkExperimentalAttributesExtractor +- `AwsSdkExperimentalAttributesExtractor` + - Extracts attributes from AWS requests/responses and enriches spans + - Uses RequestAccess to get field values + - Special handling for Bedrock services +- `RequestAccess` + - Provides access to AWS SDK object fields + - Caches method handles for performance + - Uses BedrockJsonParser for parsing LLM payloads +- `BedrockJsonParser` + - Custom JSON parser for Bedrock payloads + - Handles different LLM model formats +- `AwsBedrockResourceType` + - Maps Bedrock class names to resource types + - Provides attribute keys and accessors for each type + +### AWS SDK v2 Instrumentation Summary + +**AdotAwsSdkInstrumentationModule** + +The AdotAwsSdkInstrumentationModule registers the AdotAwsSdkTracingExecutionInterceptor in `registerHelperResources`. + +Key aspects of interceptor registration: +- AWS SDK's ExecutionInterceptor loads global interceptors from files named '/software/amazon/awssdk/global/handlers/execution.interceptors' in the classpath +- Interceptors are executed in the order they appear in the classpath - earlier entries run first +- `order` method ensures ADOT instrumentation runs after OpenTelemetry's base instrumentation, maintaining proper sequence of interceptor registration in AWS SDK classpath + +**AdotAwsSdkTracingExecutionInterceptor** + +The AdotAwsSdkTracingExecutionInterceptor hooks onto OpenTelemetry's spans during specific phases of the SDK request and response life cycle. These hooks are strategically chosen to ensure proper ordering of attribute injection. + +1. `beforeTransmission`: the latest point where the SDK request can be obtained after it is modified by the upstream's interceptor +2. `modifyResponse`: the latest point to access the SDK response before the span closes in the upstream afterExecution method + +All the span lifecycle hooks provided by AWS SDK ExecutionInterceptor can be found [here.](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/core/interceptor/ExecutionInterceptor.html) + +_**Important Notes:**_ +- The upstream interceptor's last point of request modification occurs in [beforeTransmission](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/release/v2.11.x/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java#L237). +- The upstream interceptor closes the span in [afterExecution](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/release/v2.11.x/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java#L348). That hook is inaccessible for span modification. +`modifyResponse` is our final hook point, giving us access to both the fully processed response and active span. + +**High-Level Sequence Diagram:** +![img.png](sequence-diagram-2.2.png) + +_Class Functionalities:_ +- `AdotAwsSdkTracingExecutionInterceptor` + - Intercepts AWS SDK calls to create and enrich OpenTelemetry spans with AWS attributes + - Coordinates the attribute mapping process +- `FieldMapper` + - Maps the AWS SDK fields to span attributes + - Coordinates with Serializer for value conversion +- `FieldMapping` + - Defines what fields to map from SDK to spans + - Groups mappings by type (REQUEST/RESPONSE) +- `MethodHandleFacotry` + - Provides fast, cached access to AWS SDK object fields for better performance + - Used by FieldMapper for efficient field value extraction +- `Serializer` + - Converts AWS SDK objects and Bedrock objects into string values that can be used as span attributes + - Works with BedrockJsonParser for LLM responses +- `AwsJsonProtocolFactoryAccess` + - Enables access to AWS SDK's internal JSON serialization capabilities for complex SDK objects + - Uses reflection to access internal SDK classes + - Caches method handles for performance +- `BedrockJasonParser` + - Parses and extracts specific attributes from Bedrock LLM responses for GenAI telemetry + +### Commands for Running Groovy Tests + +To run the BedrockJsonParserTest for aws-sdk v1.11: +```` +./gradlew :instrumentation:aws-sdk:test --tests "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.BedrockJsonParserTest" +```` + +To run the BedrockJsonParserTest for aws-sdk v2.2: +```` +./gradlew :instrumentation:aws-sdk:test --tests "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.BedrockJsonParserTest" +```` \ No newline at end of file diff --git a/instrumentation/aws-sdk/build.gradle.kts b/instrumentation/aws-sdk/build.gradle.kts new file mode 100644 index 0000000000..5863df2a10 --- /dev/null +++ b/instrumentation/aws-sdk/build.gradle.kts @@ -0,0 +1,43 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +plugins { + java + id("com.gradleup.shadow") + id("groovy") +} + +base.archivesBaseName = "aws-instrumentation-aws-sdk" + +dependencies { + compileOnly("com.google.code.findbugs:jsr305:3.0.2") + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") + compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") + compileOnly("com.amazonaws:aws-java-sdk-core:1.11.0") + compileOnly("software.amazon.awssdk:aws-core:2.2.0") + compileOnly("software.amazon.awssdk:aws-json-protocol:2.2.0") + + compileOnly("net.bytebuddy:byte-buddy") + compileOnly("com.google.code.findbugs:jsr305:3.0.2") + + testImplementation("com.google.guava:guava") + testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common") + + testImplementation("com.amazonaws:aws-java-sdk-core:1.11.0") + testImplementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") + testImplementation("org.mockito:mockito-core:5.14.2") + testImplementation("com.google.guava:guava") + testImplementation("io.opentelemetry.javaagent:opentelemetry-testing-common") +} diff --git a/instrumentation/aws-sdk/sequence-diagram-1.11.png b/instrumentation/aws-sdk/sequence-diagram-1.11.png new file mode 100644 index 0000000000..891b07f384 Binary files /dev/null and b/instrumentation/aws-sdk/sequence-diagram-1.11.png differ diff --git a/instrumentation/aws-sdk/sequence-diagram-2.2.png b/instrumentation/aws-sdk/sequence-diagram-2.2.png new file mode 100644 index 0000000000..afa64753ef Binary files /dev/null and b/instrumentation/aws-sdk/sequence-diagram-2.2.png differ diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkClientInstrumentation.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkClientInstrumentation.java new file mode 100644 index 0000000000..7cbbb44eb2 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkClientInstrumentation.java @@ -0,0 +1,104 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +import static net.bytebuddy.matcher.ElementMatchers.*; + +import com.amazonaws.handlers.RequestHandler2; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.List; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * This class provides instrumentation by injecting our request handler into the AWS client's + * handler chain. Key components: + * + *

1. Type Matching: Targets AmazonWebServiceClient (base class for all AWS SDK v1.11 clients). + * Ensures handler injection during client initialization. + * + *

2. Transformation: Uses ByteBuddy to modify the client constructor. Injects our handler + * registration code. + * + *

3. Handler Registration (via Advice): Checks for existing OpenTelemetry handler and adds ADOT + * handler only if: a) OpenTelemetry handler is present (ensuring base instrumentation) b) ADOT + * handler isn't already added (preventing duplicates) + * + *

Based on OpenTelemetry Java Instrumentation's AWS SDK v1.11 AwsClientInstrumentation + * (release/v2.11.x). Adapts the base instrumentation pattern to add ADOT-specific functionality. + * + *

Source: ... + */ +public class AdotAwsSdkClientInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // AmazonWebServiceClient is the base interface for all AWS SDK clients. + // Type matching against it ensures our interceptor is injected as soon as any AWS SDK client is + // initialized. + return named("com.amazonaws.AmazonWebServiceClient") + .and(declaresField(named("requestHandler2s"))); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + isConstructor(), + AdotAwsSdkClientInstrumentation.class.getName() + "$AdotAwsSdkClientAdvice"); + } + + /** + * Upstream handler registration: @see ... + */ + @SuppressWarnings("unused") + public static class AdotAwsSdkClientAdvice { + + @Advice.OnMethodExit(suppress = Throwable.class) + public static void addHandler( + @Advice.FieldValue(value = "requestHandler2s") List handlers) { + + if (handlers == null) { + return; + } + + boolean hasOtelHandler = false; + boolean hasAdotHandler = false; + + // Checks if aws-sdk spans are enabled + for (RequestHandler2 handler : handlers) { + if (handler + .toString() + .contains( + "io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.TracingRequestHandler")) { + hasOtelHandler = true; + } + if (handler instanceof AdotAwsSdkTracingRequestHandler) { + hasAdotHandler = true; + break; + } + } + + // Only adds our handler if aws-sdk spans are enabled. This also ensures upstream + // instrumentation is applied first. + if (hasOtelHandler && !hasAdotHandler) { + handlers.add(new AdotAwsSdkTracingRequestHandler()); + } + } + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkInstrumentationModule.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkInstrumentationModule.java new file mode 100644 index 0000000000..628997dfa4 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkInstrumentationModule.java @@ -0,0 +1,72 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.*; + +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Based on OpenTelemetry Java Instrumentation's AWS SDK v1.11 AbstractAwsSdkInstrumentationModule + * (release/v2.11.x). Adapts the base instrumentation pattern to add ADOT-specific functionality. + * + *

Source: ... + */ +public class AdotAwsSdkInstrumentationModule extends InstrumentationModule { + + public AdotAwsSdkInstrumentationModule() { + super("aws-sdk-adot", "aws-sdk-1.11-adot"); + } + + @Override + public int order() { + // Ensure this runs after OTel (> 0) + return Integer.MAX_VALUE; + } + + @Override + public List getAdditionalHelperClassNames() { + return Arrays.asList( + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AdotAwsSdkTracingRequestHandler", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsSdkExperimentalAttributesExtractor", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsBedrockResourceType", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsBedrockResourceType$AwsBedrockResourceTypeMap", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.BedrockJsonParser", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.BedrockJsonParser$JsonParser", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.BedrockJsonParser$LlmJson", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.BedrockJsonParser$JsonPathResolver", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.RequestAccess", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.RequestAccess$1"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("com.amazonaws.AmazonWebServiceClient"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new AdotAwsSdkClientInstrumentation()); + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkTracingRequestHandler.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkTracingRequestHandler.java new file mode 100644 index 0000000000..edd0d14564 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkTracingRequestHandler.java @@ -0,0 +1,118 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +import static io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil.getBoolean; + +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.handlers.HandlerAfterAttemptContext; +import com.amazonaws.handlers.RequestHandler2; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; + +/** + * This handler extends the AWS SDK v1.11 request handling chain to add ADOT-specific span + * attributes. It operates at two key points in the request lifecycle: + * + *

1. Request Phase (beforeRequest): + * + *

    + *
  • Intercepts the request after upstream modifications + *
  • Extracts experimental attributes from the request in a separate AttributesBuilder + *
  • Adds these attributes to the current span + *
+ * + *

2. Response/Error Phase (afterAttempt): + * + *

    + *
  • Captures final state after all upstream handlers + *
  • Extracts attributes from both request and response/error in a separate AttributesBuilder + *
  • Adds these attributes to the current span before it closes + *
+ * + * Based on OpenTelemetry Java Instrumentation's AWS SDK v1.11 TracingRequestHandler + * (release/v2.11.x). Adapts the base instrumentation pattern to add ADOT-specific functionality. + * + *

Source: ... + */ +public class AdotAwsSdkTracingRequestHandler extends RequestHandler2 { + private final AwsSdkExperimentalAttributesExtractor experimentalAttributesExtractor; + private final boolean captureExperimentalSpanAttributes = + getBoolean("otel.instrumentation.aws-sdk.experimental-span-attributes", true); + + public AdotAwsSdkTracingRequestHandler() { + this.experimentalAttributesExtractor = new AwsSdkExperimentalAttributesExtractor(); + } + + /** + * This is the latest point we can obtain the Sdk Request after it is modified by the upstream + * TracingInterceptor. It ensures upstream handles the request and applies its changes first. + * + *

Upstream's last Sdk Request modification: @see reference + */ + @Override + public void beforeRequest(Request request) { + Span currentSpan = Span.current(); + + if (captureExperimentalSpanAttributes + && currentSpan != null + && currentSpan.getSpanContext().isValid()) { + AttributesBuilder attributes = Attributes.builder(); + experimentalAttributesExtractor.onStart(attributes, Context.current(), request); + + attributes + .build() + .forEach((key, value) -> currentSpan.setAttribute(key.getKey(), value.toString())); + } + } + + /** + * This is the latest point to access the sdk response before the span closes in the upstream + * afterResponse/afterError methods. This ensures we capture attributes from the final, fully + * modified response after all upstream interceptors have processed it. + * + *

Upstream's last Sdk Response modification before span closure: @see reference + * + * @see reference + */ + @Override + public void afterAttempt(HandlerAfterAttemptContext context) { + Span currentSpan = Span.current(); + + if (captureExperimentalSpanAttributes + && currentSpan != null + && currentSpan.getSpanContext().isValid()) { + Request request = context.getRequest(); + Response response = context.getResponse(); + Exception exception = context.getException(); + + AttributesBuilder attributes = Attributes.builder(); + experimentalAttributesExtractor.onEnd( + attributes, Context.current(), request, response, exception); + + attributes + .build() + .forEach((key, value) -> currentSpan.setAttribute(key.getKey(), value.toString())); + } + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsBedrockResourceType.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsBedrockResourceType.java new file mode 100644 index 0000000000..d006bc365d --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsBedrockResourceType.java @@ -0,0 +1,143 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_AGENT_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_DATA_SOURCE_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_KNOWLEDGE_BASE_ID; + +import io.opentelemetry.api.common.AttributeKey; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +enum AwsBedrockResourceType { + AGENT_TYPE(AWS_AGENT_ID, RequestAccess::getAgentId), + DATA_SOURCE_TYPE(AWS_DATA_SOURCE_ID, RequestAccess::getDataSourceId), + KNOWLEDGE_BASE_TYPE(AWS_KNOWLEDGE_BASE_ID, RequestAccess::getKnowledgeBaseId); + + @SuppressWarnings("ImmutableEnumChecker") + private final AttributeKey keyAttribute; + + @SuppressWarnings("ImmutableEnumChecker") + private final Function attributeValueAccessor; + + AwsBedrockResourceType( + AttributeKey keyAttribute, Function attributeValueAccessor) { + this.keyAttribute = keyAttribute; + this.attributeValueAccessor = attributeValueAccessor; + } + + public AttributeKey getKeyAttribute() { + return keyAttribute; + } + + public Function getAttributeValueAccessor() { + return attributeValueAccessor; + } + + public static AwsBedrockResourceType getRequestType(String requestClass) { + return AwsBedrockResourceTypeMap.BEDROCK_REQUEST_MAP.get(requestClass); + } + + public static AwsBedrockResourceType getResponseType(String responseClass) { + return AwsBedrockResourceTypeMap.BEDROCK_RESPONSE_MAP.get(responseClass); + } + + private static class AwsBedrockResourceTypeMap { + private static final Map BEDROCK_REQUEST_MAP = new HashMap<>(); + private static final Map BEDROCK_RESPONSE_MAP = new HashMap<>(); + + // Bedrock request/response mapping + // We only support operations that are related to the resource and where the context contains + // the AgentID/DataSourceID/KnowledgeBaseID. + // AgentID + private static final List agentRequestClasses = + Arrays.asList( + "CreateAgentActionGroupRequest", + "CreateAgentAliasRequest", + "DeleteAgentActionGroupRequest", + "DeleteAgentAliasRequest", + "DeleteAgentRequest", + "DeleteAgentVersionRequest", + "GetAgentActionGroupRequest", + "GetAgentAliasRequest", + "GetAgentRequest", + "GetAgentVersionRequest", + "ListAgentActionGroupsRequest", + "ListAgentAliasesRequest", + "ListAgentKnowledgeBasesRequest", + "ListAgentVersionsRequest", + "PrepareAgentRequest", + "UpdateAgentActionGroupRequest", + "UpdateAgentAliasRequest", + "UpdateAgentRequest"); + private static final List agentResponseClasses = + Arrays.asList( + "DeleteAgentAliasResult", + "DeleteAgentResult", + "DeleteAgentVersionResult", + "PrepareAgentResult"); + // DataSourceID + private static final List dataSourceRequestClasses = + Arrays.asList("DeleteDataSourceRequest", "GetDataSourceRequest", "UpdateDataSourceRequest"); + private static final List dataSourceResponseClasses = + Arrays.asList("DeleteDataSourceResult"); + // KnowledgeBaseID + private static final List knowledgeBaseRequestClasses = + Arrays.asList( + "AssociateAgentKnowledgeBaseRequest", + "CreateDataSourceRequest", + "DeleteKnowledgeBaseRequest", + "DisassociateAgentKnowledgeBaseRequest", + "GetAgentKnowledgeBaseRequest", + "GetKnowledgeBaseRequest", + "ListDataSourcesRequest", + "UpdateAgentKnowledgeBaseRequest"); + private static final List knowledgeBaseResponseClasses = + Arrays.asList("DeleteKnowledgeBaseResult"); + + private AwsBedrockResourceTypeMap() {} + + static { + // Populate the BEDROCK_REQUEST_MAP + for (String agentRequestClass : agentRequestClasses) { + BEDROCK_REQUEST_MAP.put(agentRequestClass, AwsBedrockResourceType.AGENT_TYPE); + } + for (String dataSourceRequestClass : dataSourceRequestClasses) { + BEDROCK_REQUEST_MAP.put(dataSourceRequestClass, AwsBedrockResourceType.DATA_SOURCE_TYPE); + } + for (String knowledgeBaseRequestClass : knowledgeBaseRequestClasses) { + BEDROCK_REQUEST_MAP.put( + knowledgeBaseRequestClass, AwsBedrockResourceType.KNOWLEDGE_BASE_TYPE); + } + + // Populate the BEDROCK_RESPONSE_MAP + for (String agentResponseClass : agentResponseClasses) { + BEDROCK_REQUEST_MAP.put(agentResponseClass, AwsBedrockResourceType.AGENT_TYPE); + } + for (String dataSourceResponseClass : dataSourceResponseClasses) { + BEDROCK_REQUEST_MAP.put(dataSourceResponseClass, AwsBedrockResourceType.DATA_SOURCE_TYPE); + } + for (String knowledgeBaseResponseClass : knowledgeBaseResponseClasses) { + BEDROCK_REQUEST_MAP.put( + knowledgeBaseResponseClass, AwsBedrockResourceType.KNOWLEDGE_BASE_TYPE); + } + } + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsExperimentalAttributes.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsExperimentalAttributes.java new file mode 100644 index 0000000000..f1870caa1c --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsExperimentalAttributes.java @@ -0,0 +1,70 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + * + * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; + +final class AwsExperimentalAttributes { + + // 2025-07-22: Amazon addition + static final AttributeKey AWS_STREAM_ARN = stringKey("aws.stream.arn"); + static final AttributeKey AWS_TABLE_ARN = stringKey("aws.table.arn"); + static final AttributeKey AWS_AGENT_ID = stringKey("aws.bedrock.agent.id"); + static final AttributeKey AWS_KNOWLEDGE_BASE_ID = + stringKey("aws.bedrock.knowledge_base.id"); + static final AttributeKey AWS_DATA_SOURCE_ID = stringKey("aws.bedrock.data_source.id"); + static final AttributeKey AWS_GUARDRAIL_ID = stringKey("aws.bedrock.guardrail.id"); + static final AttributeKey AWS_GUARDRAIL_ARN = stringKey("aws.bedrock.guardrail.arn"); + // TODO: Merge in gen_ai attributes in opentelemetry-semconv-incubating once upgrade to v1.26.0 + static final AttributeKey AWS_BEDROCK_RUNTIME_MODEL_ID = + stringKey("gen_ai.request.model"); + static final AttributeKey AWS_BEDROCK_SYSTEM = stringKey("gen_ai.system"); + static final AttributeKey GEN_AI_REQUEST_MAX_TOKENS = + stringKey("gen_ai.request.max_tokens"); + static final AttributeKey GEN_AI_REQUEST_TEMPERATURE = + stringKey("gen_ai.request.temperature"); + static final AttributeKey GEN_AI_REQUEST_TOP_P = stringKey("gen_ai.request.top_p"); + static final AttributeKey GEN_AI_RESPONSE_FINISH_REASONS = + stringKey("gen_ai.response.finish_reasons"); + static final AttributeKey GEN_AI_USAGE_INPUT_TOKENS = + stringKey("gen_ai.usage.input_tokens"); + static final AttributeKey GEN_AI_USAGE_OUTPUT_TOKENS = + stringKey("gen_ai.usage.output_tokens"); + static final AttributeKey AWS_STATE_MACHINE_ARN = + stringKey("aws.stepfunctions.state_machine.arn"); + static final AttributeKey AWS_STEP_FUNCTIONS_ACTIVITY_ARN = + stringKey("aws.stepfunctions.activity.arn"); + static final AttributeKey AWS_SNS_TOPIC_ARN = stringKey("aws.sns.topic.arn"); + static final AttributeKey AWS_SECRET_ARN = stringKey("aws.secretsmanager.secret.arn"); + static final AttributeKey AWS_LAMBDA_NAME = stringKey("aws.lambda.function.name"); + static final AttributeKey AWS_LAMBDA_ARN = stringKey("aws.lambda.function.arn"); + static final AttributeKey AWS_LAMBDA_RESOURCE_ID = + stringKey("aws.lambda.resource_mapping.id"); + static final AttributeKey AWS_AUTH_ACCESS_KEY = stringKey("aws.auth.account.access_key"); + + // End of Amazon addition + + private AwsExperimentalAttributes() {} +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsSdkExperimentalAttributesExtractor.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsSdkExperimentalAttributesExtractor.java new file mode 100644 index 0000000000..5aa3d39d78 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsSdkExperimentalAttributesExtractor.java @@ -0,0 +1,243 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + * + * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_AGENT_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_AUTH_ACCESS_KEY; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_BEDROCK_RUNTIME_MODEL_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_BEDROCK_SYSTEM; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_GUARDRAIL_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_GUARDRAIL_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_KNOWLEDGE_BASE_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_LAMBDA_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_LAMBDA_NAME; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_LAMBDA_RESOURCE_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_SECRET_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_SNS_TOPIC_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_STATE_MACHINE_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_STREAM_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.AWS_TABLE_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_MAX_TOKENS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_TEMPERATURE; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_TOP_P; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.GEN_AI_RESPONSE_FINISH_REASONS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.GEN_AI_USAGE_INPUT_TOKENS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsExperimentalAttributes.GEN_AI_USAGE_OUTPUT_TOKENS; + +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.handlers.HandlerContextKey; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor; +import java.util.Objects; +import java.util.function.Function; +import javax.annotation.Nullable; + +class AwsSdkExperimentalAttributesExtractor + implements AttributesExtractor, Response> { + // 2025-07-22: Amazon addition + private static final String BEDROCK_SERVICE = "AmazonBedrock"; + private static final String BEDROCK_AGENT_SERVICE = "AWSBedrockAgent"; + private static final String BEDROCK_AGENT_RUNTIME_SERVICE = "AWSBedrockAgentRuntime"; + private static final String BEDROCK_RUNTIME_SERVICE = "AmazonBedrockRuntime"; + private static final HandlerContextKey AWS_CREDENTIALS = + new HandlerContextKey("AWSCredentials"); + + AwsSdkExperimentalAttributesExtractor() {} + + @Override + public void onStart(AttributesBuilder attributes, Context parentContext, Request request) { + + Object originalRequest = request.getOriginalRequest(); + String requestClassName = originalRequest.getClass().getSimpleName(); + + AWSCredentials credentials = request.getHandlerContext(AWS_CREDENTIALS); + if (credentials != null) { + String accessKeyId = credentials.getAWSAccessKeyId(); + if (accessKeyId != null) { + attributes.put(AWS_AUTH_ACCESS_KEY, accessKeyId); + } + } + + setAttribute(attributes, AWS_STREAM_ARN, originalRequest, RequestAccess::getStreamArn); + setAttribute( + attributes, AWS_STATE_MACHINE_ARN, originalRequest, RequestAccess::getStateMachineArn); + setAttribute( + attributes, + AWS_STEP_FUNCTIONS_ACTIVITY_ARN, + originalRequest, + RequestAccess::getStepFunctionsActivityArn); + setAttribute(attributes, AWS_SNS_TOPIC_ARN, originalRequest, RequestAccess::getSnsTopicArn); + setAttribute(attributes, AWS_SECRET_ARN, originalRequest, RequestAccess::getSecretArn); + setAttribute(attributes, AWS_LAMBDA_NAME, originalRequest, RequestAccess::getLambdaName); + setAttribute( + attributes, AWS_LAMBDA_RESOURCE_ID, originalRequest, RequestAccess::getLambdaResourceId); + // Get serviceName defined in the AWS Java SDK V1 Request class. + String serviceName = request.getServiceName(); + // Extract request attributes only for Bedrock services. + if (isBedrockService(serviceName)) { + bedrockOnStart(attributes, originalRequest, requestClassName, serviceName); + } + } + + @Override + public void onEnd( + AttributesBuilder attributes, + Context context, + Request request, + @Nullable Response response, + @Nullable Throwable error) { + if (response != null) { + Object awsResp = response.getAwsResponse(); + setAttribute(attributes, AWS_TABLE_ARN, awsResp, RequestAccess::getTableArn); + setAttribute(attributes, AWS_LAMBDA_ARN, awsResp, RequestAccess::getLambdaArn); + setAttribute(attributes, AWS_STATE_MACHINE_ARN, awsResp, RequestAccess::getStateMachineArn); + setAttribute( + attributes, + AWS_STEP_FUNCTIONS_ACTIVITY_ARN, + awsResp, + RequestAccess::getStepFunctionsActivityArn); + setAttribute(attributes, AWS_SNS_TOPIC_ARN, awsResp, RequestAccess::getSnsTopicArn); + setAttribute(attributes, AWS_SECRET_ARN, awsResp, RequestAccess::getSecretArn); + // Get serviceName defined in the AWS Java SDK V1 Request class. + String serviceName = request.getServiceName(); + // Extract response attributes for Bedrock services + if (awsResp != null && isBedrockService(serviceName)) { + bedrockOnEnd(attributes, awsResp, serviceName); + } + } + } + + private static void bedrockOnStart( + AttributesBuilder attributes, + Object originalRequest, + String requestClassName, + String serviceName) { + switch (serviceName) { + case BEDROCK_SERVICE: + setAttribute(attributes, AWS_GUARDRAIL_ID, originalRequest, RequestAccess::getGuardrailId); + break; + case BEDROCK_AGENT_SERVICE: + AwsBedrockResourceType resourceType = + AwsBedrockResourceType.getRequestType(requestClassName); + if (resourceType != null) { + setAttribute( + attributes, + resourceType.getKeyAttribute(), + originalRequest, + resourceType.getAttributeValueAccessor()); + } + break; + case BEDROCK_AGENT_RUNTIME_SERVICE: + setAttribute(attributes, AWS_AGENT_ID, originalRequest, RequestAccess::getAgentId); + setAttribute( + attributes, AWS_KNOWLEDGE_BASE_ID, originalRequest, RequestAccess::getKnowledgeBaseId); + break; + case BEDROCK_RUNTIME_SERVICE: + if (!Objects.equals(requestClassName, "InvokeModelRequest")) { + break; + } + attributes.put(AWS_BEDROCK_SYSTEM, "aws.bedrock"); + Function getter = RequestAccess::getModelId; + String modelId = getter.apply(originalRequest); + attributes.put(AWS_BEDROCK_RUNTIME_MODEL_ID, modelId); + + setAttribute( + attributes, GEN_AI_REQUEST_MAX_TOKENS, originalRequest, RequestAccess::getMaxTokens); + setAttribute( + attributes, GEN_AI_REQUEST_TEMPERATURE, originalRequest, RequestAccess::getTemperature); + setAttribute(attributes, GEN_AI_REQUEST_TOP_P, originalRequest, RequestAccess::getTopP); + setAttribute( + attributes, GEN_AI_USAGE_INPUT_TOKENS, originalRequest, RequestAccess::getInputTokens); + break; + default: + break; + } + } + + private static void bedrockOnEnd( + AttributesBuilder attributes, Object awsResp, String serviceName) { + switch (serviceName) { + case BEDROCK_SERVICE: + setAttribute(attributes, AWS_GUARDRAIL_ID, awsResp, RequestAccess::getGuardrailId); + setAttribute(attributes, AWS_GUARDRAIL_ARN, awsResp, RequestAccess::getGuardrailArn); + break; + case BEDROCK_AGENT_SERVICE: + String responseClassName = awsResp.getClass().getSimpleName(); + AwsBedrockResourceType resourceType = + AwsBedrockResourceType.getResponseType(responseClassName); + if (resourceType != null) { + setAttribute( + attributes, + resourceType.getKeyAttribute(), + awsResp, + resourceType.getAttributeValueAccessor()); + } + break; + case BEDROCK_AGENT_RUNTIME_SERVICE: + setAttribute(attributes, AWS_AGENT_ID, awsResp, RequestAccess::getAgentId); + setAttribute(attributes, AWS_KNOWLEDGE_BASE_ID, awsResp, RequestAccess::getKnowledgeBaseId); + break; + case BEDROCK_RUNTIME_SERVICE: + if (!Objects.equals(awsResp.getClass().getSimpleName(), "InvokeModelResult")) { + break; + } + + setAttribute(attributes, GEN_AI_USAGE_INPUT_TOKENS, awsResp, RequestAccess::getInputTokens); + setAttribute( + attributes, GEN_AI_USAGE_OUTPUT_TOKENS, awsResp, RequestAccess::getOutputTokens); + setAttribute( + attributes, GEN_AI_RESPONSE_FINISH_REASONS, awsResp, RequestAccess::getFinishReasons); + break; + default: + break; + } + } + + private static boolean isBedrockService(String serviceName) { + // Check if the serviceName belongs to Bedrock Services defined in AWS Java SDK V1. + // For example AmazonBedrock + return serviceName.equals(BEDROCK_SERVICE) + || serviceName.equals(BEDROCK_AGENT_SERVICE) + || serviceName.equals(BEDROCK_AGENT_RUNTIME_SERVICE) + || serviceName.equals(BEDROCK_RUNTIME_SERVICE); + } + + // End of Amazon addition + + private static void setAttribute( + AttributesBuilder attributes, + AttributeKey key, + Object request, + Function getter) { + String value = getter.apply(request); + if (value != null) { + attributes.put(key, value); + } + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/BedrockJsonParser.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/BedrockJsonParser.java new file mode 100644 index 0000000000..60297e4948 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/BedrockJsonParser.java @@ -0,0 +1,277 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BedrockJsonParser { + + // Prevent instantiation + private BedrockJsonParser() { + throw new UnsupportedOperationException("Utility class"); + } + + public static LlmJson parse(String jsonString) { + JsonParser parser = new JsonParser(jsonString); + Map jsonBody = parser.parse(); + return new LlmJson(jsonBody); + } + + static class JsonParser { + private final String json; + private int position; + + public JsonParser(String json) { + this.json = json.trim(); + this.position = 0; + } + + private void skipWhitespace() { + while (position < json.length() && Character.isWhitespace(json.charAt(position))) { + position++; + } + } + + private char currentChar() { + return json.charAt(position); + } + + private static boolean isHexDigit(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + private void expect(char c) { + skipWhitespace(); + if (currentChar() != c) { + throw new IllegalArgumentException( + "Expected '" + c + "' but found '" + currentChar() + "'"); + } + position++; + } + + private String readString() { + skipWhitespace(); + expect('"'); // Ensure the string starts with a quote + StringBuilder result = new StringBuilder(); + while (currentChar() != '"') { + // Handle escape sequences + if (currentChar() == '\\') { + position++; // Move past the backslash + if (position >= json.length()) { + throw new IllegalArgumentException("Unexpected end of input in string escape sequence"); + } + char escapeChar = currentChar(); + switch (escapeChar) { + case '"': + case '\\': + case '/': + result.append(escapeChar); + break; + case 'b': + result.append('\b'); + break; + case 'f': + result.append('\f'); + break; + case 'n': + result.append('\n'); + break; + case 'r': + result.append('\r'); + break; + case 't': + result.append('\t'); + break; + case 'u': // Unicode escape sequence + if (position + 4 >= json.length()) { + throw new IllegalArgumentException("Invalid unicode escape sequence in string"); + } + char[] hexChars = new char[4]; + for (int i = 0; i < 4; i++) { + position++; // Move to the next character + char hexChar = json.charAt(position); + if (!isHexDigit(hexChar)) { + throw new IllegalArgumentException( + "Invalid hexadecimal digit in unicode escape sequence"); + } + hexChars[i] = hexChar; + } + int unicodeValue = Integer.parseInt(new String(hexChars), 16); + result.append((char) unicodeValue); + break; + default: + throw new IllegalArgumentException("Invalid escape character: \\" + escapeChar); + } + position++; + } else { + result.append(currentChar()); + position++; + } + } + position++; // Skip closing quote + return result.toString(); + } + + private Object readValue() { + skipWhitespace(); + char c = currentChar(); + + if (c == '"') { + return readString(); + } else if (Character.isDigit(c)) { + return readScopedNumber(); + } else if (c == '{') { + return readObject(); // JSON Objects + } else if (c == '[') { + return readArray(); // JSON Arrays + } else if (json.startsWith("true", position)) { + position += 4; + return true; + } else if (json.startsWith("false", position)) { + position += 5; + return false; + } else if (json.startsWith("null", position)) { + position += 4; + return null; // JSON null + } else { + throw new IllegalArgumentException("Unexpected character: " + c); + } + } + + private Number readScopedNumber() { + int start = position; + + // Consume digits and the optional decimal point + while (position < json.length() + && (Character.isDigit(json.charAt(position)) || json.charAt(position) == '.')) { + position++; + } + + String number = json.substring(start, position); + + if (number.contains(".")) { + double value = Double.parseDouble(number); + if (value < 0.0 || value > 1.0) { + throw new IllegalArgumentException( + "Value out of bounds for Bedrock Floating Point Attribute: " + number); + } + return value; + } else { + return Integer.parseInt(number); + } + } + + private Map readObject() { + Map map = new HashMap<>(); + expect('{'); + skipWhitespace(); + while (currentChar() != '}') { + String key = readString(); + expect(':'); + Object value = readValue(); + map.put(key, value); + skipWhitespace(); + if (currentChar() == ',') { + position++; + } + } + position++; // Skip closing brace + return map; + } + + private List readArray() { + List list = new ArrayList<>(); + expect('['); + skipWhitespace(); + while (currentChar() != ']') { + list.add(readValue()); + skipWhitespace(); + if (currentChar() == ',') { + position++; + } + } + position++; + return list; + } + + public Map parse() { + return readObject(); + } + } + + // Resolves paths in a JSON structure + static class JsonPathResolver { + + // Private constructor to prevent instantiation + private JsonPathResolver() { + throw new UnsupportedOperationException("Utility class"); + } + + public static Object resolvePath(LlmJson llmJson, String... paths) { + for (String path : paths) { + Object value = resolvePath(llmJson.getJsonBody(), path); + if (value != null) { + return value; + } + } + return null; + } + + private static Object resolvePath(Map json, String path) { + String[] keys = path.split("/"); + Object current = json; + + for (String key : keys) { + if (key.isEmpty()) { + continue; + } + + if (current instanceof Map) { + current = ((Map) current).get(key); + } else if (current instanceof List) { + try { + int index = Integer.parseInt(key); + current = ((List) current).get(index); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + return null; + } + } else { + return null; + } + + if (current == null) { + return null; + } + } + return current; + } + } + + public static class LlmJson { + private final Map jsonBody; + + public LlmJson(Map jsonBody) { + this.jsonBody = jsonBody; + } + + public Map getJsonBody() { + return jsonBody; + } + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/RequestAccess.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/RequestAccess.java new file mode 100644 index 0000000000..7232c7d3c8 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/RequestAccess.java @@ -0,0 +1,508 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + * + * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; +import javax.annotation.Nullable; + +final class RequestAccess { + + private static final ClassValue REQUEST_ACCESSORS = + new ClassValue() { + @Override + protected RequestAccess computeValue(Class type) { + return new RequestAccess(type); + } + }; + + // 2025-07-22: Amazon addition + @Nullable + private static BedrockJsonParser.LlmJson parseTargetBody(ByteBuffer buffer) { + try { + byte[] bytes; + // Create duplicate to avoid mutating the original buffer position + ByteBuffer duplicate = buffer.duplicate(); + if (buffer.hasArray()) { + bytes = + Arrays.copyOfRange( + duplicate.array(), + duplicate.arrayOffset(), + duplicate.arrayOffset() + duplicate.remaining()); + } else { + bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + } + String jsonString = new String(bytes, StandardCharsets.UTF_8); // Convert to String + return BedrockJsonParser.parse(jsonString); + } catch (RuntimeException e) { + return null; + } + } + + @Nullable + private static BedrockJsonParser.LlmJson getJsonBody(Object target) { + if (target == null) { + return null; + } + + RequestAccess access = REQUEST_ACCESSORS.get(target.getClass()); + ByteBuffer bodyBuffer = invokeOrNullGeneric(access.getBody, target, ByteBuffer.class); + if (bodyBuffer == null) { + return null; + } + + return parseTargetBody(bodyBuffer); + } + + @Nullable + private static String findFirstMatchingPath(BedrockJsonParser.LlmJson jsonBody, String... paths) { + if (jsonBody == null) { + return null; + } + + return Stream.of(paths) + .map(path -> BedrockJsonParser.JsonPathResolver.resolvePath(jsonBody, path)) + .filter(Objects::nonNull) + .map(Object::toString) + .findFirst() + .orElse(null); + } + + @Nullable + private static String approximateTokenCount( + BedrockJsonParser.LlmJson jsonBody, String... textPaths) { + if (jsonBody == null) { + return null; + } + + return Stream.of(textPaths) + .map(path -> BedrockJsonParser.JsonPathResolver.resolvePath(jsonBody, path)) + .filter(value -> value instanceof String) + .map(value -> Integer.toString((int) Math.ceil(((String) value).length() / 6.0))) + .findFirst() + .orElse(null); + } + + // Model -> Path Mapping: + // Amazon Nova -> "/inferenceConfig/max_new_tokens" + // Amazon Titan -> "/textGenerationConfig/maxTokenCount" + // Anthropic Claude -> "/max_tokens" + // Cohere Command -> "/max_tokens" + // Cohere Command R -> "/max_tokens" + // AI21 Jamba -> "/max_tokens" + // Meta Llama -> "/max_gen_len" + // Mistral AI -> "/max_tokens" + @Nullable + static String getMaxTokens(Object target) { + BedrockJsonParser.LlmJson jsonBody = getJsonBody(target); + return findFirstMatchingPath( + jsonBody, + "/max_tokens", + "/max_gen_len", + "/textGenerationConfig/maxTokenCount", + "/inferenceConfig/max_new_tokens"); + } + + // Model -> Path Mapping: + // Amazon Nova -> "/inferenceConfig/temperature" + // Amazon Titan -> "/textGenerationConfig/temperature" + // Anthropic Claude -> "/temperature" + // Cohere Command -> "/temperature" + // Cohere Command R -> "/temperature" + // AI21 Jamba -> "/temperature" + // Meta Llama -> "/temperature" + // Mistral AI -> "/temperature" + @Nullable + static String getTemperature(Object target) { + BedrockJsonParser.LlmJson jsonBody = getJsonBody(target); + return findFirstMatchingPath( + jsonBody, + "/temperature", + "/textGenerationConfig/temperature", + "inferenceConfig/temperature"); + } + + // Model -> Path Mapping: + // Amazon Nova -> "/inferenceConfig/top_p" + // Amazon Titan -> "/textGenerationConfig/topP" + // Anthropic Claude -> "/top_p" + // Cohere Command -> "/p" + // Cohere Command R -> "/p" + // AI21 Jamba -> "/top_p" + // Meta Llama -> "/top_p" + // Mistral AI -> "/top_p" + @Nullable + static String getTopP(Object target) { + BedrockJsonParser.LlmJson jsonBody = getJsonBody(target); + return findFirstMatchingPath( + jsonBody, "/top_p", "/p", "/textGenerationConfig/topP", "/inferenceConfig/top_p"); + } + + // Model -> Path Mapping: + // Amazon Nova -> "/usage/inputTokens" + // Amazon Titan -> "/inputTextTokenCount" + // Anthropic Claude -> "/usage/input_tokens" + // Cohere Command -> "/prompt" + // Cohere Command R -> "/message" + // AI21 Jamba -> "/usage/prompt_tokens" + // Meta Llama -> "/prompt_token_count" + // Mistral AI -> "/prompt" + @Nullable + static String getInputTokens(Object target) { + BedrockJsonParser.LlmJson jsonBody = getJsonBody(target); + if (jsonBody == null) { + return null; + } + + // Try direct token counts first + String directCount = + findFirstMatchingPath( + jsonBody, + "/inputTextTokenCount", + "/prompt_token_count", + "/usage/input_tokens", + "/usage/prompt_tokens", + "/usage/inputTokens"); + + if (directCount != null && !directCount.equals("null")) { + return directCount; + } + + // Fall back to token approximation + return approximateTokenCount(jsonBody, "/prompt", "/message"); + } + + // Model -> Path Mapping: + // Amazon Nova -> "/usage/outputTokens" + // Amazon Titan -> "/results/0/tokenCount" + // Anthropic Claude -> "/usage/output_tokens" + // Cohere Command -> "/generations/0/text" + // Cohere Command R -> "/text" + // AI21 Jamba -> "/usage/completion_tokens" + // Meta Llama -> "/generation_token_count" + // Mistral AI -> "/outputs/0/text" + @Nullable + static String getOutputTokens(Object target) { + BedrockJsonParser.LlmJson jsonBody = getJsonBody(target); + if (jsonBody == null) { + return null; + } + + // Try direct token counts first + String directCount = + findFirstMatchingPath( + jsonBody, + "/generation_token_count", + "/results/0/tokenCount", + "/usage/output_tokens", + "/usage/completion_tokens", + "/usage/outputTokens"); + + if (directCount != null && !directCount.equals("null")) { + return directCount; + } + + // Fall back to token approximation + return approximateTokenCount(jsonBody, "/text", "/outputs/0/text"); + } + + // Model -> Path Mapping: + // Amazon Nova -> "/stopReason" + // Amazon Titan -> "/results/0/completionReason" + // Anthropic Claude -> "/stop_reason" + // Cohere Command -> "/generations/0/finish_reason" + // Cohere Command R -> "/finish_reason" + // AI21 Jamba -> "/choices/0/finish_reason" + // Meta Llama -> "/stop_reason" + // Mistral AI -> "/outputs/0/stop_reason" + @Nullable + static String getFinishReasons(Object target) { + BedrockJsonParser.LlmJson jsonBody = getJsonBody(target); + String finishReason = + findFirstMatchingPath( + jsonBody, + "/stopReason", + "/finish_reason", + "/stop_reason", + "/results/0/completionReason", + "/generations/0/finish_reason", + "/choices/0/finish_reason", + "/outputs/0/stop_reason"); + + return finishReason != null ? "[" + finishReason + "]" : null; + } + + @Nullable + static String getLambdaName(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getLambdaName, request); + } + + @Nullable + static String getLambdaArn(Object request) { + if (request == null) { + return null; + } + return findNestedAccessorOrNull(request, "getConfiguration", "getFunctionArn"); + } + + @Nullable + static String getLambdaResourceId(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getLambdaResourceId, request); + } + + @Nullable + static String getSecretArn(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getSecretArn, request); + } + + @Nullable + static String getSnsTopicArn(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getSnsTopicArn, request); + } + + @Nullable + static String getStepFunctionsActivityArn(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getStepFunctionsActivityArn, request); + } + + @Nullable + static String getStateMachineArn(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getStateMachineArn, request); + } + + @Nullable + static String getTableArn(Object request) { + if (request == null) { + return null; + } + return findNestedAccessorOrNull(request, "getTable", "getTableArn"); + } + + @Nullable + static String getStreamArn(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getStreamArn, request); + } + + @Nullable + static String getAgentId(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getAgentId, request); + } + + @Nullable + static String getKnowledgeBaseId(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getKnowledgeBaseId, request); + } + + @Nullable + static String getDataSourceId(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getDataSourceId, request); + } + + @Nullable + static String getGuardrailId(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getGuardrailId, request); + } + + @Nullable + static String getGuardrailArn(Object request) { + if (request == null) { + return null; + } + return findNestedAccessorOrNull(request, "getGuardrailArn"); + } + + @Nullable + static String getModelId(Object request) { + if (request == null) { + return null; + } + RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); + return invokeOrNull(access.getModelId, request); + } + + // End of Amazon addition + + @Nullable + private static String invokeOrNull(@Nullable MethodHandle method, Object obj) { + if (method == null) { + return null; + } + try { + return (String) method.invoke(obj); + } catch (Throwable t) { + return null; + } + } + + // 2025-07-22: Amazon addition + @Nullable + private static T invokeOrNullGeneric( + @Nullable MethodHandle method, Object obj, Class returnType) { + if (method == null) { + return null; + } + try { + return returnType.cast(method.invoke(obj)); + } catch (Throwable e) { + return null; + } + } + + @Nullable private final MethodHandle getStreamArn; + @Nullable private final MethodHandle getAgentId; + @Nullable private final MethodHandle getKnowledgeBaseId; + @Nullable private final MethodHandle getDataSourceId; + @Nullable private final MethodHandle getGuardrailId; + @Nullable private final MethodHandle getModelId; + @Nullable private final MethodHandle getBody; + @Nullable private final MethodHandle getStateMachineArn; + @Nullable private final MethodHandle getStepFunctionsActivityArn; + @Nullable private final MethodHandle getSnsTopicArn; + @Nullable private final MethodHandle getSecretArn; + @Nullable private final MethodHandle getLambdaName; + @Nullable private final MethodHandle getLambdaResourceId; + + private RequestAccess(Class clz) { + getStreamArn = findAccessorOrNull(clz, "getStreamARN", String.class); + getAgentId = findAccessorOrNull(clz, "getAgentId", String.class); + getKnowledgeBaseId = findAccessorOrNull(clz, "getKnowledgeBaseId", String.class); + getDataSourceId = findAccessorOrNull(clz, "getDataSourceId", String.class); + getGuardrailId = findAccessorOrNull(clz, "getGuardrailId", String.class); + getModelId = findAccessorOrNull(clz, "getModelId", String.class); + getBody = findAccessorOrNull(clz, "getBody", ByteBuffer.class); + getStateMachineArn = findAccessorOrNull(clz, "getStateMachineArn", String.class); + getStepFunctionsActivityArn = findAccessorOrNull(clz, "getActivityArn", String.class); + getSnsTopicArn = findAccessorOrNull(clz, "getTopicArn", String.class); + getSecretArn = findAccessorOrNull(clz, "getARN", String.class); + getLambdaName = findAccessorOrNull(clz, "getFunctionName", String.class); + getLambdaResourceId = findAccessorOrNull(clz, "getUUID", String.class); + } + + /** + * Uses Java reflection to find a getter method on a class and create a MethodHandle for it. + * + * @param clz The class to search for the method + * @param methodName The name of the getter method (e.g., "getStreamARN") + * @param returnType The expected return type of the method + * @return A MethodHandle for the method, or null if not found + *

Example: For class PutRecordRequest with method "getStreamARN": + * findAccessorOrNull(PutRecordRequest.class, "getStreamARN", String.class) Creates a method + * handle that can invoke getStreamARN() on PutRecordRequest instances + */ + @Nullable + private static MethodHandle findAccessorOrNull( + Class clz, String methodName, Class returnType) { + try { + // Uses MethodHandles.publicLookup() to get access to public methods + // findVirtual finds an instance method with the given name and type + // methodType creates a method type with no parameters and the specified return type + return MethodHandles.publicLookup() + .findVirtual(clz, methodName, MethodType.methodType(returnType)); + } catch (Throwable t) { + // Returns null if method doesn't exist or can't be accessed + return null; + } + } + + /** + * Uses reflection to navigate through nested method calls and extract a String value. Unlike + * using method handles, this supports chained method calls where each method might return a + * different type of object. + * + * @param obj The initial object to start method calls from + * @param methodNames Variable list of method names to call in sequence + * @return The final String value, or null if any method in the chain fails or returns null + *

Example: For Lambda ARN: findNestedAccessorOrNull(request, "getConfiguration", + * "getFunctionArn") - First calls request.getConfiguration() to get a Configuration object, + * then calls configuration.getFunctionArn() to get the ARN string + */ + @Nullable + private static String findNestedAccessorOrNull(Object obj, String... methodNames) { + Object current = obj; + for (String methodName : methodNames) { + if (current == null) { + return null; + } + try { + Method method = current.getClass().getMethod(methodName); + current = method.invoke(current); + } catch (Exception e) { + return null; + } + } + return (current instanceof String) ? (String) current : null; + } + // End of Amazon addition +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkInstrumentationModule.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkInstrumentationModule.java new file mode 100644 index 0000000000..d8911abfd1 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkInstrumentationModule.java @@ -0,0 +1,103 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed; +import static net.bytebuddy.matcher.ElementMatchers.named; + +import io.opentelemetry.javaagent.extension.instrumentation.HelperResourceBuilder; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public class AdotAwsSdkInstrumentationModule extends InstrumentationModule { + + public AdotAwsSdkInstrumentationModule() { + super("aws-sdk-adot", "aws-sdk-2.2-adot"); + } + + @Override + public int order() { + // Ensure this runs after OTel (> 0) + return Integer.MAX_VALUE; + } + + @Override + public List getAdditionalHelperClassNames() { + return Arrays.asList( + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkTracingExecutionInterceptor", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequest", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.FieldMapper", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.FieldMapping", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.FieldMapping$Type", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.BedrockJsonParser", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.BedrockJsonParser$JsonPathResolver", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.BedrockJsonParser$LlmJson", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.BedrockJsonParser$JsonParser", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.MethodHandleFactory", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.MethodHandleFactory$1", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.Serializer", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsJsonProtocolFactoryAccess", + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType$AttributeKeys"); + } + + /** + * Registers resource file containing reference to our {@link + * AdotAwsSdkTracingExecutionInterceptor} with SDK's service loading mechanism. The 'order' method + * ensures this interceptor is registered after upstream. Interceptors are executed in the order + * they appear in the classpath. + * + * @see reference + */ + @Override + public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) { + helperResourceBuilder.register( + "software/amazon/awssdk/global/handlers/execution.interceptors", + "software/amazon/awssdk/global/handlers/execution.interceptors.adot"); + } + + @Override + public ElementMatcher.Junction classLoaderMatcher() { + return hasClassesNamed("software.amazon.awssdk.core.interceptor.ExecutionInterceptor"); + } + + @Override + public List typeInstrumentations() { + return Collections.singletonList(new ResourceInjectingTypeInstrumentation()); + } + + public static class ResourceInjectingTypeInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + // SdkClient is the base interface for all AWS SDK clients. Type matching against it ensures + // our interceptor is injected as soon as any AWS SDK client is initialized. + return named("software.amazon.awssdk.core.SdkClient"); + } + + @Override + public void transform(TypeTransformer transformer) { + // Empty as we use ExecutionInterceptor + } + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkTracingExecutionInterceptor.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkTracingExecutionInterceptor.java new file mode 100644 index 0000000000..8e0478ef5a --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkTracingExecutionInterceptor.java @@ -0,0 +1,152 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +import static io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil.getBoolean; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_AUTH_ACCESS_KEY; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_AUTH_REGION; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.GEN_AI_SYSTEM; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.BEDROCKRUNTIME; + +import io.opentelemetry.api.trace.Span; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.interceptor.*; +import software.amazon.awssdk.regions.Region; + +/** + * This interceptor manages the AWS SDK requests and responses for attribute mapping. Here's the + * flow: + * + *

1. Request Phase (beforeTransmission): + * + *

    + *
  • Receives the SDK request before it's sent to AWS + *
  • Creates an AwsSdkRequest object containing request metadata + *
  • Stores this AwsSdkRequest in ExecutionAttributes using ADOT_AWS_SDK_REQUEST_ATTRIBUTE + *
  • Maps request attributes to the current span + *
+ * + *

2. Response Phase (modifyResponse): + * + *

    + *
  • Retrieves the stored AwsSdkRequest from ExecutionAttributes via + * ADOT_AWS_SDK_REQUEST_ATTRIBUTE + *
  • Uses this request context to properly map response fields to the span + *
  • Cleans up by removing the stored request from ExecutionAttributes + *
+ * + *

The ExecutionAttributes object persists throughout the entire request lifecycle, allowing + * correlation between the request and response phases. All ExecutionInterceptor's have access to + * the ExecutionAttributes. + * + * @see reference + */ +public class AdotAwsSdkTracingExecutionInterceptor implements ExecutionInterceptor { + + private static final String GEN_AI_SYSTEM_BEDROCK = "aws.bedrock"; + private static final ExecutionAttribute ADOT_AWS_SDK_REQUEST_ATTRIBUTE = + new ExecutionAttribute<>( + AdotAwsSdkTracingExecutionInterceptor.class.getName() + ".AwsSdkRequest"); + + private final FieldMapper fieldMapper = new FieldMapper(); + private final boolean captureExperimentalSpanAttributes = + getBoolean("otel.instrumentation.aws-sdk.experimental-span-attributes", true); + + /** + * This method coordinates the request attribute mapping process. This is the latest point we can + * obtain the Sdk Request after it is modified by the upstream TracingInterceptor. It ensures + * upstream handles the request and applies its changes to the span first. We use this hook to + * extract the ADOT AWS attributes from the Sdk Request and map them to the span via the + * FieldMapper. + * + *

Upstream's last Sdk Request modification: @see reference + */ + @Override + public void beforeTransmission( + Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { + + if (captureExperimentalSpanAttributes) { + SdkRequest request = context.request(); + Span currentSpan = Span.current(); + + try { + if (request == null || currentSpan == null || !currentSpan.getSpanContext().isValid()) { + return; + } + + AwsCredentials credentials = + executionAttributes.getAttribute(AwsSignerExecutionAttribute.AWS_CREDENTIALS); + Region signingRegion = + executionAttributes.getAttribute(AwsSignerExecutionAttribute.SIGNING_REGION); + + if (credentials != null) { + String accessKeyId = credentials.accessKeyId(); + if (accessKeyId != null) { + currentSpan.setAttribute(AWS_AUTH_ACCESS_KEY, accessKeyId); + } + } + + if (signingRegion != null) { + String region = signingRegion.toString(); + currentSpan.setAttribute(AWS_AUTH_REGION, region); + } + + AwsSdkRequest awsSdkRequest = AwsSdkRequest.ofSdkRequest(request); + if (awsSdkRequest != null) { + executionAttributes.putAttribute(ADOT_AWS_SDK_REQUEST_ATTRIBUTE, awsSdkRequest); + fieldMapper.mapToAttributes(request, awsSdkRequest, currentSpan); + if (awsSdkRequest.type() == BEDROCKRUNTIME) { + currentSpan.setAttribute(GEN_AI_SYSTEM, GEN_AI_SYSTEM_BEDROCK); + } + } + } catch (Throwable throwable) { + // ignore + } + } + } + + /** + * This method coordinates the response attribute mapping process. This is the latest point we can + * obtain the Sdk Response before span completion in upstream's afterExecution. This ensures we + * capture attributes from the final, fully modified response after all upstream interceptors have + * processed it. We use this hook to extract the ADOT AWS attributes from the Sdk Response and map + * them to the span via the FieldMapper. + * + *

Upstream's last Sdk Response modification before span closure: @see reference + */ + @Override + public SdkResponse modifyResponse( + Context.ModifyResponse context, ExecutionAttributes executionAttributes) { + + if (captureExperimentalSpanAttributes) { + Span currentSpan = Span.current(); + AwsSdkRequest sdkRequest = executionAttributes.getAttribute(ADOT_AWS_SDK_REQUEST_ATTRIBUTE); + + if (sdkRequest != null) { + fieldMapper.mapToAttributes(context.response(), sdkRequest, currentSpan); + executionAttributes.putAttribute(ADOT_AWS_SDK_REQUEST_ATTRIBUTE, null); + } + } + + return context.response(); + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsExperimentalAttributes.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsExperimentalAttributes.java new file mode 100644 index 0000000000..7acd9a8df8 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsExperimentalAttributes.java @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; + +final class AwsExperimentalAttributes { + static final AttributeKey AWS_BUCKET_NAME = stringKey("aws.bucket.name"); + static final AttributeKey AWS_QUEUE_URL = stringKey("aws.queue.url"); + static final AttributeKey AWS_QUEUE_NAME = stringKey("aws.queue.name"); + static final AttributeKey AWS_STREAM_NAME = stringKey("aws.stream.name"); + static final AttributeKey AWS_STREAM_ARN = stringKey("aws.stream.arn"); + static final AttributeKey AWS_TABLE_NAME = stringKey("aws.table.name"); + static final AttributeKey AWS_GUARDRAIL_ID = stringKey("aws.bedrock.guardrail.id"); + static final AttributeKey AWS_GUARDRAIL_ARN = stringKey("aws.bedrock.guardrail.arn"); + static final AttributeKey AWS_AGENT_ID = stringKey("aws.bedrock.agent.id"); + static final AttributeKey AWS_DATA_SOURCE_ID = stringKey("aws.bedrock.data_source.id"); + static final AttributeKey AWS_KNOWLEDGE_BASE_ID = + stringKey("aws.bedrock.knowledge_base.id"); + + // TODO: Merge in gen_ai attributes in opentelemetry-semconv-incubating once upgrade to v1.26.0 + static final AttributeKey GEN_AI_MODEL = stringKey("gen_ai.request.model"); + static final AttributeKey GEN_AI_SYSTEM = stringKey("gen_ai.system"); + + static final AttributeKey GEN_AI_REQUEST_MAX_TOKENS = + stringKey("gen_ai.request.max_tokens"); + + static final AttributeKey GEN_AI_REQUEST_TEMPERATURE = + stringKey("gen_ai.request.temperature"); + + static final AttributeKey GEN_AI_REQUEST_TOP_P = stringKey("gen_ai.request.top_p"); + + static final AttributeKey GEN_AI_RESPONSE_FINISH_REASONS = + stringKey("gen_ai.response.finish_reasons"); + + static final AttributeKey GEN_AI_USAGE_INPUT_TOKENS = + stringKey("gen_ai.usage.input_tokens"); + + static final AttributeKey GEN_AI_USAGE_OUTPUT_TOKENS = + stringKey("gen_ai.usage.output_tokens"); + + static final AttributeKey AWS_STATE_MACHINE_ARN = + stringKey("aws.stepfunctions.state_machine.arn"); + + static final AttributeKey AWS_STEP_FUNCTIONS_ACTIVITY_ARN = + stringKey("aws.stepfunctions.activity.arn"); + + static final AttributeKey AWS_SNS_TOPIC_ARN = stringKey("aws.sns.topic.arn"); + + static final AttributeKey AWS_SECRET_ARN = stringKey("aws.secretsmanager.secret.arn"); + + static final AttributeKey AWS_LAMBDA_NAME = stringKey("aws.lambda.function.name"); + + static final AttributeKey AWS_LAMBDA_ARN = stringKey("aws.lambda.function.arn"); + + static final AttributeKey AWS_LAMBDA_RESOURCE_ID = + stringKey("aws.lambda.resource_mapping.id"); + + static final AttributeKey AWS_TABLE_ARN = stringKey("aws.table.arn"); + + static final AttributeKey AWS_AUTH_ACCESS_KEY = stringKey("aws.auth.account.access_key"); + + static final AttributeKey AWS_AUTH_REGION = stringKey("aws.auth.region"); + + static boolean isGenAiAttribute(String attributeKey) { + return attributeKey.equals(GEN_AI_REQUEST_MAX_TOKENS.getKey()) + || attributeKey.equals(GEN_AI_REQUEST_TEMPERATURE.getKey()) + || attributeKey.equals(GEN_AI_REQUEST_TOP_P.getKey()) + || attributeKey.equals(GEN_AI_RESPONSE_FINISH_REASONS.getKey()) + || attributeKey.equals(GEN_AI_USAGE_INPUT_TOKENS.getKey()) + || attributeKey.equals(GEN_AI_USAGE_OUTPUT_TOKENS.getKey()); + } + + private AwsExperimentalAttributes() {} +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsJsonProtocolFactoryAccess.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsJsonProtocolFactoryAccess.java new file mode 100644 index 0000000000..4ff1294a80 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsJsonProtocolFactoryAccess.java @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.net.URI; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.client.config.SdkClientConfiguration; +import software.amazon.awssdk.core.client.config.SdkClientOption; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.protocols.core.OperationInfo; +import software.amazon.awssdk.protocols.core.ProtocolMarshaller; + +final class AwsJsonProtocolFactoryAccess { + + private static final OperationInfo OPERATION_INFO = + OperationInfo.builder().hasPayloadMembers(true).httpMethod(SdkHttpMethod.POST).build(); + + @Nullable private static final MethodHandle INVOKE_CREATE_PROTOCOL_MARSHALLER; + + static { + MethodHandle invokeCreateProtocolMarshaller = null; + try { + Class awsJsonProtocolFactoryClass = + Class.forName("software.amazon.awssdk.protocols.json.AwsJsonProtocolFactory"); + Object awsJsonProtocolFactoryBuilder = + awsJsonProtocolFactoryClass.getMethod("builder").invoke(null); + awsJsonProtocolFactoryBuilder + .getClass() + .getMethod("clientConfiguration", SdkClientConfiguration.class) + .invoke( + awsJsonProtocolFactoryBuilder, + SdkClientConfiguration.builder() + // AwsJsonProtocolFactory requires any URI to be present + .option(SdkClientOption.ENDPOINT, URI.create("http://empty")) + .build()); + @SuppressWarnings("rawtypes") + Class awsJsonProtocolClass = + Class.forName("software.amazon.awssdk.protocols.json.AwsJsonProtocol"); + @SuppressWarnings("unchecked") + Object awsJsonProtocol = Enum.valueOf(awsJsonProtocolClass, "AWS_JSON"); + awsJsonProtocolFactoryBuilder + .getClass() + .getMethod("protocol", awsJsonProtocolClass) + .invoke(awsJsonProtocolFactoryBuilder, awsJsonProtocol); + Object awsJsonProtocolFactory = + awsJsonProtocolFactoryBuilder + .getClass() + .getMethod("build") + .invoke(awsJsonProtocolFactoryBuilder); + + MethodHandle createProtocolMarshaller = + MethodHandles.publicLookup() + .findVirtual( + awsJsonProtocolFactoryClass, + "createProtocolMarshaller", + MethodType.methodType(ProtocolMarshaller.class, OperationInfo.class)); + invokeCreateProtocolMarshaller = + createProtocolMarshaller.bindTo(awsJsonProtocolFactory).bindTo(OPERATION_INFO); + } catch (Throwable t) { + // Ignore; + } + INVOKE_CREATE_PROTOCOL_MARSHALLER = invokeCreateProtocolMarshaller; + } + + @SuppressWarnings("unchecked") + @Nullable + static ProtocolMarshaller createMarshaller() { + if (INVOKE_CREATE_PROTOCOL_MARSHALLER == null) { + return null; + } + + try { + return (ProtocolMarshaller) INVOKE_CREATE_PROTOCOL_MARSHALLER.invoke(); + } catch (Throwable t) { + return null; + } + } + + private AwsJsonProtocolFactoryAccess() {} +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkRequest.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkRequest.java new file mode 100644 index 0000000000..e09a650149 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkRequest.java @@ -0,0 +1,144 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + * + * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.BEDROCK; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.BEDROCKAGENTOPERATION; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.BEDROCKAGENTRUNTIMEOPERATION; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.BEDROCKDATASOURCEOPERATION; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.BEDROCKKNOWLEDGEBASEOPERATION; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.BEDROCKRUNTIME; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.DYNAMODB; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.KINESIS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.LAMBDA; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.S3; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.SECRETSMANAGER; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.SNS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.SQS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkRequestType.STEPFUNCTION; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.FieldMapping.request; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.SdkRequest; + +@SuppressWarnings("MemberName") +enum AwsSdkRequest { + // generic requests + DynamoDbRequest(DYNAMODB, "DynamoDbRequest"), + S3Request(S3, "S3Request"), + SnsRequest(SNS, "SnsRequest"), + SqsRequest(SQS, "SqsRequest"), + KinesisRequest(KINESIS, "KinesisRequest"), + + // 2025-07-22: Amazon addition + BedrockRequest(BEDROCK, "BedrockRequest"), + BedrockAgentRuntimeRequest(BEDROCKAGENTRUNTIMEOPERATION, "BedrockAgentRuntimeRequest"), + BedrockRuntimeRequest(BEDROCKRUNTIME, "BedrockRuntimeRequest"), + // BedrockAgent API based requests. We only support operations that are related to + // Agent/DataSources/KnowledgeBases + // resources and the request/response context contains the resource ID. + BedrockCreateAgentActionGroupRequest(BEDROCKAGENTOPERATION, "CreateAgentActionGroupRequest"), + BedrockCreateAgentAliasRequest(BEDROCKAGENTOPERATION, "CreateAgentAliasRequest"), + BedrockDeleteAgentActionGroupRequest(BEDROCKAGENTOPERATION, "DeleteAgentActionGroupRequest"), + BedrockDeleteAgentAliasRequest(BEDROCKAGENTOPERATION, "DeleteAgentAliasRequest"), + BedrockDeleteAgentVersionRequest(BEDROCKAGENTOPERATION, "DeleteAgentVersionRequest"), + BedrockGetAgentActionGroupRequest(BEDROCKAGENTOPERATION, "GetAgentActionGroupRequest"), + BedrockGetAgentAliasRequest(BEDROCKAGENTOPERATION, "GetAgentAliasRequest"), + BedrockGetAgentRequest(BEDROCKAGENTOPERATION, "GetAgentRequest"), + BedrockGetAgentVersionRequest(BEDROCKAGENTOPERATION, "GetAgentVersionRequest"), + BedrockListAgentActionGroupsRequest(BEDROCKAGENTOPERATION, "ListAgentActionGroupsRequest"), + BedrockListAgentAliasesRequest(BEDROCKAGENTOPERATION, "ListAgentAliasesRequest"), + BedrockListAgentKnowledgeBasesRequest(BEDROCKAGENTOPERATION, "ListAgentKnowledgeBasesRequest"), + BedrocListAgentVersionsRequest(BEDROCKAGENTOPERATION, "ListAgentVersionsRequest"), + BedrockPrepareAgentRequest(BEDROCKAGENTOPERATION, "PrepareAgentRequest"), + BedrockUpdateAgentActionGroupRequest(BEDROCKAGENTOPERATION, "UpdateAgentActionGroupRequest"), + BedrockUpdateAgentAliasRequest(BEDROCKAGENTOPERATION, "UpdateAgentAliasRequest"), + BedrockUpdateAgentRequest(BEDROCKAGENTOPERATION, "UpdateAgentRequest"), + BedrockBedrockAgentRequest(BEDROCKAGENTOPERATION, "BedrockAgentRequest"), + BedrockDeleteDataSourceRequest(BEDROCKDATASOURCEOPERATION, "DeleteDataSourceRequest"), + BedrockGetDataSourceRequest(BEDROCKDATASOURCEOPERATION, "GetDataSourceRequest"), + BedrockUpdateDataSourceRequest(BEDROCKDATASOURCEOPERATION, "UpdateDataSourceRequest"), + BedrocAssociateAgentKnowledgeBaseRequest( + BEDROCKKNOWLEDGEBASEOPERATION, "AssociateAgentKnowledgeBaseRequest"), + BedrockCreateDataSourceRequest(BEDROCKKNOWLEDGEBASEOPERATION, "CreateDataSourceRequest"), + BedrockDeleteKnowledgeBaseRequest(BEDROCKKNOWLEDGEBASEOPERATION, "DeleteKnowledgeBaseRequest"), + BedrockDisassociateAgentKnowledgeBaseRequest( + BEDROCKKNOWLEDGEBASEOPERATION, "DisassociateAgentKnowledgeBaseRequest"), + BedrockGetAgentKnowledgeBaseRequest( + BEDROCKKNOWLEDGEBASEOPERATION, "GetAgentKnowledgeBaseRequest"), + BedrockGetKnowledgeBaseRequest(BEDROCKKNOWLEDGEBASEOPERATION, "GetKnowledgeBaseRequest"), + BedrockListDataSourcesRequest(BEDROCKKNOWLEDGEBASEOPERATION, "ListDataSourcesRequest"), + BedrockUpdateAgentKnowledgeBaseRequest( + BEDROCKKNOWLEDGEBASEOPERATION, "UpdateAgentKnowledgeBaseRequest"), + + SfnRequest(STEPFUNCTION, "SfnRequest"), + + SecretsManagerRequest(SECRETSMANAGER, "SecretsManagerRequest"), + + LambdaRequest(LAMBDA, "LambdaRequest"); + // End of Amazon addition + + private final AwsSdkRequestType type; + private final String requestClass; + + // Wrap in unmodifiableMap + @SuppressWarnings("ImmutableEnumChecker") + private final Map> fields; + + AwsSdkRequest(AwsSdkRequestType type, String requestClass, FieldMapping... fields) { + this.type = type; + this.requestClass = requestClass; + this.fields = Collections.unmodifiableMap(FieldMapping.groupByType(fields)); + } + + @Nullable + static AwsSdkRequest ofSdkRequest(SdkRequest request) { + // try request type + AwsSdkRequest result = ofType(request.getClass().getSimpleName()); + // try parent - generic + if (result == null) { + result = ofType(request.getClass().getSuperclass().getSimpleName()); + } + return result; + } + + private static AwsSdkRequest ofType(String typeName) { + for (AwsSdkRequest type : values()) { + if (type.requestClass.equals(typeName)) { + return type; + } + } + return null; + } + + List fields(FieldMapping.Type type) { + return fields.get(type); + } + + AwsSdkRequestType type() { + return type; + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkRequestType.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkRequestType.java new file mode 100644 index 0000000000..a874efbef9 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkRequestType.java @@ -0,0 +1,139 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + * + * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_AGENT_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_BUCKET_NAME; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_DATA_SOURCE_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_GUARDRAIL_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_GUARDRAIL_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_KNOWLEDGE_BASE_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_LAMBDA_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_LAMBDA_NAME; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_LAMBDA_RESOURCE_ID; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_QUEUE_NAME; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_QUEUE_URL; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_SECRET_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_SNS_TOPIC_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_STATE_MACHINE_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_STEP_FUNCTIONS_ACTIVITY_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_STREAM_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_STREAM_NAME; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_TABLE_ARN; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.AWS_TABLE_NAME; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.GEN_AI_MODEL; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.GEN_AI_REQUEST_MAX_TOKENS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.GEN_AI_REQUEST_TEMPERATURE; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.GEN_AI_REQUEST_TOP_P; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.GEN_AI_RESPONSE_FINISH_REASONS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.GEN_AI_USAGE_INPUT_TOKENS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsExperimentalAttributes.GEN_AI_USAGE_OUTPUT_TOKENS; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.FieldMapping.request; +import static software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.FieldMapping.response; + +import io.opentelemetry.api.common.AttributeKey; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +enum AwsSdkRequestType { + // 2025-07-22: Amazon addition + S3(request(AWS_BUCKET_NAME.getKey(), "Bucket")), + + SQS(request(AWS_QUEUE_URL.getKey(), "QueueUrl"), request(AWS_QUEUE_NAME.getKey(), "QueueName")), + + KINESIS( + request(AWS_STREAM_NAME.getKey(), "StreamName"), + request(AWS_STREAM_ARN.getKey(), "StreamARN")), + + DYNAMODB( + request(AWS_TABLE_NAME.getKey(), "TableName"), + response(AWS_TABLE_ARN.getKey(), "Table.TableArn")), + + SNS( + /* + * Only one of TopicArn and TargetArn are permitted on an SNS request. + */ + request(AttributeKeys.MESSAGING_DESTINATION_NAME.getKey(), "TargetArn"), + request(AttributeKeys.MESSAGING_DESTINATION_NAME.getKey(), "TopicArn"), + request(AWS_SNS_TOPIC_ARN.getKey(), "TopicArn")), + + BEDROCK( + request(AWS_GUARDRAIL_ID.getKey(), "guardrailIdentifier"), + response(AWS_GUARDRAIL_ARN.getKey(), "guardrailArn")), + BEDROCKAGENTOPERATION( + request(AWS_AGENT_ID.getKey(), "agentId"), response(AWS_AGENT_ID.getKey(), "agentId")), + BEDROCKAGENTRUNTIMEOPERATION( + request(AWS_AGENT_ID.getKey(), "agentId"), + response(AWS_AGENT_ID.getKey(), "agentId"), + request(AWS_KNOWLEDGE_BASE_ID.getKey(), "knowledgeBaseId"), + response(AWS_KNOWLEDGE_BASE_ID.getKey(), "knowledgeBaseId")), + BEDROCKDATASOURCEOPERATION( + request(AWS_DATA_SOURCE_ID.getKey(), "dataSourceId"), + response(AWS_DATA_SOURCE_ID.getKey(), "dataSourceId")), + BEDROCKKNOWLEDGEBASEOPERATION( + request(AWS_KNOWLEDGE_BASE_ID.getKey(), "knowledgeBaseId"), + response(AWS_KNOWLEDGE_BASE_ID.getKey(), "knowledgeBaseId")), + BEDROCKRUNTIME( + request(GEN_AI_MODEL.getKey(), "modelId"), + request(GEN_AI_REQUEST_MAX_TOKENS.getKey(), "body"), + request(GEN_AI_REQUEST_TEMPERATURE.getKey(), "body"), + request(GEN_AI_REQUEST_TOP_P.getKey(), "body"), + request(GEN_AI_USAGE_INPUT_TOKENS.getKey(), "body"), + response(GEN_AI_RESPONSE_FINISH_REASONS.getKey(), "body"), + response(GEN_AI_USAGE_INPUT_TOKENS.getKey(), "body"), + response(GEN_AI_USAGE_OUTPUT_TOKENS.getKey(), "body")), + + STEPFUNCTION( + request(AWS_STATE_MACHINE_ARN.getKey(), "stateMachineArn"), + request(AWS_STEP_FUNCTIONS_ACTIVITY_ARN.getKey(), "activityArn")), + + // SNS(request(AWS_SNS_TOPIC_ARN.getKey(), "TopicArn")), + + SECRETSMANAGER(response(AWS_SECRET_ARN.getKey(), "ARN")), + + LAMBDA( + request(AWS_LAMBDA_NAME.getKey(), "FunctionName"), + request(AWS_LAMBDA_RESOURCE_ID.getKey(), "UUID"), + response(AWS_LAMBDA_ARN.getKey(), "Configuration.FunctionArn")); + + // End of Amazon addition + + // Wrapping in unmodifiableMap + @SuppressWarnings("ImmutableEnumChecker") + private final Map> fields; + + AwsSdkRequestType(FieldMapping... fieldMappings) { + this.fields = Collections.unmodifiableMap(FieldMapping.groupByType(fieldMappings)); + } + + List fields(FieldMapping.Type type) { + return fields.get(type); + } + + private static class AttributeKeys { + // copied from MessagingIncubatingAttributes + static final AttributeKey MESSAGING_DESTINATION_NAME = + AttributeKey.stringKey("messaging.destination.name"); + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/BedrockJsonParser.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/BedrockJsonParser.java new file mode 100644 index 0000000000..a958f64201 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/BedrockJsonParser.java @@ -0,0 +1,289 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public class BedrockJsonParser { + + // Prevent instantiation + private BedrockJsonParser() { + throw new UnsupportedOperationException("Utility class"); + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + public static LlmJson parse(String jsonString) { + JsonParser parser = new JsonParser(jsonString); + Map jsonBody = parser.parse(); + return new LlmJson(jsonBody); + } + + static class JsonParser { + private final String json; + private int position; + + public JsonParser(String json) { + this.json = json.trim(); + this.position = 0; + } + + private void skipWhitespace() { + while (position < json.length() && Character.isWhitespace(json.charAt(position))) { + position++; + } + } + + private char currentChar() { + return json.charAt(position); + } + + private static boolean isHexDigit(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + private void expect(char c) { + skipWhitespace(); + if (currentChar() != c) { + throw new IllegalArgumentException( + "Expected '" + c + "' but found '" + currentChar() + "'"); + } + position++; + } + + private String readString() { + skipWhitespace(); + expect('"'); // Ensure the string starts with a quote + StringBuilder result = new StringBuilder(); + while (currentChar() != '"') { + // Handle escape sequences + if (currentChar() == '\\') { + position++; // Move past the backslash + if (position >= json.length()) { + throw new IllegalArgumentException("Unexpected end of input in string escape sequence"); + } + char escapeChar = currentChar(); + switch (escapeChar) { + case '"': + case '\\': + case '/': + result.append(escapeChar); + break; + case 'b': + result.append('\b'); + break; + case 'f': + result.append('\f'); + break; + case 'n': + result.append('\n'); + break; + case 'r': + result.append('\r'); + break; + case 't': + result.append('\t'); + break; + case 'u': // Unicode escape sequence + if (position + 4 >= json.length()) { + throw new IllegalArgumentException("Invalid unicode escape sequence in string"); + } + char[] hexChars = new char[4]; + for (int i = 0; i < 4; i++) { + position++; // Move to the next character + char hexChar = json.charAt(position); + if (!isHexDigit(hexChar)) { + throw new IllegalArgumentException( + "Invalid hexadecimal digit in unicode escape sequence"); + } + hexChars[i] = hexChar; + } + int unicodeValue = Integer.parseInt(new String(hexChars), 16); + result.append((char) unicodeValue); + break; + default: + throw new IllegalArgumentException("Invalid escape character: \\" + escapeChar); + } + position++; + } else { + result.append(currentChar()); + position++; + } + } + position++; // Skip closing quote + return result.toString(); + } + + private Object readValue() { + skipWhitespace(); + char c = currentChar(); + + if (c == '"') { + return readString(); + } else if (Character.isDigit(c)) { + return readScopedNumber(); + } else if (c == '{') { + return readObject(); // JSON Objects + } else if (c == '[') { + return readArray(); // JSON Arrays + } else if (json.startsWith("true", position)) { + position += 4; + return true; + } else if (json.startsWith("false", position)) { + position += 5; + return false; + } else if (json.startsWith("null", position)) { + position += 4; + return null; // JSON null + } else { + throw new IllegalArgumentException("Unexpected character: " + c); + } + } + + private Number readScopedNumber() { + int start = position; + + // Consume digits and the optional decimal point + while (position < json.length() + && (Character.isDigit(json.charAt(position)) || json.charAt(position) == '.')) { + position++; + } + + String number = json.substring(start, position); + + if (number.contains(".")) { + double value = Double.parseDouble(number); + if (value < 0.0 || value > 1.0) { + throw new IllegalArgumentException( + "Value out of bounds for Bedrock Floating Point Attribute: " + number); + } + return value; + } else { + return Integer.parseInt(number); + } + } + + private Map readObject() { + Map map = new HashMap<>(); + expect('{'); + skipWhitespace(); + while (currentChar() != '}') { + String key = readString(); + expect(':'); + Object value = readValue(); + map.put(key, value); + skipWhitespace(); + if (currentChar() == ',') { + position++; + } + } + position++; // Skip closing brace + return map; + } + + private List readArray() { + List list = new ArrayList<>(); + expect('['); + skipWhitespace(); + while (currentChar() != ']') { + list.add(readValue()); + skipWhitespace(); + if (currentChar() == ',') { + position++; + } + } + position++; + return list; + } + + public Map parse() { + return readObject(); + } + } + + // Resolves paths in a JSON structure + static class JsonPathResolver { + + // Private constructor to prevent instantiation + private JsonPathResolver() { + throw new UnsupportedOperationException("Utility class"); + } + + public static Object resolvePath(LlmJson llmJson, String... paths) { + for (String path : paths) { + Object value = resolvePath(llmJson.getJsonBody(), path); + if (value != null) { + return value; + } + } + return null; + } + + private static Object resolvePath(Map json, String path) { + String[] keys = path.split("/"); + Object current = json; + + for (String key : keys) { + if (key.isEmpty()) { + continue; + } + + if (current instanceof Map) { + current = ((Map) current).get(key); + } else if (current instanceof List) { + try { + int index = Integer.parseInt(key); + current = ((List) current).get(index); + } catch (NumberFormatException | IndexOutOfBoundsException e) { + return null; + } + } else { + return null; + } + + if (current == null) { + return null; + } + } + return current; + } + } + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + public static class LlmJson { + private final Map jsonBody; + + public LlmJson(Map jsonBody) { + this.jsonBody = jsonBody; + } + + public Map getJsonBody() { + return jsonBody; + } + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/FieldMapper.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/FieldMapper.java new file mode 100644 index 0000000000..4d4c56cf52 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/FieldMapper.java @@ -0,0 +1,109 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + * + * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +import io.opentelemetry.api.trace.Span; +import java.util.List; +import java.util.function.Function; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.utils.StringUtils; + +class FieldMapper { + + private final Serializer serializer; + private final MethodHandleFactory methodHandleFactory; + + FieldMapper() { + serializer = new Serializer(); + methodHandleFactory = new MethodHandleFactory(); + } + + FieldMapper(Serializer serializer, MethodHandleFactory methodHandleFactory) { + this.methodHandleFactory = methodHandleFactory; + this.serializer = serializer; + } + + void mapToAttributes(SdkRequest sdkRequest, AwsSdkRequest request, Span span) { + mapToAttributes( + field -> sdkRequest.getValueForField(field, Object.class).orElse(null), + FieldMapping.Type.REQUEST, + request, + span); + } + + void mapToAttributes(SdkResponse sdkResponse, AwsSdkRequest request, Span span) { + mapToAttributes( + field -> sdkResponse.getValueForField(field, Object.class).orElse(null), + FieldMapping.Type.RESPONSE, + request, + span); + } + + private void mapToAttributes( + Function fieldValueProvider, + FieldMapping.Type type, + AwsSdkRequest request, + Span span) { + for (FieldMapping fieldMapping : request.fields(type)) { + mapToAttributes(fieldValueProvider, fieldMapping, span); + } + for (FieldMapping fieldMapping : request.type().fields(type)) { + mapToAttributes(fieldValueProvider, fieldMapping, span); + } + } + + private void mapToAttributes( + Function fieldValueProvider, FieldMapping fieldMapping, Span span) { + // traverse path + List path = fieldMapping.getFields(); + Object target = fieldValueProvider.apply(path.get(0)); + for (int i = 1; i < path.size() && target != null; i++) { + target = next(target, path.get(i)); + } + // 2025-07-22: Amazon addition + String value; + if (target != null) { + if (AwsExperimentalAttributes.isGenAiAttribute(fieldMapping.getAttribute())) { + value = serializer.serialize(fieldMapping.getAttribute(), target); + } else { + value = serializer.serialize(target); + } + // End of Amazon addition + if (!StringUtils.isEmpty(value)) { + span.setAttribute(fieldMapping.getAttribute(), value); + } + } + } + + @Nullable + private Object next(Object current, String fieldName) { + try { + return methodHandleFactory.forField(current.getClass(), fieldName).invoke(current); + } catch (Throwable t) { + // ignore + } + return null; + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/FieldMapping.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/FieldMapping.java new file mode 100644 index 0000000000..bf0750813c --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/FieldMapping.java @@ -0,0 +1,78 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +class FieldMapping { + + enum Type { + REQUEST, + RESPONSE + } + + private final Type type; + private final String attribute; + private final List fields; + + static FieldMapping request(String attribute, String fieldPath) { + return new FieldMapping(Type.REQUEST, attribute, fieldPath); + } + + static FieldMapping response(String attribute, String fieldPath) { + return new FieldMapping(Type.RESPONSE, attribute, fieldPath); + } + + FieldMapping(Type type, String attribute, String fieldPath) { + this.type = type; + this.attribute = attribute; + this.fields = Collections.unmodifiableList(Arrays.asList(fieldPath.split("\\."))); + } + + String getAttribute() { + return attribute; + } + + List getFields() { + return fields; + } + + Type getType() { + return type; + } + + static Map> groupByType(FieldMapping[] fieldMappings) { + + EnumMap> fields = new EnumMap<>(Type.class); + for (FieldMapping.Type type : FieldMapping.Type.values()) { + fields.put(type, new ArrayList<>()); + } + for (FieldMapping fieldMapping : fieldMappings) { + fields.get(fieldMapping.getType()).add(fieldMapping); + } + return fields; + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/MethodHandleFactory.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/MethodHandleFactory.java new file mode 100644 index 0000000000..9108806d40 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/MethodHandleFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.util.Locale; +import java.util.concurrent.ConcurrentHashMap; + +class MethodHandleFactory { + + private static String unCapitalize(String string) { + return string.substring(0, 1).toLowerCase(Locale.ROOT) + string.substring(1); + } + + private final ClassValue> getterCache = + new ClassValue>() { + @Override + protected ConcurrentHashMap computeValue(Class type) { + return new ConcurrentHashMap<>(); + } + }; + + MethodHandle forField(Class clazz, String fieldName) + throws NoSuchMethodException, IllegalAccessException { + MethodHandle methodHandle = getterCache.get(clazz).get(fieldName); + if (methodHandle == null) { + // getter in AWS SDK is lowercased field name + methodHandle = + MethodHandles.publicLookup().unreflect(clazz.getMethod(unCapitalize(fieldName))); + getterCache.get(clazz).put(fieldName, methodHandle); + } + return methodHandle; + } +} diff --git a/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/Serializer.java b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/Serializer.java new file mode 100644 index 0000000000..1d5dc47907 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/Serializer.java @@ -0,0 +1,292 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2; + +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + * + * Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkPojo; +import software.amazon.awssdk.http.ContentStreamProvider; +import software.amazon.awssdk.http.SdkHttpFullRequest; +import software.amazon.awssdk.protocols.core.ProtocolMarshaller; +import software.amazon.awssdk.utils.IoUtils; +import software.amazon.awssdk.utils.StringUtils; + +class Serializer { + + @Nullable + String serialize(Object target) { + + if (target == null) { + return null; + } + + if (target instanceof SdkPojo) { + return serialize((SdkPojo) target); + } + + if (target instanceof Collection) { + return serialize((Collection) target); + } + if (target instanceof Map) { + return serialize(((Map) target).keySet()); + } + // simple type + return target.toString(); + } + + // 2025-07-22: Amazon addition + @Nullable + String serialize(String attributeName, Object target) { + try { + // Extract JSON string from target if it is a Bedrock Runtime JSON blob + String jsonString; + if (target instanceof SdkBytes) { + jsonString = ((SdkBytes) target).asUtf8String(); + } else { + if (target != null) { + return target.toString(); + } + return null; + } + + // Parse the LLM JSON string into a Map + BedrockJsonParser.LlmJson llmJson = BedrockJsonParser.parse(jsonString); + + // Use attribute name to extract the corresponding value + switch (attributeName) { + case "gen_ai.request.max_tokens": + return getMaxTokens(llmJson); + case "gen_ai.request.temperature": + return getTemperature(llmJson); + case "gen_ai.request.top_p": + return getTopP(llmJson); + case "gen_ai.response.finish_reasons": + return getFinishReasons(llmJson); + case "gen_ai.usage.input_tokens": + return getInputTokens(llmJson); + case "gen_ai.usage.output_tokens": + return getOutputTokens(llmJson); + default: + return null; + } + } catch (RuntimeException e) { + return null; + } + } + + // End of Amazon addition + + @Nullable + private static String serialize(SdkPojo sdkPojo) { + ProtocolMarshaller marshaller = + AwsJsonProtocolFactoryAccess.createMarshaller(); + if (marshaller == null) { + return null; + } + Optional optional = marshaller.marshall(sdkPojo).contentStreamProvider(); + return optional + .map( + csp -> { + try (InputStream cspIs = csp.newStream()) { + return IoUtils.toUtf8String(cspIs); + } catch (IOException e) { + return null; + } + }) + .orElse(null); + } + + private String serialize(Collection collection) { + String serialized = collection.stream().map(this::serialize).collect(Collectors.joining(",")); + return (StringUtils.isEmpty(serialized) ? null : "[" + serialized + "]"); + } + + // 2025-07-22: Amazon addition + @Nullable + private static String approximateTokenCount( + BedrockJsonParser.LlmJson jsonBody, String... textPaths) { + return Arrays.stream(textPaths) + .map( + path -> { + Object value = BedrockJsonParser.JsonPathResolver.resolvePath(jsonBody, path); + if (value instanceof String) { + int tokenEstimate = (int) Math.ceil(((String) value).length() / 6.0); + return Integer.toString(tokenEstimate); + } + return null; + }) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + // Model -> Path Mapping: + // Amazon Nova -> "/inferenceConfig/max_new_tokens" + // Amazon Titan -> "/textGenerationConfig/maxTokenCount" + // Anthropic Claude -> "/max_tokens" + // Cohere Command -> "/max_tokens" + // Cohere Command R -> "/max_tokens" + // AI21 Jamba -> "/max_tokens" + // Meta Llama -> "/max_gen_len" + // Mistral AI -> "/max_tokens" + @Nullable + private static String getMaxTokens(BedrockJsonParser.LlmJson jsonBody) { + Object value = + BedrockJsonParser.JsonPathResolver.resolvePath( + jsonBody, + "/max_tokens", + "/max_gen_len", + "/textGenerationConfig/maxTokenCount", + "inferenceConfig/max_new_tokens"); + return value != null ? String.valueOf(value) : null; + } + + // Model -> Path Mapping: + // Amazon Nova -> "/inferenceConfig/temperature" + // Amazon Titan -> "/textGenerationConfig/temperature" + // Anthropic Claude -> "/temperature" + // Cohere Command -> "/temperature" + // Cohere Command R -> "/temperature" + // AI21 Jamba -> "/temperature" + // Meta Llama -> "/temperature" + // Mistral AI -> "/temperature" + @Nullable + private static String getTemperature(BedrockJsonParser.LlmJson jsonBody) { + Object value = + BedrockJsonParser.JsonPathResolver.resolvePath( + jsonBody, + "/temperature", + "/textGenerationConfig/temperature", + "/inferenceConfig/temperature"); + return value != null ? String.valueOf(value) : null; + } + + // Model -> Path Mapping: + // Amazon Nova -> "/inferenceConfig/top_p" + // Amazon Titan -> "/textGenerationConfig/topP" + // Anthropic Claude -> "/top_p" + // Cohere Command -> "/p" + // Cohere Command R -> "/p" + // AI21 Jamba -> "/top_p" + // Meta Llama -> "/top_p" + // Mistral AI -> "/top_p" + @Nullable + private static String getTopP(BedrockJsonParser.LlmJson jsonBody) { + Object value = + BedrockJsonParser.JsonPathResolver.resolvePath( + jsonBody, "/top_p", "/p", "/textGenerationConfig/topP", "/inferenceConfig/top_p"); + return value != null ? String.valueOf(value) : null; + } + + // Model -> Path Mapping: + // Amazon Nova -> "/stopReason" + // Amazon Titan -> "/results/0/completionReason" + // Anthropic Claude -> "/stop_reason" + // Cohere Command -> "/generations/0/finish_reason" + // Cohere Command R -> "/finish_reason" + // AI21 Jamba -> "/choices/0/finish_reason" + // Meta Llama -> "/stop_reason" + // Mistral AI -> "/outputs/0/stop_reason" + @Nullable + private static String getFinishReasons(BedrockJsonParser.LlmJson jsonBody) { + Object value = + BedrockJsonParser.JsonPathResolver.resolvePath( + jsonBody, + "/stopReason", + "/finish_reason", + "/stop_reason", + "/results/0/completionReason", + "/generations/0/finish_reason", + "/choices/0/finish_reason", + "/outputs/0/stop_reason"); + + return value != null ? "[" + value + "]" : null; + } + + // Model -> Path Mapping: + // Amazon Nova -> "/usage/inputTokens" + // Amazon Titan -> "/inputTextTokenCount" + // Anthropic Claude -> "/usage/input_tokens" + // Cohere Command -> "/prompt" + // Cohere Command R -> "/message" + // AI21 Jamba -> "/usage/prompt_tokens" + // Meta Llama -> "/prompt_token_count" + // Mistral AI -> "/prompt" + @Nullable + private static String getInputTokens(BedrockJsonParser.LlmJson jsonBody) { + // Try direct tokens counts first + Object directCount = + BedrockJsonParser.JsonPathResolver.resolvePath( + jsonBody, + "/inputTextTokenCount", + "/prompt_token_count", + "/usage/input_tokens", + "/usage/prompt_tokens", + "/usage/inputTokens"); + + if (directCount != null) { + return String.valueOf(directCount); + } + + // Fall back to token approximation + Object approxTokenCount = approximateTokenCount(jsonBody, "/prompt", "/message"); + + return approxTokenCount != null ? String.valueOf(approxTokenCount) : null; + } + + // Model -> Path Mapping: + // Amazon Nova -> "/usage/outputTokens" + // Amazon Titan -> "/results/0/tokenCount" + // Anthropic Claude -> "/usage/output_tokens" + // Cohere Command -> "/generations/0/text" + // Cohere Command R -> "/text" + // AI21 Jamba -> "/usage/completion_tokens" + // Meta Llama -> "/generation_token_count" + // Mistral AI -> "/outputs/0/text" + @Nullable + private static String getOutputTokens(BedrockJsonParser.LlmJson jsonBody) { + // Try direct token counts first + Object directCount = + BedrockJsonParser.JsonPathResolver.resolvePath( + jsonBody, + "/generation_token_count", + "/results/0/tokenCount", + "/usage/output_tokens", + "/usage/completion_tokens", + "/usage/outputTokens"); + + if (directCount != null) { + return String.valueOf(directCount); + } + + // Fall back to token approximation + Object approxTokenCount = approximateTokenCount(jsonBody, "/text", "/outputs/0/text"); + + return approxTokenCount != null ? String.valueOf(approxTokenCount) : null; + } + // End of Amazon addition +} diff --git a/instrumentation/aws-sdk/src/main/resources/META-INF/services/io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule b/instrumentation/aws-sdk/src/main/resources/META-INF/services/io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule new file mode 100644 index 0000000000..36ffc94182 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/resources/META-INF/services/io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule @@ -0,0 +1,2 @@ +software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkInstrumentationModule +software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AdotAwsSdkInstrumentationModule \ No newline at end of file diff --git a/instrumentation/aws-sdk/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors.adot b/instrumentation/aws-sdk/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors.adot new file mode 100644 index 0000000000..2ff36d9efd --- /dev/null +++ b/instrumentation/aws-sdk/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors.adot @@ -0,0 +1 @@ +software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkTracingExecutionInterceptor \ No newline at end of file diff --git a/instrumentation/aws-sdk/src/test/groovy/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/BedrockJsonParserTest.groovy b/instrumentation/aws-sdk/src/test/groovy/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/BedrockJsonParserTest.groovy new file mode 100644 index 0000000000..d415e40e2f --- /dev/null +++ b/instrumentation/aws-sdk/src/test/groovy/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/BedrockJsonParserTest.groovy @@ -0,0 +1,117 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11 + +import spock.lang.Specification + +class BedrockJsonParserTest extends Specification { + def "should parse simple JSON object"() { + given: + String json = '{"key":"value"}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + + then: + parsedJson.getJsonBody() == [key: "value"] + } + + def "should parse nested JSON object"() { + given: + String json = '{"parent":{"child":"value"}}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + + then: + def parent = parsedJson.getJsonBody().get("parent") + parent instanceof Map + parent["child"] == "value" + } + + def "should parse JSON array"() { + given: + String json = '{"array":[1, "two", 1.0]}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + + then: + def array = parsedJson.getJsonBody().get("array") + array instanceof List + array == [1, "two", 1.0] + } + + def "should parse escape sequences"() { + given: + String json = '{"escaped":"Line1\\nLine2\\tTabbed\\\"Quoted\\\"\\bBackspace\\fFormfeed\\rCarriageReturn\\\\Backslash\\/Slash\\u0041"}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + + then: + parsedJson.getJsonBody().get("escaped") == + "Line1\nLine2\tTabbed\"Quoted\"\bBackspace\fFormfeed\rCarriageReturn\\Backslash/SlashA" + } + + def "should throw exception for malformed JSON"() { + given: + String malformedJson = '{"key":value}' + + when: + BedrockJsonParser.parse(malformedJson) + + then: + def ex = thrown(IllegalArgumentException) + ex.message.contains("Unexpected character") + } + + def "should resolve path in JSON object"() { + given: + String json = '{"parent":{"child":{"key":"value"}}}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + def resolvedValue = BedrockJsonParser.JsonPathResolver.resolvePath(parsedJson, "/parent/child/key") + + then: + resolvedValue == "value" + } + + def "should resolve path in JSON array"() { + given: + String json = '{"array":[{"key":"value1"}, {"key":"value2"}]}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + def resolvedValue = BedrockJsonParser.JsonPathResolver.resolvePath(parsedJson, "/array/1/key") + + then: + resolvedValue == "value2" + } + + def "should return null for invalid path resolution"() { + given: + String json = '{"parent":{"child":{"key":"value"}}}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + def resolvedValue = BedrockJsonParser.JsonPathResolver.resolvePath(parsedJson, "/invalid/path") + + then: + resolvedValue == null + } +} diff --git a/instrumentation/aws-sdk/src/test/groovy/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/BedrockJsonParserTest.groovy b/instrumentation/aws-sdk/src/test/groovy/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/BedrockJsonParserTest.groovy new file mode 100644 index 0000000000..bd66b726e0 --- /dev/null +++ b/instrumentation/aws-sdk/src/test/groovy/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/BedrockJsonParserTest.groovy @@ -0,0 +1,117 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2 + +import spock.lang.Specification + +class BedrockJsonParserTest extends Specification { + def "should parse simple JSON object"() { + given: + String json = '{"key":"value"}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + + then: + parsedJson.getJsonBody() == [key: "value"] + } + + def "should parse nested JSON object"() { + given: + String json = '{"parent":{"child":"value"}}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + + then: + def parent = parsedJson.getJsonBody().get("parent") + parent instanceof Map + parent["child"] == "value" + } + + def "should parse JSON array"() { + given: + String json = '{"array":[1, "two", 1.0]}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + + then: + def array = parsedJson.getJsonBody().get("array") + array instanceof List + array == [1, "two", 1.0] + } + + def "should parse escape sequences"() { + given: + String json = '{"escaped":"Line1\\nLine2\\tTabbed\\\"Quoted\\\"\\bBackspace\\fFormfeed\\rCarriageReturn\\\\Backslash\\/Slash\\u0041"}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + + then: + parsedJson.getJsonBody().get("escaped") == + "Line1\nLine2\tTabbed\"Quoted\"\bBackspace\fFormfeed\rCarriageReturn\\Backslash/SlashA" + } + + def "should throw exception for malformed JSON"() { + given: + String malformedJson = '{"key":value}' + + when: + BedrockJsonParser.parse(malformedJson) + + then: + def ex = thrown(IllegalArgumentException) + ex.message.contains("Unexpected character") + } + + def "should resolve path in JSON object"() { + given: + String json = '{"parent":{"child":{"key":"value"}}}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + def resolvedValue = BedrockJsonParser.JsonPathResolver.resolvePath(parsedJson, "/parent/child/key") + + then: + resolvedValue == "value" + } + + def "should resolve path in JSON array"() { + given: + String json = '{"array":[{"key":"value1"}, {"key":"value2"}]}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + def resolvedValue = BedrockJsonParser.JsonPathResolver.resolvePath(parsedJson, "/array/1/key") + + then: + resolvedValue == "value2" + } + + def "should return null for invalid path resolution"() { + given: + String json = '{"parent":{"child":{"key":"value"}}}' + + when: + def parsedJson = BedrockJsonParser.parse(json) + def resolvedValue = BedrockJsonParser.JsonPathResolver.resolvePath(parsedJson, "/invalid/path") + + then: + resolvedValue == null + } +} diff --git a/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkClientAdviceTest.java b/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkClientAdviceTest.java new file mode 100644 index 0000000000..ffdbadec5f --- /dev/null +++ b/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkClientAdviceTest.java @@ -0,0 +1,86 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazonaws.handlers.RequestHandler2; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AdotAwsSdkClientAdviceTest { + + private AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice advice; + private List handlers; + + @BeforeEach + void setUp() { + advice = new AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice(); + handlers = new ArrayList<>(); + } + + @Test + void testAddHandlerWhenHandlersIsNull() { + AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice.addHandler(null); + assertThat(handlers).hasSize(0); + } + + @Test + void testAddHandlerWhenNoOtelHandler() { + RequestHandler2 someOtherHandler = mock(RequestHandler2.class); + handlers.add(someOtherHandler); + + AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice.addHandler(handlers); + + assertThat(handlers).hasSize(1); + assertThat(handlers).containsExactly(someOtherHandler); + } + + @Test + void testAddHandlerWhenOtelHandlerPresent() { + RequestHandler2 otelHandler = mock(RequestHandler2.class); + when(otelHandler.toString()) + .thenReturn( + "io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.TracingRequestHandler"); + handlers.add(otelHandler); + + AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice.addHandler(handlers); + + assertThat(handlers).hasSize(2); + assertThat(handlers.get(0)).isEqualTo(otelHandler); + assertThat(handlers.get(1)).isInstanceOf(AdotAwsSdkTracingRequestHandler.class); + } + + @Test + void testAddHandlerWhenAdotHandlerAlreadyPresent() { + RequestHandler2 otelHandler = mock(RequestHandler2.class); + when(otelHandler.toString()) + .thenReturn( + "io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.TracingRequestHandler"); + handlers.add(otelHandler); + handlers.add(new AdotAwsSdkTracingRequestHandler()); + + AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice.addHandler(handlers); + + assertThat(handlers).hasSize(2); + assertThat(handlers.get(0)).isEqualTo(otelHandler); + assertThat(handlers.get(1)).isInstanceOf(AdotAwsSdkTracingRequestHandler.class); + } +} diff --git a/lambda-layer/patches/aws-otel-java-instrumentation.patch b/lambda-layer/patches/aws-otel-java-instrumentation.patch index 99dd328af1..2c6dfa70dd 100644 --- a/lambda-layer/patches/aws-otel-java-instrumentation.patch +++ b/lambda-layer/patches/aws-otel-java-instrumentation.patch @@ -6,7 +6,7 @@ index 9493189..6090207 100644 val TEST_SNAPSHOTS = rootProject.findProperty("testUpstreamSnapshots") == "true" // This is the version of the upstream instrumentation BOM --val otelVersion = "2.11.0-adot2" +-val otelVersion = "2.11.0-adot3" +val otelVersion = "2.11.0-adot-lambda1" val otelSnapshotVersion = "2.12.0" val otelAlphaVersion = if (!TEST_SNAPSHOTS) "$otelVersion-alpha" else "$otelSnapshotVersion-alpha-SNAPSHOT" diff --git a/lambda-layer/patches/opentelemetry-java-instrumentation.patch b/lambda-layer/patches/opentelemetry-java-instrumentation.patch index e9253c5d87..127751f5b0 100644 --- a/lambda-layer/patches/opentelemetry-java-instrumentation.patch +++ b/lambda-layer/patches/opentelemetry-java-instrumentation.patch @@ -310,8 +310,8 @@ index 7900c9a4d9..80383d7c22 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -1,5 +1,5 @@ --val stableVersion = "2.11.0-adot2" --val alphaVersion = "2.11.0-adot2-alpha" +-val stableVersion = "2.11.0-adot3" +-val alphaVersion = "2.11.0-adot3-alpha" +val stableVersion = "2.11.0-adot-lambda1" +val alphaVersion = "2.11.0-adot-lambda1-alpha" diff --git a/otelagent/build.gradle.kts b/otelagent/build.gradle.kts index 4208516c29..f7a543fb39 100644 --- a/otelagent/build.gradle.kts +++ b/otelagent/build.gradle.kts @@ -60,6 +60,7 @@ dependencies { javaagentLibs(project(":awsagentprovider")) javaagentLibs(project(":instrumentation:log4j-2.13.2")) + javaagentLibs(project(":instrumentation:aws-sdk")) javaagentLibs(project(":instrumentation:logback-1.0")) javaagentLibs(project(":instrumentation:jmx-metrics")) } diff --git a/settings.gradle.kts b/settings.gradle.kts index f6b5033352..6c44234701 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,7 +33,7 @@ dependencyResolutionManagement { mavenLocal() maven { - setUrl("https://oss.sonatype.org/content/repositories/snapshots") + setUrl("https://central.sonatype.com/repository/maven-snapshots/") } } } @@ -44,6 +44,7 @@ include(":dependencyManagement") include(":instrumentation:logback-1.0") include(":instrumentation:log4j-2.13.2") include(":instrumentation:jmx-metrics") +include("instrumentation:aws-sdk") include(":otelagent") include(":smoke-tests:fakebackend") include(":smoke-tests:runner")