From db62147c273ed79e4b3f75de2026e6649b309065 Mon Sep 17 00:00:00 2001 From: Anahat Date: Tue, 8 Jul 2025 10:46:40 -0700 Subject: [PATCH 1/6] Base of AWS SDK v2.2 SPI Implementation (#1111) Note: this is not the complete SPI implementation ### Issue The current ADOT Java SDK implementation relies on a combination of OpenTelemetry SPI and Git patches to extend the OTel SDK functionality. This approach presents several challenges: - Reduced modularity and maintainability - Increased risk of errors during OTel SDK version upgrades - Manual intervention required for patch management - Limited ecosystem compatibility with upstream OpenTelemetry - Difficulty in extending functionality for users This is the skeleton set up for the SPI, which aims to remove the [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch) for aws-sdk v2.2 by using OTel's InstrumentationModule SPI extension. This instrumentation is essentially complementing the upstream java agent. It is completely separate from upstream and instruments after the Otel agent. ### Description of Changes This PR sets up the foundational structure for AWS SDK v2.2 Instrumentation, moving away from the current patching approach. It doesn't modify current ADOT functionality or the upstream span. It just registers the ADOT SPI implementation and sets up the interceptor hooks. #### Core Components 1. **AdotAwsSdkInstrumentationModule** - Extends OpenTelemetry's `InstrumentationModule` SPI - Registers custom interceptors in specific order: 1. Upstream AWS SDK execution interceptor 2. ADOT custom interceptor - Ensures proper instrumentation sequencing through careful resource registration 2. **AdotTracingExecutionInterceptor** - Extends AWS SDK's `ExecutionInterceptor` - Hooks into key SDK lifecycle points: - `beforeTransmission`: Captures final SDK request after upstream modifications - `modifyResponse`: Processes response before span closure in upstream - Will be used to enriches spans - Acts as central coordinator for all the awssdk_v2_2 components 3. **Resources Folder** - Registers the AdotAwsSdkInstrumentationModule into OTel's SPI extension classpath in META-INF/services - Registers AdotTracingExecutionInterceptor into AWS SDK's interceptor classpath in software.amazon.awssdk.global.handlers ### Key Design Decisions 1. **Instrumentation Ordering** - Deliberately structured to run after upstream OTel agent - Ensures all upstream modifications are captured - Maintains compatibility with existing instrumentation 2. **Lifecycle Hook Points** - `beforeTransmission`: Last point to access modified request - `modifyResponse`: Final opportunity to enrich span before closure - Carefully chosen to ensure complete attribute capture ### Testing - Verified existing functionality remains unchanged and contract tests pass (all contract tests pass after following the steps [here](https://github.com/aws-observability/aws-otel-java-instrumentation/tree/main/appsignals-tests)) - Confirmed build success with new structure ### Benefits of using SPI - Improved Maintainability: Clear separation between OTel core and AWS-specific instrumentation - Better Extensibility: Users can more easily extend or modify AWS-specific behavior - Reduced Risk: Eliminates manual patching during OTel upgrades - Enhanced Compatibility: Better alignment with OpenTelemetry's extension mechanisms - Clearer Code Organization: More intuitive structure for future contributions By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- instrumentation/aws-sdk/README.md | 38 ++++++++ instrumentation/aws-sdk/build.gradle.kts | 27 ++++++ .../AdotAwsSdkInstrumentationModule.java | 88 +++++++++++++++++++ ...AdotAwsSdkTracingExecutionInterceptor.java | 38 ++++++++ ...sion.instrumentation.InstrumentationModule | 1 + .../handlers/execution.interceptors.adot | 1 + otelagent/build.gradle.kts | 1 + settings.gradle.kts | 1 + 8 files changed, 195 insertions(+) create mode 100644 instrumentation/aws-sdk/README.md create mode 100644 instrumentation/aws-sdk/build.gradle.kts create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkInstrumentationModule.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkTracingExecutionInterceptor.java create mode 100644 instrumentation/aws-sdk/src/main/resources/META-INF/services/io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule create mode 100644 instrumentation/aws-sdk/src/main/resources/software/amazon/awssdk/global/handlers/execution.interceptors.adot diff --git a/instrumentation/aws-sdk/README.md b/instrumentation/aws-sdk/README.md new file mode 100644 index 0000000000..0d5b56c011 --- /dev/null +++ b/instrumentation/aws-sdk/README.md @@ -0,0 +1,38 @@ +## ADOT AWS SDK Instrumentation + +### Overview +The aws-sdk instrumentation is an SPI-based implementation that extends the upstream OpenTelemetry AWS Java SDK instrumentation. + +_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) + +### 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 + +_**Important Note:**_ +The upstream interceptor closes the span in `afterExecution`. 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. + + diff --git a/instrumentation/aws-sdk/build.gradle.kts b/instrumentation/aws-sdk/build.gradle.kts new file mode 100644 index 0000000000..4f04e804db --- /dev/null +++ b/instrumentation/aws-sdk/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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") +} + +base.archivesBaseName = "aws-instrumentation-aws-sdk" + +dependencies { + compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") + compileOnly("software.amazon.awssdk:aws-core:2.2.0") + compileOnly("net.bytebuddy:byte-buddy") +} 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..0516352373 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkInstrumentationModule.java @@ -0,0 +1,88 @@ +/* + * 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 99; + } + + @Override + public List getAdditionalHelperClassNames() { + return Arrays.asList( + "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkTracingExecutionInterceptor"); + } + + /** + * 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..0775775edd --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AdotAwsSdkTracingExecutionInterceptor.java @@ -0,0 +1,38 @@ +/* + * 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 software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.interceptor.*; + +public class AdotAwsSdkTracingExecutionInterceptor implements ExecutionInterceptor { + + // 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. + @Override + public void beforeTransmission( + Context.BeforeTransmission context, ExecutionAttributes executionAttributes) {} + + // 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. + @Override + public SdkResponse modifyResponse( + Context.ModifyResponse context, ExecutionAttributes executionAttributes) { + + return context.response(); + } +} 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..138c87c03f --- /dev/null +++ b/instrumentation/aws-sdk/src/main/resources/META-INF/services/io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule @@ -0,0 +1 @@ +software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.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/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 1a76fa3e2c..6c44234701 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") From 3a939c1ccea1ec1c7f04ed6033643507edbb3348 Mon Sep 17 00:00:00 2001 From: Anahat Date: Thu, 17 Jul 2025 09:24:25 -0700 Subject: [PATCH 2/6] Base of AWS SDK v1.11 SPI Implementation (#1115) This PR is similar to #1111, as it sets a base SPI implementation for AWS SDK v1.11. ### Issue This is the skeleton set up for the SPI, which aims to remove the [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch) for aws-sdk v1.11 by using OTel's InstrumentationModule SPI extension. This instrumentation is essentially complementing the upstream java agent. It is completely separate from upstream and instruments after the Otel agent. ### Description of Changes This PR sets up the foundational structure for AWS SDK v1.11 Instrumentation, moving away from the current patching approach. It doesn't modify current ADOT functionality or the upstream span. It just registers the ADOT SPI implementation and sets up the interceptor hooks. #### Core Components 1. **AdotAwsSdkInstrumentationModule** - Extends OpenTelemetry's `InstrumentationModule` SPI - Registers custom handler through AdotAwsSdkClientInstrumentation class in `typeInstrumentations` method 2. **AdotAwsSdkClientInstrumentation** - 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). 3. **AdotAwsSdkTracingRequestHandler** - Extends AWS SDK's `RequestHandler2` - Hooks into key SDK lifecycle points: - `beforeRequest`: Captures final SDK request after upstream modifications - `afterAttempt`: Processes response before span closure in upstream - Will be used to enriches spans - Acts as central coordinator for all the awssdk_v1_11 components 4. **Resources Folder** - Registers the v1.11 AdotAwsSdkInstrumentationModule into OTel's SPI extension classpath in META-INF/services ### Key Design Decisions 1. **Instrumentation Ordering** - Deliberately structured to run after upstream OTel agent - Ensures all upstream modifications are captured - Maintains compatibility with existing instrumentation 2. **Lifecycle Hook Points** - `beforeRequest`: Last point to access modified request - `afterAttempt`: Final opportunity to enrich span before closure ### Testing - Verified existing functionality remains unchanged and contract tests pass (all contract tests pass after following the steps [here](https://github.com/aws-observability/aws-otel-java-instrumentation/tree/main/appsignals-tests)) - Confirmed build success with new structure By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --- instrumentation/aws-sdk/README.md | 50 ++++++++- instrumentation/aws-sdk/build.gradle.kts | 5 + .../AdotAwsSdkClientInstrumentation.java | 104 ++++++++++++++++++ .../AdotAwsSdkInstrumentationModule.java | 61 ++++++++++ .../AdotAwsSdkTracingRequestHandler.java | 56 ++++++++++ ...sion.instrumentation.InstrumentationModule | 3 +- .../AdotAwsSdkClientAdviceTest.java | 86 +++++++++++++++ 7 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkClientInstrumentation.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkInstrumentationModule.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkTracingRequestHandler.java create mode 100644 instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkClientAdviceTest.java diff --git a/instrumentation/aws-sdk/README.md b/instrumentation/aws-sdk/README.md index 0d5b56c011..78ac5344f4 100644 --- a/instrumentation/aws-sdk/README.md +++ b/instrumentation/aws-sdk/README.md @@ -3,7 +3,21 @@ ### Overview The aws-sdk instrumentation is an SPI-based implementation that extends the upstream OpenTelemetry AWS Java SDK instrumentation. -_Initialization Workflow_ +##### _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 @@ -13,6 +27,40 @@ _Initialization Workflow_ - Finds ADOT’s **AdotAwsSdkInstrumentationModule** - Registers **AdotAwsSdkTracingExecutionInterceptor** (order > 0) +### 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. + ### AWS SDK v2 Instrumentation Summary **AdotAwsSdkInstrumentationModule** diff --git a/instrumentation/aws-sdk/build.gradle.kts b/instrumentation/aws-sdk/build.gradle.kts index 4f04e804db..0e87d814a6 100644 --- a/instrumentation/aws-sdk/build.gradle.kts +++ b/instrumentation/aws-sdk/build.gradle.kts @@ -22,6 +22,11 @@ base.archivesBaseName = "aws-instrumentation-aws-sdk" dependencies { compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") + compileOnly("com.amazonaws:aws-java-sdk-core:1.11.0") compileOnly("software.amazon.awssdk:aws-core:2.2.0") compileOnly("net.bytebuddy:byte-buddy") + + 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") } 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..677ea4612f --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkInstrumentationModule.java @@ -0,0 +1,61 @@ +/* + * 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 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"); + } + + @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..4ead53f5b1 --- /dev/null +++ b/instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AdotAwsSdkTracingRequestHandler.java @@ -0,0 +1,56 @@ +/* + * 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 com.amazonaws.Request; +import com.amazonaws.handlers.HandlerAfterAttemptContext; +import com.amazonaws.handlers.RequestHandler2; + +/** + * 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 { + + public AdotAwsSdkTracingRequestHandler() {} + + /** + * 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) {} + + /** + * 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) {} +} 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 index 138c87c03f..36ffc94182 100644 --- 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 @@ -1 +1,2 @@ -software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkInstrumentationModule \ No newline at end of file +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/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); + } +} From de3a1f2d6a81ddb3b235879e39d5c2e2796d215c Mon Sep 17 00:00:00 2001 From: Anahat Date: Thu, 24 Jul 2025 11:21:59 -0700 Subject: [PATCH 3/6] AWS SDK v2.2 SPI Patch Migration (#1113) Note: This is a continuation of #1111 #### Description of Changes This implementation builds on the foundation established in PR #1111, transforming the structural setup into a fully functional SPI-based solution that will replace our current patching approach. This PR does not change the current ADOT functionality because patches have not been removed. The next/final PR for v2.2 will remove the patches for aws-sdk-2.2 and have unit tests to ensure correct SPI functionality and behaviour. The final PR will also pass all the contract-tests once patches are removed. #### Changes include: - Migration of patched files into proper package structure: NOTE: We are not copying entire files from upstream. Instead, we only migrated the new components that were added by our patches and the methods that use these AWS-specific components. I deliberately removed any code that was untouched by our patches to avoid duplicating upstream instrumentation code. This selective migration ensures we maintain only our AWS-specific additions while letting OTel handle its base functionality. - `AwsExperimentalAttributes` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L2587) creates new class - `AwsSdkRequest` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L2673) on [this otel file](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/AwsSdkRequest.java) - `AwsSdkRequestType` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L2751) on [this otel file](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/AwsSdkRequestType.java) - `BedrockJsonParser` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L2860) creates new class - `FieldMapper` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L3145) on [this otel file](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/FieldMapper.java) - `Serializer` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L3164) on [this otel file](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/Serializer.java) - `BedrockJsonParserTest` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L3474) creates new class - Setup of dependent files directly copied from upstream aws-sdk: - `MethodHandleFactory` - copy-pasted from [here](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/MethodHandleFactory.java) - `FieldMapping` - copy-pasted from [here](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/FieldMapping.java) - `AwsJsonProtocolFactoryAccess` - copy-pasted from [here](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/AwsJsonProtocolFactoryAccess.java) These added files: - Access and modify span attributes - Provide consistent formatting tools for span attributes - Can be updated at our convenience if needed The classes we copied from upstream are just helper utilities that make it easier to inject span attributes during instrumentation. They're not core functionality that needs to stay in sync with upstream changes, rather standalone utilities that support our simpler, independent instrumentation without creating version lock-in. They're independent of OTel's core functionality, so we don't face the same version dependency issues we had with patching. We're just following upstream's structure for consistency. #### OTel attribution for copied files The 3 coped files have the OTel header included in them. This follows section 4 a)-c) in the ADOT [LICENSE](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/LICENSE#L89) #### Testing - Existing functionality verified - Contract tests passing - Build successful #### Related - Skeleton PR for aws-sdk v2.2: https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1111 - Replaces patch: [current patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch) By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Thomas Pierce --- instrumentation/aws-sdk/README.md | 34 +- instrumentation/aws-sdk/build.gradle.kts | 8 + .../aws-sdk/sequence-diagram-2.2.png | Bin 0 -> 117452 bytes .../AdotAwsSdkInstrumentationModule.java | 19 +- ...AdotAwsSdkTracingExecutionInterceptor.java | 126 +++++++- .../AwsExperimentalAttributes.java | 90 ++++++ .../AwsJsonProtocolFactoryAccess.java | 102 ++++++ .../awssdk_v2_2/AwsSdkRequest.java | 144 +++++++++ .../awssdk_v2_2/AwsSdkRequestType.java | 139 +++++++++ .../awssdk_v2_2/BedrockJsonParser.java | 289 +++++++++++++++++ .../awssdk_v2_2/FieldMapper.java | 109 +++++++ .../awssdk_v2_2/FieldMapping.java | 78 +++++ .../awssdk_v2_2/MethodHandleFactory.java | 53 ++++ .../awssdk_v2_2/Serializer.java | 292 ++++++++++++++++++ .../awssdk_v2_2/BedrockJsonParserTest.groovy | 117 +++++++ 15 files changed, 1589 insertions(+), 11 deletions(-) create mode 100644 instrumentation/aws-sdk/sequence-diagram-2.2.png create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsExperimentalAttributes.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsJsonProtocolFactoryAccess.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkRequest.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkRequestType.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/BedrockJsonParser.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/FieldMapper.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/FieldMapping.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/MethodHandleFactory.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/Serializer.java create mode 100644 instrumentation/aws-sdk/src/test/groovy/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/BedrockJsonParserTest.groovy diff --git a/instrumentation/aws-sdk/README.md b/instrumentation/aws-sdk/README.md index 78ac5344f4..9d2bffb8b7 100644 --- a/instrumentation/aws-sdk/README.md +++ b/instrumentation/aws-sdk/README.md @@ -79,8 +79,36 @@ The AdotAwsSdkTracingExecutionInterceptor hooks onto OpenTelemetry's spans durin 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 -_**Important Note:**_ -The upstream interceptor closes the span in `afterExecution`. 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. +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 \ No newline at end of file diff --git a/instrumentation/aws-sdk/build.gradle.kts b/instrumentation/aws-sdk/build.gradle.kts index 0e87d814a6..e2cd769150 100644 --- a/instrumentation/aws-sdk/build.gradle.kts +++ b/instrumentation/aws-sdk/build.gradle.kts @@ -16,15 +16,23 @@ plugins { java id("com.gradleup.shadow") + id("groovy") } base.archivesBaseName = "aws-instrumentation-aws-sdk" dependencies { 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") 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 0000000000000000000000000000000000000000..afa64753efdaa92280f31024fc55aa0bca28aa6b GIT binary patch literal 117452 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

1w`S>b>AH2rGwj zLuz2UVu8E`_j*g)<8e|m?EKetl&6@txN+Of_7mZZpBtg$IhWtpi7${nl&*2C0b{d0 zbU%d8-kx9CvY%L;tF+{yBvy(sK_%RZm~eCNi@3N%d1b z-hYO@>Jc`6)8>%`K;P;Vp6i75WUgjpi0Wqqzhc?y!#mmTBio1-GS+B4{5WQi$D>I9 z=Vus?ajN)DB!@<=eNT^V|b&u`W435BcPRc!`U!8<)Y8$Ef ztURx29`i}%_JvB|vFvvYrG1#{rHzO0&%ZPxRY|=MzTZkI{nV@=c}lT;LKH7~>)SJc z^wj3*7c=0Lc^{%Nc|&N^*HF!Z~K&rowG~aYi^vi1k|fjx#&wXQ{AXHOX4$2fM3NqH2D!PsK4(mK_w{*awHzF`EP}t7k8p|HhSZ=ZL;S& zGE$B5PJ$jTg@c@QJl6aZcUA)VWu$s9r<@v>a<5j}qefy@ZnLWO?AFlcn^-vXYkszD zCbDb6k4gQ*F4~FqTE5SqS~G11!Ivs4&s(@a{;+ekT1VQN&71HMI6p zhvlakhEUhN>APV6lQy9?pBHB<<2!Za^C(_fE|}RkxA$^$*VFge0nQ~)2@^g87ok2O zgi+eAfe!QZ*_0*x%}RUQB+B*_$a}X3+%sPDCg8DxmC+>rpu6RKTTNdsUe7rzQp+<} z*&uWI?|B4_+3?D{-4E5>qG=6pvvMmnbaBWZy;nn=2p3{jvGzyAXfMEi>_p168(@#3 z&&$u*f2_rJiF3=qXi(kH89BI2hrPaFweHT8+OQJbT^^JzM1%Jpdg`e=)oBK-u6fsD z84@wO8khZ-Cx};9vJlnF{Vr!O$H*w4iS)| zJ^0eG%(5@pM6RF2aagOtvAMg-xJ^P_e}|a5Ehmy5Z*;l@rH;i=;4Ug~H24~v$v6{q ze|jhSwSt=aTSZ%vt?EIjiX^ZT+xQ;ghwZy}(dtxuO51(f zMQ5X3Ku(uT*zlS$syuFkHz?~Z`K%y$b>ZsTrq$U%xjqFKI%&c?rUQx26DyojUH3%K zR1mXPz6t;yItP0t+N_E)XnyR6F&P6pXK2~D-iAH!&A^Bjk-Ab6Qc4`~`S|ZQT2H{! zGY!EZ(hdHf(ckKbFg(!-)pm-`K8?@7aP^~l_C>CVHVDqT1WuNMd;l&P^;x1OTzh=e z{Ug`mZ`RBv8P6{E%ea}xfud0x{2-!3sRGO8YOustlGBIN-cOt}oi-f<+}v}Ju#PL6 zO}+&m-~o2Ort@fJ>v4LaB&AT3Os1i4t`w9DBr?ZqVQ6H|;k04Eb;Mn%Hu8ZS2TZ*w z;dp|Ah!Q_ZT(24}UlD&G%Au>IijoEskp|&z_+IdrUkQX`Up21K?A4!?TLj*Avo4`- z2;Y*-Q?O63;$27u#}hQ1kZ?3Q0j0Cu`teMpt?48nUw}y-Nq?}G4_Pjl5y$y*wImdWMa)34*|G7t_1WbyqSuk(FM`66wx ziQscK-O9HwjnPkJB_kb0@h^CQb7N7t4)QaRz{37s1v&{!((8rJFW7`tJyUK5 z0KAi$y-KCN-61kY9>1V&f?mt!+n?_UHnEd zXen1l_JO{#d7IFM>CVx`AS3|f`QjpOrmU)+rnqZ-?Whe7_V2kf^0}PI!pNdd%GOJx z^&HI;&HUi!C4CZ)_w_9`3K3AWx(}-W!TQ_X>q!L6+hw&uM&IMTtH$R;Ez){quiPyg z5#HcfQlgmgHoN8}i2V~%ZBRJiJpU&g6b3C)$xyEIM>^4!Ue!lx9HsRmN(nsKn<(BJ zftMDe`8n4>FRrMnDiK}$SV6`@I51nwaznm`>ni5g&M&^zzqA~_8yofJYf``o`0hta z&IeFP*NfN(mv$@1BZ40Qw2MGmw9`QD@6j`c6D;)Mk!`gI9!Db2hOl8uOw=7_>g9gE zjd5#qap$u<2bD_@_TC#Gws`eK&agHT)gqe(CvwXUzCB$=-BC7jfmtJEv*O~cX@}JC zrj0fn5$xwh9#2KnoiA>QrB!cZbfJ1&aUL$+xf{KhPi?PAuig_a%FsQm`M@-c-6sX= zZYLE?T@%?UuWez|6uMskYn7rZD|b7P^c8nY3@iCbWDK;ggs_mq?(m!Yf;`O zviFm5lDl2`{CaQ87)GHW-8_uzYI;@0*9M(c#Xjd4}mK>lfSeSiRHj=iMK`S^SW!F(wv~f5Q zslXPz;d#NmZ*nkAzJYAHe6}?hDf4a57(q%ByYmv+;k$Q?=EaxJ*B|UZMu*Uxv4>4M zR5mNMc1RtRClReVIWcOQ+qHb|W#FH<67?3>q_i8~p?Dv`-LvMfzVR;O#nqrPNV@QS z@^}ziVEC=HSliY72MLkKlkTRzY3|v5ed8r0st^=aH^e6q#I%C1 z-)PiYP(RcGmy0NNq7FF3oX02DP5JhrWsW>2m*bU+W$EL?$u#J!D5G-RJyTV9ojVQ+ zdtdzoO!Vuzy`t0Uj^y*YIwW8IX=x%s4g__#esjtsn0_>bA&}Rrvl$loky$UNUyQiB zAA%A#=e0FlX`x_X#IPa=wsHV8NQ!V<@c<7)SE5@moe*nigC#KDKAjiTeQ-r_!3W|F zN&)-)OQ}cRjoIJi7GE~v#KuSqf&#yY_XvkIOgA`*sdd}{(p~se`7Q5^JKPr_7_dD4 z5n&lPiBXH^RN&VO9@i0-x3q85cO7un3KN5efagsv8;oY4-tI{HK@COfo&IR_10}om z-wfiZUyz>QfSKLj!ooQoBX&^;K88=QMadHRDgJkU&f$h*YjA&ZN~))kpSEE}%_-%0 zi=;Uf^)zksE&_uC>g`(tEOL*skvFi5Y_q}MiOxc?lqbneDaT1orzU*vQkNpC?T!P; zE*NK`m9g0)MJW1pwjtT&on!?Djpy|{H3+(=D?|9kr6al`E_^SdpOz@@BIDawtoe{u znT`{u0lgxv-zM@W8acdhx5w2EynWdHoTUws&3CXs`EVGvf5Qnlqr}S%MtV^y2yKKt z2>&bGi{blXMzT2^-gSFAB2FcyHRELhZ2$D*tt%nVb28KQUR_N zFQMSMZ+OTq=@@A(tN<|tFWQ=o zx*SH6X7^31T`U$UgwSgK0(R$F+I~Radkz0c%iFgLLNS9vPW<-AJmY z6dOklQwqg@UhyQwM4-I@X|@MUs5BlJG+{0Qo{Hwr(0r~ja#^2veT6r_bqzbTY{+4r zPHJ-76}=M{Wum|w|!95&sLapx7~mgXvgL>W&Iey+(vblOa_pBBpxt6^g;m9gafG68n0lGLI^sX~jc z8NWVx`EZsX=EU#i+3h6_*Ozr=f=*bSc|YO z6j4uMBLI5R<4z7BKUo696G6_O&5zOh%d;6f(Qz2fEf6pfyp5zMMvA0zPp#}~3wX)_ zOIQz~r^o++mox@ig!7BODaX;cUh!UNqwoL@(d|DQ(AB9O=lT=ovVN9D@LA6*Is`F} zxc$(`t-LZT0c3bUYB41P_4BxjBM{tpTXY~dTa}7Ei1848qKHaOD$9&&y2&k~5uX{p zS^;|>UT>2Vq_RUj1Yc@2_|V`u;-iEMH-T~Y8TNOsf=Bd9LUu1Vhk^0)Yg83M_2rcj!A zz=zS~K?{uW;-#^u%8wI%Z*-b8&gA*T87l#3Rk|iYiWV_}d0G)2FChR#{E>XjV)zfC zZIYi8?tM6$wtJ4~qgUHN)?J`QgOBe~y-)LG5Zf6Unh`=9Q@dTIJSzn^RA|lP-dhwo z4-XIKU5Dxjwcl8Sf|CT*7I7@~l){l{b!He7mFAobR+534H~lQMazAjhx5s_{^ezJc zbu4!{u+79WufRHs_VuKUh8F48fSWCwi#wc2_3dwwttTTqKfDttcO5)Fk~*Hz4-m4J zbG@;vNfG1;FP>0r#^-x2%==QBMgTJ1haK^4>ll1TAJm`vg+!#z7xZ}Jv)tswt;)EUW@9|Hf%Fpd&&h~Xd!x`cS&7vfO9A4@K${uQ4u-T-ObhOH zTJdXg&T~___{{RoMtb)Y`!x%|@cX51@wT=-r4eGLyUjQ|u8_F@i>B?}QvdkyV=N?E^txz|%K zWsoy;r|vvx`V&)aZHL8^`9e&_oiox8s%^4?I|KT{W8?3BWGbP`t&tzT?NjHl@ny8z zj?yH}6~=f?7@jAEmRzo5Rr4bH&RA5s&t#IU1GtBfNGMi8Z8ymo4a@EX3K+4XUUN2n zTUjma%R>zA?Cma~%jpOtluA|*Oa%!TY6Z&1R-QJlSleddLv1t2aK;sxD_4`PS zzjNP8V}XP8v0klp{dA0(w9;&@>M$r{zrg7}1-Pho<8*6v_B3tqG|_IV&#aufzo&jx zw=X>uAR8f`qF48MZMfP@79>xQ!mO=$-(p7O0`nc@^zkS9scRqf@(9_lb2r3#+Q zm&_a=JI;GyYqA&D%i{535rs}&6WR^&AB0$-hT9(u?+%2UyC7BT1AjtEYnvh^BTqT} z-687Va4m=$pYAdO_9SV147Zz~-)h%Z_f7-yyBdZE697`jWG;zlDzJ(H9_GLig07cSPn>-!s2CzL(=8`pMIzA| zgyv&kq_`t2GT@watc3xR>-HdV#z$P-x-BBShKbZUI>4c7ZN{?s>FGYmV3Dk^e&l9j zf?Vm`By*WQ4Ln+e62q>rxESN)Jlst=)td=Fs=(#>k_ zXIB(|c6v?}^U($D_c?pw>w)6%bw{7;qe$M}3GH_)<8A>!Fpkq%c1-Xq=STDLpDz6U zd5?TlL7oA_2L;>j(k7ylR@V0mPsP`JC5}Osi(Mr))=&W*!+5gO&<00(jsY%?3HmFe zu#gNA$Hk2_UdSIl^r9$@FnZ>L-}do@_R38N`le9LtzFo>8+-ZHhJR4(g6!3D7|-En zGmF^gsVHcR#pEJ0o_f8sfW#M(>(Mf(XoMIKWV$deqJs#Y5i-$>=bWRDur8rx`|r>? z5)3j$452WWZu35(@F@M#lHme|XsT0fviYe_y$_n0 zd-d!~W^ShHkEd+^GF)WC&nFDKjZ_amib*ooFCt&2; zMKW?Tt29c4`eK+}Z|!sL*9D~#^PXsbfi&K1k&999TgeDAjCj4ab!U7u<09pVQ`rnb za&auCaek=y9^3OzwrT>sGz-RRA8*sR6iD0rFUPV;c+h0Ob?Di2hI7}SWi%YzX2sI0 z&Q$I=K*>Z4l0rnOOR&`olxH(C zAyNGMsrK1~%DphmWAqyUPdP3&(Xxqj@a3R17#UD*$?Z!v)X@IcPwcLVks~ zTr@t7FPw$!O3=x2qhi#0BPo-KiGwJu8z7B;LrEgVvpn3_-(o%l}Q!OWS z$lif6TutbAKIU^G<68xwuMA6M-Ufn_K6L6Bhb9Sq@U>v`(T9+j5R*OLDs=jG!zS9` zlKD$KU5%+QbI@jSQ*eh4-q7r4_X5^N0xu$S5>h!4ddoNBxM|w6m3z!$k}}qjCT5Om zV(I&SFa{N3ERD@hoBU6oScZ)*OQ=|Wbp{3mt8yV}csa$eS>IW8e9p7EZ$)dWTB zI+LV$1euyKp{zLLo|@u#%SIH(cuRaCTP|lcj?!#5Tb**5PX`cQxE=_%vMQzb0Q;VH zhVv>c!ud{~$7@)iLOlDlfRs36S9?P4No07YhB5hRZf|IG_Tt0P?70G8yJNwwRrC;YtpqY{)0>J_{oh)W{Mo|ndjW< zRCcU@yrDs2N4OTi-xg9+Nugw!@6hdyJ z#KkDQW<#j_GaTEYy7U_?nu>;=U3VvdT-8K*&hG@`IP^ASmARIdJ0(}9>zA};TBYlL zFM>NKFxyIXu{DKyd+~6}=pEv2<3wFF8U^>|~n$2-nqa* zz1bV&T~Fp&K5oeY_e}-8J+2*4KnD%p0f*eC2w%*Yz=R;+!Rt2{pMubC>>sjx7f+^_ zg24z`Mz-X>x5P}C^N_kBwUEKE+$EejPI##v(_#~~GL7GIWTfvC*CqFCqkL?L;ejEK zWD?Yq+UVj%WsupTQ088Cez`9&%o)@zZ$ynwzkH3uXmsNBSzM^+j$2%Eu8GpVxIMLP$91)5BpB=dBYGL z?xMe@+guqr={jO1J|ixk~S55k)fE5V8ggJ*Zs#H>Y2U*53@cn(d{?^8ELiVImh}rA z@iPozGV?!__@nC12r2YTy4q++?LJE9w`~N6Jc?#Q#0tS`qfL`&y7#cWvSGu>vl1O!4ud76yq;QbacY|yk#K<)M)E4ZwMD+sRB%M44Y-f&$ z=mBD%LBiMiyir-lL#1k^-k9|fEx@HA?-WdY0d>e|4MlMtj#Ks~P~6jI(XVUrkB_|| zO0RB-R`$a5V$_M)ji-)kK@MeKz!J~(^{L>RaJs0&24m~d55Muig$AePD-DBt4W^N# z4vw^wAMfZgSh)Tj*(e0_-O<&)PSoBe^I7*}=$aTz!8|m&md`Xqj^}?XVT{;>BCl}! zM{{~Dolw_Vy+4tOubxpHoyxt>EGqE2@$=ctAZydZ5n|s$R-~s<7n<`q3X9F zYE-mT7ToBTOIKtzKS7g1=pV)kCw!tN=7{5Ebk@j z){?VMQ~`fp;bWm~dQV8DHr?OjF%ij8H`e2x2aCpA%ddn!KPRdgvUV9HVNUp_oojiW z$PUJ(SdC)zSOMlm=kOpXHStq4wn~%gDHI}Io);cczBm-o;VO0KaSkp0acZ+tFH@G3 zy??Ol%1~}oIEMNO%y0!l!o5Up@2*N>Q?P^1$-u3@U+1$ z*z6&7%}p?^7eO}pto>#AW?G1Q8kLtroqD-b=58&C6rBf5nN?nPr@C0i?V5J>)l+RR zSzZVLlM`}w`GZ#?im2%D!&6b?J7hnhcL-yB9oNidBx(_8TRF_@ST55VL8hDDaKK;@#^g`+#Qn9{ zK>`L(e&lWJkH_ZD!(j$tS4BH$3I!h3P9V8-&GeGanBaAa$6At@8*6W^TEhJnmSVl|E%Z zEk0M%om-}L`88s+^G_6DY_Xx+z{mUIaH6<%np*O-`#h@oH7QVKu|F7aZtNZZ_=sMG zO75(s}c zyJN{F%7LHO+_`#7VEN5x2EURsDC$(aGH&5EfCxYg<%90E!iXJ%$@nN_aXOnO+v&J8 zEa+xLNrBK!0V74ywlIgkYh!v^h}zDh%ux^CBAouROtS+M$CU-kk(F&Hr!J61TmvZm z>c9o?)RxgEa!w&&Z%wYa=7e2yS;cP!S~c|fNIk`UW#Zd$s`zX)TI>hhuZfh*`OvRo>YB+^4;nI>{p3HTzCTrcBPs82y0z(z>X}ZUso^=S3vM40*7iL# z-0O`QaElU}mK&KmSz^>0Cn@yflOf$~dUBv4%U#xxB_<8kpB&oCD4u9cEZy!L`vC{z zj%cji7;a{(brLeI-~O|#mQ4`$7xyR*GqRw5#gViok?qtCqG~#h+{OF{)l{CIg{LEZ zYUlYnHc^~q4I2O5V>w@snw4PmJxx@-loQ8g3xK3f7T;GY@oRbyF$2Yv23cy{vqSbO zknqI&8%si$*=mi>l34g^vR=1MBd87GF1=>|?KJW-5nxB|+Sa6jt81YrjCiIlY|T**wadfFHw2JDgGS*PRc|}?b~p#a zW`hwzglV=n$f8QMQ^E5D9mIm;Zm|q0XA8FskaCH4O-n&jPh)-{-v>SOo3+V#5ZOhp zVsGz@Gwf(E#ec?jMu74l0e4Kbm91maDd-}_-=!CSP5!CHGfHGi(Lk(bN(IgGus6=} z=4v5l-zoAd*-UV=N@Y+=S*9I(#)lQN+jBO}$BjIx$7`dPwuu$kWRx{6W?KWvqFhO; zS^;YEM%De1f}k4W`8Bj-&4gl_0LNU(+eX&4OTVYe?G35|l}>XYf;Rfc<&Ni#0SuiV zHIgq+F9+Yn%AIiqM*S)6ZbYb5MT5TcA>z&NRz1Fk?WSeuQK5y`uZ;H04I?#%5VAB~ zpGCL!E7sMNwXh!7E&dL;yT6l8E)iZ0{W61FKPHpd$SO5ub_4jE@EOsZuPILzGbmOl z18xWbR(i2TyQI~v&*xOA3A$~-JL0@x7dc&B$T5-)t0icIo${t}}ce zs2BLv+@mOm3S~Q1KrbNvhSZllhTuM?+mPp~H)&>v7o$`NKc$5eseNXK;57~*Yl13ZoVAkE6X?94vjkFH{!%5fLg{|RKzoKzosDyaUaFkn z#hBFQaFN7o8>>kOypy`B(zEE{zq>bIdS<49H^Il{_qWVU7ryf#ov0R&JN3RRnZMig zTb;TdA$PYorK4ZS7~0KGS-m!YFLOLD9lePTBf!H}i-#U!cS8@lKSB^)e>UytW6oZs zI&{a+-gpad|FCjqfR)ZusW|X?kRRl?TI93=iTy+noom7IEA0A9kCP}2AvT%jD&~lHJQhj(p@RtW(C2jQ%Ww{KDT-1Qa-^|wQt5B(@$+9&!g77m^zL%<* zcPnf2YL%BGPjN?!O%&J@r=_TRGIEUqEWvFMAAO4=)7gu;eapN81IlBl?lfHiu#VQc)q(D~K_5XsdA8-wS`6@NsHA@x(OHs_yPcTzrBZwF z%)a0U#ju0ZTz35#aZ^`??9<7xjsea_!vftiw3U@=vo;;~{N0Nu;0%2OW2e)e$ns0A zL(+EvVYD0BSmqzo1O;3XY9-@{7*ATa9uiVJH39Kn2mBzSfU}_UVN@5dX}yz-#UqtE zflaqgTNGJ}`FDsm_wF90(4gnr!#W>QxUN>FcMzg=eZr_Ff}s@Lm}=XvVG;~$5-=WS zdxR9sJC<$0P*?Ko0Zt_0qQgK>f@iK2VOv%?_qNfRPu;SL>u|k)*1?mw;VL`2#WV@e$yb9k*KUH@LAUR= z^Gxx=W{>$)&61puQ!yo{CXs-4M40yQVi&V|7IA7L?CgjkvP2%YmBH>c|899+&q2-^`?hC$ z^c!2xJ5WX``5?9>jjVRrOq7C>o!BzVNlb}}{u+nF-6Vrm^bfy|SCka(Fmsb=d3R6^n!DDX72BpcLK)f{Oj9%E7>?)c zqNju~XD>=J6xY7{Mr%MJRxt7xINc^9@DSUl zj`Zit-4!E8q60R$#9^{Sm)(C=X;q9$9VRuG@!nL$ctb8;Bibjc^Q4d<{#%y)tFkI^ zt{0E>ahXN|d&;`*A-~HlV5Do@9{kT#>)chav}j1Ig{pH(pUfa zF95r3dH2M2RzIk`)LVp*?f-!fUv@ z*O}Y-+-#9!RTsoNSCLEL(D2=Pq`!y>Xlk>p;z8^4WX8X3W}mn6hGrXG zNIcGBS0c1Zgq1Ii1~N|FA>O2Vo2G2OPY*u3N(xU6Y*BJls;(lzms2UeVFO`aw&20f z$mCnA5wub|^N|F{0j{`l==h?aTz@{xm_ji-Neu`j?W1br4~mti%vRv}W4rRm6BO^y zZ`p?tL<5JL80wz`rnH58w~h)vH9G$+XxjQACb6}na}wY+W~@^EqCR=9aQXT94wMLb zKa-i!;*wh7w-VkJfN8L{7XtGEdJEB;_dH?mEp$7he@eL(#21cuKo%xT2fjFxAmK z^v8LIGes_L7TOD{a#i6q*S1cq7zg4w*#F1bTZdKMZd=2!1YLx53Ift0ts)B2paK$- z(j_7w0#ef50@9*@ln6*kcb9}x(p}OGlJ8v4-tK*_@0{m-_xb*KynH0qTKDh1=RL-l zbB-z2>$u%6vv)j}T16n(H`qW7x%Q@zV@J`dsXw-(`$3-S6JqX&FfeXM*B*cxTEq0qW# z+~MG-_|3`S*EqR;UM|Canml35iLb84OG*$f(Cd|LLy0wIBex^3*ZSvAG?A=>^_NNq zvTt4yc#*YHz6Dx*#FkJ*fPRT5?9fe)u0_m2MzEm$3aShE$Q0eaOQJyybNDL9- z>@!xA4s1B@xjk#~h4s$CSfVC^uF+xreSIF!1l5ga8iqIV*64CAjRUXy3WO-l1hOQ) zV^XVf)t5YICQn+f>5b+d-n?mt;XTb`m@O@oGwwL6TA|mf=;II)^6fUhku&RG%1&g- zFaOTsm95F?2lQoutvAP;f@_GqFPD0`UNO?Hk|;5coi4yLNlQ;hM z+iCv#A}_u?>tm~#eG|cNY*h)Em@Pwinr*8Et9;!^DAwB2)z*t+@@A_s;Ng|g+U+N% z!ryI0e|l?TRUyH4H13C5tXwTcYiXO&UxNLa(DAo~ZrjKVvuta#`1}%^xZ84$P(_^` zbNQS{E1IWWzl4?7I=;+umV-Fg<@fe$4u)Jx)BM6w1-Peta55~{FNHPjEu|{_vVTK( z(N8VJbts0P8)>CNfq(Hbj=Yohp-uR~P)EJv4y;q(@gVIhML%?8VI#OJ^| zE~$9^im##?j|KUsbw`hybz`ln={y%Fk8i8Bn2oCT<3uL zEB279@w?s%3m2LA&lp6wY-;6e<%E_Bhim-x!YS~J&1~aTFEu##n~m5(-fM!@snlvR zq}?_44w2lbyXA5i$T>ngJDo;dt8Lx!vyGx`U#r~vm0Hb&UGsDomn;itPCWTYDRd2R>Qd7c*yGf~`h>!{2H74GR z0@egPaV3`(5-gJL3teW(-C-&@8__${ztzQ0Rr`GGwh^JsrfV)Gx7co=+pJ`XaP3yh zXif#esSVe8yp5jCg9FKqkjFhjU$X}%t@{SuN9JfErRmW(FpSEQ+!NnF;BJg-p)lfE zkGtpkZqPaPSo+{ir&6t(gr$rAOhfQR>0@{^nU|quG9}NniC5x-^Sap`Dy;L{PmJROU&XmKpf9PEhIwvk~ zZs+ct$*5Q633$FAfn|Gp;4J z=3_g96sehd6YlYpV)pL_cadAz~5v}~PEnBJ!Krk;I1 z%=;_Ni&w+TGGz(dO@w}i>TBuU{v6F(=#)C7HBDB zKD;*>z47Q2n}OI}TaRpC_tA#${5L--m3O;V8ikh*#F$;EY3tiCFG%5{7etNsjjlK3 z@N9jO(mi!`%Hp9CK2o*1*(f+s8hmib|7xi((QpnUtX6f)l;g`{$yq#SGa2WTaUPo+R(SUpoTIsP9v{BGV}SH24+d2S5$XMB)J_gimdH9 z`lw19xH@Z(A~U{aC9*wce%;krE;XIrWyC;eZ%&Q4fpjgs~uHER>n8Rf(t9y?w43gYaD-_ zHe12*c7m2-@@m&=?%i$mYlp0Tdr;pm)Fb~v{J2{VJ+dl)t4{ybuc6$;jdJ34Y8p-0rPjmT$!|CHcExhCOcO4VOe~RxnBtAO%lsHBNOGJ#s;=HL|C&j6 zbOt>QEA73-Rj=a=Zc|g;4)niukEq4j;s=8%|1EPRCNlHQ{7j*U1@Y57rq7I2T_(ITnbr1sY~T&I=2pOXTm^kEy-2Yv!`?oiL&SDsDx8f_b)i^P* zr1>5Fx}EWcPb@BTQNvt({_WIt-*i%DXQ^u1svi5i>qOaVoaU!#w0ecc1qV6rZZ%=o ze6!OSxg``Cu{L}!{i(dr8`>)|E;AEw0@p$}1hf|YrwAbuTi>aaY~W?N`MBha|E>*wA;ts|4-a4eU;IH}K$3;HyTODwa?WnXF)n8U0MBEPto@G3r9p{{5$`L^KrdOxI@ zK*=ddz$$U^i~m~YT}jLb-%iV{o-Uirw#bV+8H|m|+Rulmko1DQbTS(A^ETx-A=!B$ z?=Q5W3B)PaH@W$_-;SR!F>FY=el7bQTn4^0^a&?q>fGK=q3^O;DRJYL&98g^mLiPY z5p$@o&)4WoPZ&Czp|gs1A&F49*j-<5%Tx8Gs($lxS<6u%DVfq-5G}&uk?)W`P28c? z!1JW3wX|IC-=C^99IuE|98ej)6ghl@W&VZ>NTGqUNr%bg_tIC7+@w#3MRq9#EcaMC>bfQ4lAMCtQw2xzd8WN&Qu~n;izB{a zb-`N*5-BlU%SlG|M6XwqgCeOye7Za-9nlFV2nI1XgSqW$3sl;UHw~LVgI=Wmh&ZP8 z#DzQ8BEzMm#ydK(gy(Zz-QdJyC8)eGpw_K2=f8gQwy_2J4Jqg0%EOuS;2aq_wdQs` zYsDBG6Pe@%RdZ+^hbAMIL52r;=hPB5qFt`-Cu@7%ogyaSEF*6L; z#G2Q{%`Nq_HQLhA@ILK&_qqCfdYD}#GBnI*i$=>S{OjrKRQ*Ghj-lK=y0}Z{c7;`p zn!57}-`r`Mt>h>Y#>iL0mcGus(q-1QZTjxG(5Xk7O`qhtuWDN;eI2H`Tz{}%O=R{G*Zd)#{46Gh6#J~+`iG?0!^?=@3=r92+q% ze4*Z)&y^?Yqud+tFJbO;EpHP<^m5NpO9YA$ASLR2tsWfXNN`!L!`&o(z~3BXV!?1F zCRoLtTkZ3N`#D`T?cLGay*<-G!dDZq8ao)kvC5cGKOxUF(MjA&czJSxZ`d(N*%6lo zyhb#mg07p8qKvctve5HHgm|<$Q_?Dc&CW}vn4G^{dcySx>%`xZYMg+JO_U9J@9*L< zDPBEmuCQQw8maWovSv~u3{g6fpT1x$HEW4k#I-9?fc^VZ(o_F?kqLqlqDQ{(r&+9U z5K73hO-?g+rKR~Z*Q(f&qxe@YdAT~y>~0o!qn`JCDi2FZ(-s5mjCv(|G?&`JQUmj=WflsP5d8T_y76xB+oI# zLwUhVM=2AB)0Y?={+?kB1(#_QEG?d~Fv())ZO&d>gx?Z=jEv&cQae38m>l#0dG$*W zIX zZI&+f9^P*}*G$y6&(R>1z*Flf({lgkRJ9z9cg}*5Q7Fqcy=(av!-kl&`Gf{BoLYuB z%srT>{dk`Wksx5D4F9P>TVQd9QmV9Fu}gWIfLb#4Gqj`B3cEE|_3;Y3>Kb!vbk20+ zN;KO%O;DzW#aFQTope!ljW+W^ zavR@#>B6rgmg-;uEL}tQ-@IpGg#A|TEjp$x)vixQA-6GBE`_oj7)X22H)T{#hBAc= zLW}ukcwN}~OA0G0k5K<+9r!v8j1<>8yZ+D}tWpEJMb%tw zvCyl>fzN*!(waMhCt(?V-9rOrD$~w5lzHL$?g*hw(PMA{czE??-8JlQPS?beSsC)J zXQlTV@h3AQAKmH3@{_=~OqC9y_*`P~TNeD79v|7U^eYoTJKnckZl$eNcew!O1z)r_ zvY{i=X-(s;Qsb)0<|zP*!FGMP?$OFJ9h%`Q-Cxp$U$H-<3SKqhHQFj;S?^ zAL`aWbTiQsNfW#jLvuTa*UU$J}Yybgw{xdf%Idt!JZ0?_`jINK z>)2nP_#)-uOwD^Ff(w~^L95C!;z`-dlI8EytScOjBBRb5G0X7!YJk5yBFPoZ)4-mM z07INGze|;5M|;3HUaYy-u_NkpqK&hs+6$kS>jJQ)lNGNiX2=ODV14~0uDP&W6EyJN zPyJ-?=YqJeKZ!K_{I&oB_X*hK$=67>ERKVX(m%LrY8~8!`C#t&&>p%|I~3P_Bq?w0-IIg=B4Y! z28~DOFFNAs6fp=hl}*$K3QcUm;*nvo_x$X5gGS_hcxUx@r5yI%)R-L)QVz9kT)G5D z=}?Nf${(=jsse-BC)oEU2lm&r#&zE+n#m-K`(jVf=d>igb+ck=yu#=6)_Qx6Z9=k6 z*(&9Q?8pu)77}sM6WVsN0X4j?eYy~{S7yKVMEFb#{0T}o6{{jN(Ie4($vg{9zB%D+ zTydOTaDC4L55;SeTO(g_!Jq!QG-PUuKMlI>o6Q@~8K!7fITjqv$NqXgkfpj1Lyd{f zf&ssNZJ>DMdF|=GEngawa)$dSg``|~!PHwXPWsrcVxdk~JA-~gHu7f6{d<&7srhs~ zMlHBq$&pGqBr%1uf~e@@Kp1z^JZ4|{dzNbUn7oEEF-8y?!U#-<{Au0F#M`+Tk;den z<+h7i;H1*W&-3iVIP09S7~LepCe(5-J(w|iBgj0tsLH3|NN_mijo)9;O0!~dU&HaC z?xrl?i?Q21bYMtXJ@_y|G-wj+VLTHJ2B>Z_QxLQds?eyDvs4JV(SDz5 z%^#fbe(Y4EZKp7qKj3?C#>e0>UAOcjRrS-O4}2ow9Ae4HYx#!F*f*^vga$xdq*qyP z(CeS^tqo9aFecC_eevN*t*+VvlXtui-0PmJ*U)MW5Rxc!Q{}L_w@k7{1&^1FVkm-% z5EO>sl$pUws@d@bgHZn%Jn3Rc>)*;o>=1cM1(P{t7d4x>>AHEZW`x?j`bM^ejLb2S4*#{wub`S0$vyhO$n59v(}CeQ3|`h1k@4VgBY(f6F9BcR)}_d{vju z7n>Ym7@|~G^C=3o$NxZ$|Jby5n*uK9wpo0-_WysLf#N97z@GAVU*<{4TGYg zhm6zvOAqeRUi-^<_jtWB+h)FFK2@7EH>dO6P1#v3xOLmS3as|P_wlQXe>}fw7Q1Rz z=1gMdM6>T>rL*YgS7r)u-89#gF3jH;&6tF1p~bs|-F9v{~ThnX0b9}nXXPIOWLW3%rl4xvb#()!I%{v` zXKRgfzmm-7V)LKu?$ia`w}n=F(}VBKM?LP(R7wT=!qEP9mZTi8-sL`hnsm>#U(@tM zdT!0p^MS9X3SZ(X!1i_k5YXPv`gkQ=i|6+uI^e+Njg(ksgWGOYS?oCR<3T;Q-%>h` z)4rN1*7bE^RxwDMX=&Y0NjOp=atI@8=3rk`nAk)1f6-EJXB1nu=6ud;s2@0^0t?!S z)W4x!X}{K>Ey<>JPcm7z`ZXV0G=6QcYz$9YL^B81{ReS$2syeV@)C@sZ^d+q8iXhZ zW{PX1gJB8Z<_){O%5gq6go>UX?cq3}r@!*@z(j9>(zgD5pSP`;mr=0vnV#IX7AxUU ziDkmRp#io7#1OU+iev?mQ<$G|=3Z zNUm}0l6g&WlGiWd$qKub6EUL2IWw@s3^W(~U%7O9o&=%*bA>Kr-nl5)ykm!%@<=DmH4i65i+l$pitX;AGhHcJbue> z-8cK0`=P}nf1_&W4W551aSfIqa}?nPvP_4K3Gf-?>N6PuyN!WHe36`uOSKzdcWGAv z*G|mv8`O0#YOY-uHxE#JL1`h^v1ze0nc-8}9B4?Ryr+ezvm`LaQp)|sG zh@UXyX7GR4;vcP$xy@xc*oxU&&dz&&;eyapm%Rb? zk4pMN9%7eTF3c5xVSB+|8r%sTwY5-ZP*qMVZ_9u=Bci2_^ zW}iM}Y|NTmxPY_$rU2tm=c@Tg^*N&hCf|p%UQYmF93PkELE@`U^CPZ(L(U0?pE(CM~{drPL_ik-(3k zaVP-9=)DxZ)uK7ZPz_aeWjI*a6KWI~`Zn}cDo+3N=I^vNBY5*OPcLcKkrCm5(GkTq zOLI6G{?)W+P#OoxYVpB(!fCO&rGAhZtl6>*v%`UeF6VC4+99y)Z5d;qQCTTstzLn zMYGdr&`GKtc~KX<7g}Oj=l(OnK$!_rjBS3zW(Q!r`NiuGC$O&}LRes|fF@E_?J(HK z=Em(9j+68lq6`E&*^{_6y~l(}P1lelBp4cLj7EK4+r`@s_oTw&YHJm;Z2ViF|3&=j z-YfFiUYDZG_ENEjd+NTP?Di8zLVd3!OZJndW#K9g{UHb;;GREne!EX_159zGz&A|> z%e^JNhN+^EeTJ4tW)VubU@`^9XsQ53(qHfXkRcs?ySXx1Tf2|uX)Osd zL_dVR9ARB;m#t>a{EGb%bJewx;sFEys~K?1C5Iw;-s$5IN>K1daqH=xuUD*a-GrE3 zO&Z{|F0%c8fR6gqr!>f)=JkDG4cb-%Tf?SFW=4_59m`=odt+f6@V{RsTz2F`TN9nv zKNkqjg-aAlHrsPL;?`?;InX#xp<`FI58`QFk^V;<%CmkDkY_-W}174%cD_$xW+q3@{Dp~kr zj3B4%v9CwVJ!U!EZk|$8(DnMmNd)TZaV#jYjamdON761|<}wvNZ6O_NCGfW7 zIx&zNDr2DG-E{LL6s4Y)j(xBXA(0succ#JgU|7%nH2rX`Wb{QKtFr2#q>m@DNR0)) zm%|&FqLBq7CN7DeIXt*MCIw7$r8Z%LGLie`-H>aDi|mRA>*K{gT2SCZI9MfDTXRyt z{cP77E381)wWm=U=5fe{BOIFIFa@IeWvaYATETniGuT#zNNR%nw{tP1LI-knDw~8+ zc*wJ#ZWBCymcOK*XkAZdej1q0dX2}VgQQv>%gKfm;=R2q{ zz_2pw_3l!HSwc%ydtlnT>jTKKJK zi$MCGTVq0sPgs;H!F?8lY|M2fz_YC1rFDKa$;{YG=mYe67pDH@$-o*W788<06oUs> z=j+qZeGGN9uiTnk$w+Yd)%Yp+^@9;Qr*uT;ts|n4l`i`zDIyHES0w}*#m}R4^OZh| z`&v|Ub7LVeNzuvPqK1Wl8eWl1Cbs=Sfk6KneQ=FN2&;-Cp+>&Gr*4{tqbw^EF~$s` z{ztx}+q)IONl?&QXp+x_$?0_O=K$QoafOGB^uTKwJiKTnd{Q#_ zOZ1xo@%+q&I=-q*^~-Fv|M4aky(WcXpuha14gYej506FS(%#bsGeGYkOLaQ6uxZRx zm;(XxT6CqJ@BM;DrvjoT@#x^(yhHnok+PTPz>WsQE?rGiFN%Y*RB#4Otufa60A#&$ zz-ecpiL<-0VLAd~#b`i+d;aTF68nBFUN77tDE62Jt-@&#Fa?2}KP(1yYY$)>vlku( zk0`jdn+D}u$Tl9hEySA=^>ktRQOM9ZrR~S{RytVO-u?DN?-tElHww%~Utg>V+fWYd zFnt%B|8y*YCOm#~IvP2nAYTc@9 zdT!Ajj5{QafZ6CCYHc8GZJG6?_<4G6*$lQTzPI;_@ruu&W~{sn6WR`_IiL2Y%>th+ ziL17qydzrenB>~Yp&Sr(GkEURIWjNR8^Nybs3my0}Bt78-*Wccb9R7JVGA6XD2wHavK0E;KRV0QKgQ$^?Cvx%<0vx|i4C*$L zkK+N&`mUPY>JTsCO^qv!$(d!~2ADC6Ml9s={k(ZPBA^QGeF!B#b8mP$nz3GCoB~1I zU5DIvd?pX?wTBu@5ZuQ{xPwpb(M&LRFL8MjJ5rKv6K>n3FJtsH-_~40>dLHVi-g{V zePz}B_6g|%Yw{eLGJoH2c*`k9=ZbVW`@M)g?gjGRV+^1&xlg;_$M-#(JB1$v!7z2= z^~x8Q_Y&>z=)&DFG&?&maYYzm@ssHDV@mTVFdTFh?dWwUX5tAp;VSoULGPRI0?ms7j-2MBLpNIF?kck zQf3h6-?%E}ocHSDFkNOgd92BO7=m;~XfUt!ylp-4uLFYp8CYZFIUlni5scbGtFnPT z!xo~HLWHr3q?@D=Y^Hf4KF5Ng_=dg|8w=4+@1gV?He#f!d;<;wlLq}21>Y^s%_Lqu zl4RCp!X3=b^7mM~u5hI)NqhYEhQj%3RtZJcZ(J=DzT00Pe*C8*xy^xa)c4qY<4GtL zzNxSD>Csh7R!{#BP~|@7)f}-Cu0oVjgMkssIhjp;QZO_FHNaz6?HZ?6i3;i(b~9c2 z{&5+G4m^fH{Ilhvqz5mCG(UkZBMQIw%%1T~h&p*iAbvMHGIo0_!!d+`9ecz|ZyiFI zc{0j28*oU~)|HGqTF|H(k-gyD+)A3gLnz+f3feDBdP>YjP21SaupTn-my-Il(c0|m zi6u>OdVKIR&H$GIrF8#4GmWTC#!kQ*A9SA)@84R%ONnH;c)kL3xPQKsvhrTMOs#Nt zt*GfPg`sFp$0+#r5+4-2Yi7NWiiu0odF*<;uj;&39NQq_>l7P%%VH?mk>w4F7oj9I znCBrn@cA*zDFatYiqm#Gi-cY?CfjHLh2b%AReCs!kd@G0|6>%MzZu4VRsO6kfwzPQ zorfBLQ;^|Z>aX&_PGt7R^TS+~t0~_oNy+_qFVVW>H=r%NGtd2D;tC#SjVY1N|HpJ~kKD^*@V~M&k*tBcH**d?{c{qn?Zr2BWE?NZ*rkn{*)1E-{i$Eov-U zr7s4wUQt6ra6~i}sKkgJU(fJUj2<%h`@n`fh)_Y6m*`6P>%Su!Gz@^m9F3i~mZ$=M zk(;}mM^`MJyW+?<%_b@lsSn2pYLF0#68ZP@ z`@bs^)C2@Q6nJ!`>2x6Ih1p%xi^>l#1ZW`ony{|XYc=|V=N2S@@_={V82|ymvh5{= z-)zv3*+K}1{EX+?A~f$^9DScZe;;65G0Wu=bhG}0ryU7%nW2PfLrY$AF<-$mS9k z2pxq;P@|-yx%KW1|75J9Q=B5ycL}XxVnoi7pzq$ho=M1Fh1_)A>f`{K!UW^H6-(*` zt_}^-v_kS7X5$nAEAIPD5moyz)5 zuN0%$dr72pZ~`7GHE~HPirEhoPv;u8s;l%D=jYNRQVHAQo z-PzUEJ?mNTdg^D49ofz(P`kK8`F3pVIBKH{m%p54(YPN*=b-uwZN#W1++^NRTMyqX z(m|(q-r>XhdiNRBiifXMf}Sw3zOF`oytkmADPlo- z$X&$3L|R71f_TrXKo56J={MA7 z>{^Aki;qh0n}{os5t@8+T>9vrqf=>rrX$25fm*D;R_y=#2i+t1wE>aDq+iT{B=x_c z5q^I8%8h`*@DOh_1Qt6gok+!_@4=~8BP52YaSOr1m3*ED&vxaG;%CyE(P~PR??5;j z1`#|5lt;>#Bwwd_r!}0{8?MhgMmyBgHj>NBaA#!}iOcKaGIcNcMufgkeh7vpte!6y#$o zNPun4A8DxUVvup`TCk^5WnozEKCQoB&X&BQoGKGm0+Qpx0?%%!s2q;PP=38yp6*)Z z>L70@jc+??NCO2fp$r1>eQ@e9pDG4HfGPTE)er8U;3H@z#O+@; zH?XQ?hLdt>4^>lIgZ@1VZLTW|k?-GIT*9NKqhMGzZPmNLehedDv`&jz)x1UTpr7Vw z*Sfh@d=ZkQbHIcto#y`dwB6%kOTy|Hy9>{{-rclKx4N@eTDQ=Z@a5I7-ZNdsYzaCa z(T5L%ZdwfSH1S)A(>Wtv(<;S_nVpf{fB10w0HJvm5szFQ{@Mzuh`4rLp{rI145Ow| zN~4;89+mKX1a{N+<}E0*Xve#H#~9pb3!osjFG~$fqM5H#|J}n!^VA@LPeIm$Z7cp~ zhj?MUp%yfR30S=2?9Jvc$wHFH$8=nK)#7>n|-

)t%L@}n&IB8pW-O$F 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 literal 0 HcmV?d00001 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 index 0516352373..d8911abfd1 100644 --- 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 @@ -37,13 +37,28 @@ public AdotAwsSdkInstrumentationModule() { @Override public int order() { // Ensure this runs after OTel (> 0) - return 99; + 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.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"); } /** 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 index 0775775edd..8e0478ef5a 100644 --- 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 @@ -15,24 +15,138 @@ 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): + * + *

+ * + *

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 { - // 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. + 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) {} + Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { - // 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. + 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/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 + } +} From 3c4ce6fc39e2ac071319852e01f76f07811477d6 Mon Sep 17 00:00:00 2001 From: Anahat Date: Thu, 24 Jul 2025 13:21:42 -0700 Subject: [PATCH 4/6] AWS SDK v1.11 Patch Migration (#1117) Note: This is a continuation of https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1115 ### Description of Changes This implementation builds on the foundation established in PR https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1115, transforming the structural setup into a fully functional SPI-based solution that will replace our current patching approach. This PR does not change the current ADOT functionality because patches have not been removed. The next/final PR for v1.11 will remove the patches for aws-sdk-1.11 and have unit tests to ensure correct SPI functionality and behaviour. The final PR will also pass all the contract-tests once patches are removed. #### Changes include: - Migration of patched files into proper package structure: NOTE: We are not copying entire files from upstream. Instead, we only migrated the new components that were added by our patches and the methods that use these AWS-specific components. I deliberately removed any code that was untouched by our patches to avoid duplicating upstream instrumentation code. This selective migration ensures we maintain only our AWS-specific additions while letting OTel handle its base functionality. - `AwsBedrockResourceType` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L224) creates new class - `AwsExperimentalAttributes` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L362) on [this otel file](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/release/v2.11.x/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsExperimentalAttributes.java) - `AwsSdkExperimentalAttributesExtractor` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L408) on [this otel file](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/release/v2.11.x/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsSdkExperimentalAttributesExtractor.java) - `BedrockJsonParser` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L655) creates new class - `RequestAccess` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L927) on [this otel file](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/release/v2.11.x/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/RequestAccess.java) - `BedrockJsonParserTest` - [patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch#L1461) creates new class These added files: - Access and modify span attributes - Provide consistent formatting tools for span attributes - Can be updated at our convenience if needed ### Testing - Existing functionality verified - Contract tests passing - Build successful ### Related - Skeleton PR for aws-sdk v1.11: https://github.com/aws-observability/aws-otel-java-instrumentation/pull/1115 - Replaces patch: [current patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch) By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Thomas Pierce --- instrumentation/aws-sdk/README.md | 53 +- instrumentation/aws-sdk/build.gradle.kts | 3 + .../aws-sdk/sequence-diagram-1.11.png | Bin 0 -> 76761 bytes .../AdotAwsSdkInstrumentationModule.java | 13 +- .../AdotAwsSdkTracingRequestHandler.java | 68 ++- .../awssdk_v1_11/AwsBedrockResourceType.java | 143 +++++ .../AwsExperimentalAttributes.java | 70 +++ ...AwsSdkExperimentalAttributesExtractor.java | 243 +++++++++ .../awssdk_v1_11/BedrockJsonParser.java | 277 ++++++++++ .../awssdk_v1_11/RequestAccess.java | 508 ++++++++++++++++++ .../awssdk_v1_11/BedrockJsonParserTest.groovy | 117 ++++ 11 files changed, 1489 insertions(+), 6 deletions(-) create mode 100644 instrumentation/aws-sdk/sequence-diagram-1.11.png create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsBedrockResourceType.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsExperimentalAttributes.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsSdkExperimentalAttributesExtractor.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/BedrockJsonParser.java create mode 100644 instrumentation/aws-sdk/src/main/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/RequestAccess.java create mode 100644 instrumentation/aws-sdk/src/test/groovy/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/BedrockJsonParserTest.groovy diff --git a/instrumentation/aws-sdk/README.md b/instrumentation/aws-sdk/README.md index 9d2bffb8b7..1b4d677d3e 100644 --- a/instrumentation/aws-sdk/README.md +++ b/instrumentation/aws-sdk/README.md @@ -27,6 +27,22 @@ The aws-sdk instrumentation is an SPI-based implementation that extends the upst - 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`. @@ -61,6 +77,28 @@ _**Important Notes:**_ - 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** @@ -87,7 +125,6 @@ _**Important Notes:**_ `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:_ @@ -111,4 +148,16 @@ _Class Functionalities:_ - 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 \ No newline at end of file + - 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 index e2cd769150..5863df2a10 100644 --- a/instrumentation/aws-sdk/build.gradle.kts +++ b/instrumentation/aws-sdk/build.gradle.kts @@ -22,6 +22,7 @@ plugins { 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") @@ -37,4 +38,6 @@ dependencies { 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 0000000000000000000000000000000000000000..891b07f384fede3fb5307168107841df716ed65f GIT binary patch literal 76761 zcmc$`cT`jB_AZQvEuwzu;zDsR5abiGo)xDch&a^4l1))9UBh0gkWZS(g}S*~9ecy~+rGFueOldJ46PoKN` zJOA;*Bg>RroDkaO~p>w`4BCI;vUMQ zQcJ61G`X01_B+s)f4}nFZk#*y??d3zA45m*Tz~&2FXqEC>)*fdbVdsyXv5fGIoxg) zZE;bLz4pmmZ|>4ixg9_MQA1_lk?)bhOI+s%ZEdSupOKn_VQ)H;Wt`f^Ti5%I`ItKF zoTjaNo1}p2FI~45C;a=~>hdwQ+4Dm^r`luuNp*e!eaS!ewkCrg&G(d#h-uRcL)PXR zm3)Rij*52`nPiRsdGw^zW^AUDar)JGn8!q(-&6FneYR+{aqo1k&+w-%T(7GT0=Gw3 z@c;3-(*M&7j|m>L-81J0*}w?g#t>-&hFF8xUxdsy&gTTHOehjeay85DN3!P+_3v$I z%^~*Jd?r4{3(sJ3o`Ht+UNm$}JnH=s7jZtCtr%kS`r6cyV3ME9c!G%9^!@WZO7<0P zJN*8H6v|=8fIf`Xm~M$31Vn|+#CHU6r*ySNFg43?cOK4sG6@tBSr5tEOj~{bvoE-* z)Uvn(ijta`SR0R5Nx+Qodvi{NEv57lzRlp#oKY93&xq6!u;cEh0kVSA~g0Hu!*%^oB;pHjB)tz#9}bc z&asj!`ZT(jj0kIGCLS>0_P@DRdG-k}=7beN7FhFxhUoQ^^w6-dnFa6@N5(c0>yKe* zc+0jfw@dp|2Rmm-B#)d>Dy3Jkd0Le{CQ}7gnWaeipt|jCY|ua97BSMIe8YH?&tN^*Vurh zhplwnmm8Y1aAtUs%{ec<;Cg!6o2zR%a;0N5pwIk?^mn=#5T|y>%Ecu(!<25_5~x_BFgU_(b>|qV4i=K*Q(-uIm zTk!)wmYi>2)+uR?nTkDa1pMRt;ETiKrS@eXEoVmllheQ{#KRxGpI!sH0dr=Yj|Ky0 zuqzb+$JFL)yJKbo2+woCHO=9NKMGv{ZZ>8ITs}VqI79pE72tU1DhF(wZH?K(XkY96nzg0Z(Im66jtNxoXfx*~+qCZQH19KZgt2Ay-Qn(BPY@E#XFrx#`XZ)J2d^K=Ecyw3mO&njTJH?$`|BZpH*S5XQ~A!0rp!Pw;Wo=jm%wb>yIY< z`Pd=NTb8*Bp;a=`!IKj|yFX_63(?kQtK7t_eR+JgR};tADZMGHo{r?m{q3XsAhhB3 z)qN9pzeCk2f%P*AyLdQSW@gqBi4Hs4z?dI&L#Sf?=_Yuls`vy-Ye5vyN^Qhx_2+2i>FjZXrb|;a|eiy~*6U{Y_XRcBF#r9X(atf>q3~I_H>< zvh8kDtiAKgl_kHh*dFfa~BvsQ=ln?*l<-bMG5+OC&fXdDUSzE3|}r_LJED zl$g~UT*m}}(sq>*3dZr(-Ev}%XNr6pm)nh{>rxvF;ks%W%(tp0lx^@5Q$h(CQ9&=S z{GrvVGhJ`AW?nop>a3dIaN6y#(A=M<>%_PvPv6DS7JK&NCziYk^5vVwDU%gVR4u9Y zt>VQ7msi3HbBl4y>J(A?kXa?ZsnRC*-8Y{J&CnCMtyIRh>z7U6*jGBp#`U6buOtPF zvF4$YGW#ZJI}f#X-s*`K|I^4g1Rn-<1z|WGk;6@|FI-q^>Begt{VyQvz8c|A zMDfHg>z&_DLRNxmOUSc&X;d!;XIhhE?PD{IN4C1mf#qt=n!85Q)gE$~k+zcQwvSsV@+<8+&xVI5GSYMV*ALy%R+W^GB5*IlsI z6KZcjgV)b*&V0kJ;H)}6$Gep0>vs~dD%QJMd-+AzcK7FC3DR(HzJLPetEaVly^)9r z-3D^aJ5!}n(TY%|2)fVYayH9mUES}@L_}aCZqaKXr(%T6>jZPu&l!Aby6U|$EcMI8 z`bcox_Sffpqz!#%w@ab{Yc!`xYunhVt;Syl^g`?)Z*u-9zdcwvbjj&pqf;|{VdR;n zjPkBf5tep+?o*|21!om)D@9>LTl(??-m+O`f{6dJ4JvhKue#n5=@fo-FI5mB%?6drLx?UTBU+H&%c_x^M0+*M{69jgnz`i@R>9_+Y`H& z%Yqw|P@%$zsQd~WS+ZsC8|%J(I*5471>5W!*-+jE&xUBYB6D||QV_)9VyKo(kJV&iq$MQ&4j#;nH|yl; z_HoGCxKZLHzW?>!%fvq+x@yPNKJ+5_t zdWJa#>^0-mh{1@D&+ft!4(bm!B{X-{Cb!F~>OEv;T$L8qtzYsL?SySjaHS4W^7B=M zF#{2+KLimWgp^Y_%nS5vtXK{uU1|1D@`$C5)BNvkmtYyTuz$pM4#(RZbMky#SfR4; z+49JB-w|Bn5@HKxG!Wd8{loi?cjUwDgLSjS<(1`w{YhMk(ikLQ@H2Po*SwkiHLZhp zkaO-GldN2K){f>)({Tq|S{vP=*RE(fhOZF&5lSLcTR5%48jQ>5JMUadb#8AN!799< zd`(H>ovC%{y!r>by@2nGvuWKp-6oh9A-T|R9BDhIFR%rjm-EUhNZileN0j#aZPkv! zAgUb&_gDL~Md#wUOu9>Gg8}Qz2TixFta-0?z(CY--$H#)24$-$7gW?^wjyY@UF0*+ znub_;PU{Ax#f=w_EL(c?D-Dl-U#jho?GHd#TEOs$Rd4s*Vi3iOzXzW|iyPsp^$dL( zZnH<>K|deIsi7tq@mEs$>nDQJ)#x?n5yFC*XT+%Yz+?T_dBvp6c**HiGIIXai`Y9U z##bBLgHKD_XZPx^8aNTE^gbp*e4s@>X-|AfpMBMqT2AgMkHz+b@0#*G+mZp6*FXfZ zR@#A@S*2I|BQ@d`$D;|mSsprVE(<1yI>0nYJzU7$Ur*OW_(BU@;2c?FX0!EaR_@fh zxE)B1<9#3@d%asSjbLeQtyR;rLA=a)~FDr;ZV>c-M!pRU|#M>LKFvzAw4u^ z_$t^+2*7cU8G+Tg7)6lNz{33qtf8Nv`|;iJgVlE6`LzDJANnj zGVv>ly;kl>F9SmNfM zDe82US3<*0=~RqGE0m($d#U3|#;cVY^(He=-H^0wS@q~pX+nX^&m1X#`i-}Hlv3mL z#^Dp7Gy+FA42=Hrthb}C`?;}QuGJ|+cg94>{GJ=`c&d>PbgRHNTN}j&k#6pw2I~qF|%P*ouO*#`ppz!D7t>irlumj=~Sx3Snl-QgR-wK;^~$F z60^j$RVoK@T4Uo&QsAVOrfsN2#MqG6!C3ter3r3gO-50@Lj2|+g2|{S*LK^l)dmMv zIEz`M#ja!JP`2Bt)aPYEI551`ymBf%7+ct~Uk(ZDroOJ^E=?ch^>>4j*Fe%0tE*8d z!LX`L{v5j!09-?6LuCD)wy3-AMa1PUkDQsGI=LU>mXMN;L}5T?=p}u$^%l4R;Xx`r zJJbr+(Cc<*#4uk4F2Vhzl4WGzex>uKj!kB%^TeD+;wm;p%2J178_UVG@Ph802eqwc z#Ync=Wi=aQm!-{Qv5hZV9x47j$ocI!?cm*MYj^X1ykj4TA3Sir0M^H}D;nu$+~!8P zg+1>t!dTw#=j2GlvgP^LTrf*IqbvYW=G`UC?+^C3`48qp!pToF*=A7g29-L*#r~%Di>a65hA)H^4pm_+0tVBo z?#zn{$)QWUg=hV&*x?C;$)SwQxGish`WG|b&Xlc|_rOK8DlQfiG_4@A2;z#nr4J>f zXagEx^rI#*HAn|q6p)fAjoN4%A{p-UAnRpbi}ETSHTdVNL+ zqshqJ){L_HALUbL5o)Vq2P-eY&!ED4oY)hSEp_u5)3ya(8#vYqn4Xt0ZDSW) z9HY^>Fw0+d(qM-keVl9OK{kxhpXrcGy<;s__4w8GLiOJ@0oGxn5w#vhw`0~I18r;& zIWJS(UGQ>N!!L4qpdt0P>M_0KWkd7#+x!0_VXs$Rxmu)k<2)CcLQ}f?Fn*u&0a&D*zjQa1LR*%h6>dJGt>qd{g9sdk2;KX5?9H}6T*iFiv z+4$Ot1?Zc&W{8Fyw&x|^5}2Cd0(VXo3q4~S<`ls%pMhCK)ws=8jYaMFIcmrf-Az-k zL&qkOxji;+ZG}5YG0>1)s{Wk~lxwO}X%SoTIgb7e*{{+H*{`p)@koD_6{f3ravfuN}|hP zP}2_@@qNq`-f{t6VVeLGGRK<r_iM9!@0DScqea_Od<^&a+5Yi1t_WosR6ha9$B+wo%{kwQt%$FihG zL1xWKulyV2>-{W$gr+{;Fz*ZTB7K9Wq2aG$nS&v*MHv;A@#eIWZ&}$>B~MlNWuiPM z&sf(a+cV`^@gU)^U+eFYROH=*nkH!g4i5 z53iCp`1*vJ9^BEn3DRMLtL%A>p-B12cE^463y&-hD4emoo1_l*few|ZQjMW!f&Ba5NFA3OJ)WWe(n=Wzz zF>)0s!sW2JEA}KQdJOEj4YMtdw{024d^=fo_1^Xe$Ng(jl6UYy_O!E>wnUh3D=}%| zt9RMSD_rP47|AQEVNLU_NZCVeT(vv|ZZAtxDoU{XAy~M2d;4peWr%hhC+-5=L=s{8 z@ep_11QA2mFH+v{6(u;!78BlJ9vODHq0bVS$XxicL5P=0+>*ZaFX{zCP6HO?AQ0q= z?37%6GYo&-<>m|pT#HR_P_$pxuz!6*wM@)haZbpI>U8_|b1=`5?lreo3s<@N~`9;K}0IYDyM6*QuPb3RV?I0PnK7keyFGr#i^e@ z3?TS1DB%DT8iZe&jHUQe2TgH_Z;?*uoOHCp#}&sP83pntvw5jDQVKO<8WI?}b22(a zr)xmCs4LVK{q>EMOO@hjq?1fXd_48ehIr8qxQc*BWkA}OqVTl zL63uXESiba1JvICKo}*!zKv7oYirK0uDd;(ogID9R5)`KDC*57(Gv-p*XN(_s$`bk({x>QZG%te<+U2JAgY+q|47 zo%sjyKy@=!qvOZ%NJT$_-RjP8a zJn zFg!Ozoq5nXhwH)hY733nbPw&BBJN`~y6)9_3f2>Yf3Okn9Er2v0hp z;1l%^vi_n!7m2oMG1&Kj@$~9ZL|KWo~}h$Qk7XY%gPNq zeZ$&MkCR-Cjf9(uy~Dl42+zIG-JJz<9`P%#@2tW24W2JXnp(SA?^ritS3Aiy)vK0r7o+D-OJ7w1_xev@zkPYhTU*bqmzTlRu@#xMc z!gbsHM6R=SMJ%$#bvAWcpOzZbu6S0tr2Oi=S!hGzA&}faHJns)mTfI{wEoecn{z8* zzA+_TqHttc8a$jbJ%9U6a9M|C+7y3Vt-teTQQ)D3LmAi?5zZ^@LinEz*xJ|G82f<3 zwa}@@LV9^=<4*^C$pLZd^YFd6^v6z>0L&AcMUuShrF$u$nXoG{16oGbK|ag}AJgEP zF$x*ZyWou$NcMR|5O2ub&(QXSEpw{OkS4wbCIvR@ z_E9J%yq4jNp7Pk31^{T1QZ5hivYOBQA6Q9T@j5058@GN)ORPInM)N(A3qh6r5s!P< za)zM+sp(?it;;@pD+1VPm}fZ7V>9wZ2aN3qeZTav>Al1j*#q z*Zx&I@u|bq?sw@gdFz9?z{6=<QmO%PEU!cWHV)m2pFR0(^4^?e;B+FC6NmX{f0 z>5YU}^3`-FlDA@PT4&O$Jm+$D87E!Uv>f1J24TU=pNb)v+? zj9X&eJNw6!veB+{ltJo6C6E$V%S6jkDwAM)kc zrcLEX#Pmm<^T`FDm0Q`i6DJ6n+ZNBZn66$EZEotP^au$%hnFZ^X-Nm70FiFC9jTmR zHWCP5MhyP5NutJP;iO)wp2myyNYy5bE$hw~YH6#BmYx7D_-Qn}qVYNNLJu&$K#b|S zTTEST{Rx-Sfqto2NxT_JN~DvbBSM_2Y*(?4dwmEOtJ6b0;>x@2OgX+T@qor;scs=Y zec=0cQ~j*_mwg*(vJo**ea}P0XywoftF~C-pT8Am$|uPLqF{>H!c&*`Dl(^RevG`b z0`nC;uJpPy*wE*cC2!Vm7r0r}I0U`%w(3rZ*1mat1)jV)y!w>eWp`c0JP7={(CJx#fSuV2{ zlYCXwu4qx{_%an)1q@Cv`3vV7dhuENv{yCJ+=gr!I-t>a;Qbmc!?^{O#TkX_xxw}B zvfgnEN~-G0xj|JlANv*(3ukGbBo;9^-C$E=E3)H4Ynhv$+98V+b;v2(Y~28!NlTyg zPWX}JP3~ZFY_Gi1I{q=I+S2p0Dp%DC8G^jQImN5dg^y2hYKbVAYVE|gV=5#&dY`E- zSwAn9!OLNi(X1BC3U@|_n`!t}-DxmEIyNpG-wTl~H(c5KEX4)xO#zf0jqaQo&nH&= z`H^PZ5)mSY$8yu3LYf^yt|E%8lH=#B1^I)Jw)2(@x>MNIpR-XHDCM?}o`D%jp>u`- zZK;??Hru8lAB&=EHv&LcZ4dV+-UQ7=WVJU`h5bG>O3xXok7qm7Fs+<8h=UwlB=m+J z4G+^tf3do&gqKTbzOd2K0@B%C_y(&rAuH&2CZ88@tT&u>ALqaKZ>XRH0Q05y@+@n1 z8Q~2?>poiUQrWK2M#t;~wxua&@fpVj(hz9p>vCBTwyCLq&#rXSdMG8Q65LC}G`s?0 z#)GY;bvPLyKrU^Y!|$i_WA?m4Zq*ARx=!zfz5k*Z06^7xu^p5g=42U!buJFpS zkhp}IRDt_}Yv&WPU4F%FpuOWQyLN?lI8lBsc(eW+gqmuPdejOeyKHFY03pWe${4k4 zAjmj3faKxlm9w9jgm7Orm{2voO|>V6wocurS>@Z@9Z$;K&@(xEV+cA%B+jIlujc*c zbzD!yP|#s&$gq^HwTB1_v%2CfT=sc%7c*7EdfDo;Y(Zg#4S1R;EU+_4j%HW$@}^%L zR{GhXtZuzh+%RyR9y-nFCY)2&%_BR$yT*suqg${K8{@ygfm=U9_7EJ(?4t)dY{^wBT$U@2CK~`9`O0W; zxwU1Ldbh(1AQPOJ<@f-S{)pt6POBsrpV2nqx9de===y%&8{uW(Sx;4Z`;ykveeKd0 z1oYW4g&XQ=pd)^*v#H;fyStf1wj5ftcU3S=1Luyhe(+;Q@4&k=5~lhAqOaPSsEF7t zW04=Bj%Tj#VixZWL>I2+3h|7QSFwdv=Vi>S9Fbf7MF3prPz6Gf(UV10l2$UUZg6(* z#-~%o4UqAXtm?G=I3a{?Kjii0xWSWE{pN?8aivc%dd>ns6KRlRF)Y9DCDYx#b%rW3Pp>w77{cA>)TN~U8H>UesUTL`a zhN^a0G92^`DetM`0S@mACpT)>+CB%&RO!U!0z@H1%FMu7!l19Ih*wKuy2Va5rwRyU zXw*4Qu3Nk|sm+mkRMm5dFTM0(Ny$`t`cNSQ;hK7^)FTdlGbThIE}Y7oGR~bk*0kc?y_JlKBTaAU8K>Ikaa zaFXWqBE6fe%AE3W1NW^eE;U+S{JWr-LR|FAA{M*IjY=WlFyBEBbdMEn#e~(JF}hnn z^-{MBsf=dntBN>g2=kCSZ&x69=l0I&s^7Dv%B9X>9u}GUko0K8k$Nwgg@P|r>n5-G zu8zgVQ0#f#`7B70cMYrav2hQwi%;Bfh=mVcu`F}3p3hfd3<_ehib9e$)Gjr=9WF~~ zIj_^RdSHi_!HnOPFzGXWXee^=gym5vZs5TC!N`v4ewF-Fr6S_g+t_4{-JJnLf76y} z+kk>tpiQG2~h2V4<% zeHj_t`SqPj^tk?){Z_Bfq@D@n*wdTOI~E`yrVH?bKTA$q+nVDoj$%bW;g)V*Rf*FE zfVBYQX%BTSFocK07d><1V04|%`qGGLBbzt;>ko#iq{^B^t?FtMALRJU&SyX!9CC4` z1<10D?bXJsm2C_V7 zxJk^t<|E}wkIt(h(jNG%CzLHl=)zcH+H(QK=J-3LwbTLB(NbJDWLVD~Ia{i-M*mgL)IS3mMb9p5 z{E1OaL4^t;Hq}q6`Ir#YY&~X+S_SfHHukaGD&drisZQm1?RaDO)vlM8;XhSg#a}Z? zH5oeYnk^ez5gP#8vKzjva3xJc+;0a)6qL~zTX%E6Mo|yk8>`O|v9-<|R#vVpC!EcG zzM7LJJQ0!$^0yB%SWz&TjheQn*mRew=livrzI=NayeRtgui(Wi0$XjxoAoAMr`-W_OqHzTWQnlv9izHM~~ z>(q&amtcC(>; zU$^gXt7&bQF+)-s(m?maWnuudkxn#lwjb57aBy-<-8ov5_|EdtIYPV)oeM8lK|@vG zLG48S{0;9V&sL4+mI3MLc@&kyxrIMs?ym4nKfBa=p@{p#z9R39qP+1W-akQD$bSS( zo%Z-&O0FSpUs z6Ia%<`?5&|$}B4i6+qOc%JDUYL&cTyZcQQ7_C?IMQ$ti6qnMB(n7FAi^;>pR9&qes8>UIC96oJCFCK)tBn!P-TKh7T*f@sn?<&ezLH(6M{yxR;o8D(1Qm_!C#`KDDJiuqd!Z_2i5= zJU8&Ga*93piN}gKMF5HwYg+AL9~QNRXs?+ruM8I_>I&erl4Oc zC+OhfENb~I-;%RnVU0iM&;#MwnRiKVRkZNT3VA7Y>!cRBj4A#|h08QSEKR;ZyT7`# zqOVQ1@C5vIS_*8T9IqeKb;iHoLwtVT?@+;girGceIt$XV$Dy}>%|O2q3AuDz!XIRi6Db*(vqgyg9< z9sG@oldl6S@<(~Jx>Gz-bA6$d+ssOjFls}<==qM`b4uUonKf?+JY#w!YDVdSPpQwR z-oc8B)kC_{?tyK6hUvMgrcW0S7j84i;K?7E@Lw`uhowlLN__<`)ID;xs39sQ8Rl55uH^5)I!_ao4AymYuChAv! zBN8v?a2Vj&F%FdT%NAU459fW>+!E@D?m7J_tj{BXU|5!DxMQL^Z9nD5IWM+dVai@Rbk17Y;nAzM;J zT&gz=Dl*U`ZTySKpjLIrhr2IIMEr6YN?mU3Fv0;yy8Z|btt01VQ*VGBh0?01>VS4> zX|nNT7%B@9$I9e)+xPvF9Ud|M;XtlM<*1bg=1TQe=QM_F+Zd&m=GSO;FDJ`jNydTT zB#@14rwZrmDVs9LIds3ZY%=vzFnr^Oe?k2llMJN&daebJxq$s}{ysw+dyK(o~!BlbG-c_sr_z zgpxK$uC+Dqah>z~e8^3yigvb}7~7@f%~EZ>VQ_Ju7O_#`dgT}nCy znQ~?jDweojste-(O3blwzGIk*epfZfkD9LgxmT`~$ZH`!T_Wn1fhdb8N&d)DwvQGC zE+Pfq?0P}ec&Fm;f|^E)f(y)${?^;FlD%d3{j!Up{WqkRY?>4F482qo`{3bBwis78 z-$=qmL*%pm!v2Ez6fpk!0x!m@bho(YLThVTgO!e|?={0FCnLL%-uHvoc`KUI94xQU zRo+}Ku`abZomoEw<%QXp)E1T6(vCs_`aEoaS}h}RbUcB`wmnZViBAFicoC)tHMUVQ zW8~GKH?Hjpl|q_ZaaDk6w^A>LxYC0dKl4strxn&7&F|YI!PjO@9@09Uz;6CJiTYf%#K3a;n03^!t1!;EB zP8)fhDHKnC`ejItpdch9>EO6Z>dNZQWdH-OQ-Y&o+Y-X8G& zIPgXhoVn&?`Q+nNNn86eNvogKOfcSbfJU3QjZG{M2yaL!EC}ba6W>~2--Yy6!;JFx zf!uJ;=xe*?Wj}$W*(sV;CP7hjWvm4BpE=}I)Uw@C&jF+@8!}*@Y4E5b* z7yq6&IdKu^qSQDAo=yZidO%U;GPV2kF z(v8r)jo@)0jhIg&_l|*OiHK6bv}?WZXw^R(FYzh9x!WX19S`Kx+p*^>_myFx1Ib(+CPP42bO7SRABH{GLK|W;pL)RiXOl-5rqD5TBp;wGAdBR9W0 zeiubBEP^K^l22><ck^Q)TZl^(5SFUT*x zjZ16y8$3vJZ**x13)C}tE>oTy6AkQDU&Gkd`#ad5{ZSQ20jorW>K_h4iD)JWo@rO( zY2jS09lpELxv#*+rLq11_|03wBxm1w0ALK#ik75k_! z_gFk#1}(}=d0ALoKD`j!KYE?zkI;gprof<}PajyWu~QZogUW2M2~kJbcr$(nrsze~ z5%0GJ1qJ@uTP#eTet%lj)G@b`cG)8=0&DZVTd$P`cu!jRe`!p?GhTYnj3C&r<5g%v16 zyMJw$?ZS%sbgQ+kz#|acX&Ll44TWikYXuMrjD^(7eKI_2HL<9{|`J@ zUc~$5DEHs+T+|~)$7`etCx(vUxd`uSS;HA|(LQ&{=q2bOQ2);BksTEn)0(O}tg@6FNhC!(zauCZ`v zAC7g;B zH=vxS*J%nM%-N7)EFbSNj@;w`J}92x(E4K&s4C3l=FloR_WJTS^#745HfIf+`4kUK zOU?Bx6)>znOMe;H-zI|P=1IW7l-9<4u#HLL`e|Ofj1_;NU~#2r@BUzkh5sxeHDDQ` zGw{5HG5tK?8n0{KlGwlhezO@cU8P6E4#ADAr&QmIzy~cZi3EPTrMd7)81_PCNnvdA zONPSC_Y<3R()=m4g=;Foi{p>2aH=aGc_wDlokWjqJ$uxvF&w#6V)ZhlOjUL#q6iaa zzP^9tZ}&I=>^}24UQX}RBmgjX>q8J)05)ke`;|etKwbI^Lb zl45BZAgtmqQ;rQn;A6SA=8GroBlk8m7viKV9^m#juoF;^>{(NwTu&@$Du%y_1jDt~ z;5x-3$M?P;)4C$-^swvF;=I;Dqp-Z|)5+lsj~t3)CxV-8&N(24!$}M8-2zU-$qUO$L8lFoluve3bQSoy9O)!&e zIrnX((q-bLT+8G2PY34owdL*k+%3JFG$kpsqWo!-ZonnJ;7YD6 zbrY;XGsq9{oQTmc?=fL{Zq1QF2zd*BnY-VSr$nf|W)umy=C$Q7xmviCAQqPBqSC7% zx&q5h*?)SPYv)g~{c-Dajn}|<9G^6Rbw^Oe#l?#X2Dg~`h}yT+?ne}P{^b=)I+nX% zy*Q$S?6InS1BDtWR_{=!mQ0sIU!m zz;I54v>8D`rKi>PUI2nq`5vF&tIfPn*gXiDmzb8+aASxd7*i-Wi;v66rQV(&ojvb+ z2j=VXoY=FTuSV?c0I^ik$*T?TvFRU*RIB;~g*Z#-Zl-aMjm!j}%G1B>4qVYJ2TDx& z_jWL0r^#CJ*VO4X14guk{q6?d(zPc~j{eI#=2-wRTf-%Q`1S%obB*_kPpo79h3vHg z2zf*y;KCk1GDgM=nx7a@hJt4e#V6Jee6X$7V^|Ip5OV`kA4suZuErPgRF0;A>gdd8 z5;<4sYR_eWDjCL$=U1Cb6h(?lf)GF_2Y9p3S5Bz_P6sI5?YyQKUsS}Vq=+`RyniAi zW2M$O6AAfzZ=gg+r$Dimyr}QXpx7bDiWd{x-K?oFpr%0l1`IHzeo%g?z5WY^|aEm{w;IoA>2{VZldNAy3hJ@#+IRKpk}&0_!CKR2F&vW-x|r zJ1K#`gG|~K&N3-=A+;5oC0i$$3i< z;l6x4{%y6Ji4wX2;(_USQgoI0`NsJulYuuvl^@OsBY>4x;xuHSNAE6T$apinT;J+F z5ZcLkJLDr}o&*vTxck2*DrP7jR=;?m5&1`}B|thtbI^(OZSi#x<@mAwExPOXH;;O5 zEPjvTx|(rM=u(k|S7zPHVF7f(dtm31N5XV{E?v-CNVbUX_Kr@f?}dd;Mr`qE1_7JP zEo(dyZyx-4;GZp`1)U#`e{oR5+{8&JFl?9=FBMuREf`C`hATGOJOmq2(?=No`T#go#S_kq4%QUu=T zywoq1IN$;o8|TvmeRt+^^O0$F*P~1>=j&aOT4>!OuZgd^=8d)Ws4b~$`IYXg@F=t$ z+nHR<gNm(q)V z6Tb1VP%I-oRuAVj=y*mb!xNeISuLo*Alu1%MU zYeN+dwBnK!N-=DF0w4h^ms!5ublr*Aqd5gT?N0Mgi0$=>HOfU$j9={h`)LoJx32Bx zahT|D6Qr?^6;7R3WS}ISaGSN*M!E2*la4zGS5>OnWU+lyVCkV`vA1WrDO|AheUaR% zc8ZPVrwU+UkFv9~e^KS8=H>}1v1|swV>f^;wfuH+uc)9Ps(19JvG$l>#Nmys8nA%V z-I9o0l{gfW^}s_MUQJ1OeayWmzC2_H1N2sJFyKHEo}c_DP0ilDD$8=yX{5$0IqRCU zHb9S~PDhY+CZ8Siyu1!x_@8SF9c1orEc7SjymX4kDAcf4?J6Rp8!D3WX9- z?fx|m5M=ikMk@W8n{?m?13Le*w!hTiq2%)9J)9C&q?n>OAYZtyndDjhACWToY@3JA z7CDqz$}sW*@T`>6$5?L)50FT-&BY1fUy@5BY_q|t%o-THYwUo;iYHaN=pRZh--#w- zxf4$F6kEyhG2_2P6brD-9?g9T0Q|&@C&Ft@Fz|@GD_1;U>%N8|BG0R~4rpQw7@gcW4s;4d=zK zMsFScYqsyb0|Yamf#;}Y*r3?64-L+~c~-=(@%<0hae!=}#iE}9I!bPl*}5 zDK0Lt#5_U2$LM10{zx{P{;vJ{$kz08IqPUn5s>0Y6{u4r_zmtrx1@9BFtG5~>+oaV zsgKCXOX>F0#RKYqcA2;7x!##l)SMC!+$~$r=nFk27XA(pm#;}x zZpL0tOKr>wH9)4ddOce2`+wN`?x?2HFIq%FCxGGzB28p$fC@-2LJ$;Gx*?)eQIRG^ z=_LVFz(!F71f+M6B2uIVK}Dq#iu9t=2~j#p;GK&)GryVnmG|dc>#Nt8B}*p0J`p|3(Tx5h^sQ(Il)%3gr3>Sg9oM5!tb4;|-fZfePc7TKU( zM-vO4(S)=lg&K#68f{_lZE?w#~M&sXyYgg2OwG5ISaF(`Jarir-}<`^5Dot)fePx?pQNt-MG`j)$5 zND4@T#cC#zmwSVcwQ@^V*Y7z(fHpVn`n`KNcI6%GB6YUOlc-rI6{V#b|3FyNLe{!1y|wM} zZ&K86Il7jHq zR&|kiO55`LWb8*s7@|rcol2dHiHEA?1^-?{`-(4`#o|rJh}?n7wc*cYohS4}Vn%cE z^I6rklsTd{?S|#Bbu@Hg&1-9I813bG^zwV{^5a=^2{8WhjilaRsALP3@G`+GU1xbG|GHp zUL&ZkD13Xjx4$a%!hu_-dwRyiIq`DR;*yXwG}2l3`PWht+sj5SUFQ2Q3$CBMG4IHW z?IFk6xiDNUuF!^1)-Kn~K;!N@Ls+YfoA@^0X}Y*~I;UcxyR}le-iLTcjx(y%{%ZXD zkRA6drawj5s{_+`Z>;Mq9j@Ey5+c2-or{g&7?f)HcH`F4=&rstg<7;deTH z5SaseceDMRrh(@Hx~jpe-33;5Z|<=p^i?a77~1o8lfX?AJ8U#heEXh1F-&vqWMaC+ zr{}f14@C+m3>BNzFs^xrw>7t>AEV_y#2-D$LU3@3PK`p^RoYs2nuZkjhSc$c^U@Mj z36F*)a?LjNw~yowfCpGiLp9p?vgJ}rj_%mn!C1G%g51LrLOLVuo--Ar#oqJn`Z%|1 zOPWy<@1qS+L{nqa4`imak8D!Ewe(LsI)31?)>y8p9TyBQ>6(6(j5_h~6HLC8TEOJS zoQX_DnyRZc<+sI?pN*N=fQ$TZWF}6MieDkdg@;2+!=;e6X1@EKPpHH80s zlzFLNwH|OwH)^k)ZzD4WCN+~iq4Ll?G`DAb>Sl`rTVrU$78bO4PO3)aS%X}wL(^O; zH6qS|%9BVl)UChMwfA~}beWTh*ulVNGV$vifmxCJc!sTwB=yQ8A)^6pHm>$`$cHG) zJ#2K_f8eRCBh^FG_j0Gut+R`|Y_hSOcyWuy!`Yo|30IfiCldwGDg#FkdzhYpC4L=h z>3@JQu*b3zPt|B!TB`*n6Qxg>XU0J1gTE40|C?rE`kHy$`+BiZt+zoPk=HK|6y0xG zvGyM@cJ2GpnQK_w=q++8iB>@Eb!=LmclVDA%69Bn{3a2YlG_Wb0` zecfq;co{gX{{R#v^K1=d=;#t!EXAU~y%VF(HOO6_Xv@G7hKjA*h3preMD7oesP=Rglp7RalfGLB>nSVnbe(P15+}kv9nF+fwkP3-EW_4;5ajy z`stZTMd`I32sR+*27fYGR2h;BY93RV-g|6wq<7-j*SQ8Y!(2C_qHyWhZv?GQ!KB6x z!>4Q|BC3TpRu^V#SDI%d2cq5ltzQaCq!K*==qn^U z&WCpIfpFxJt4qIN56rpDaNCzZT&_^mykBzT%ZK_i0VZdgB|>l~Yq3dMmTN06IP-P;MiscRHZjli9cuZK%!sxR(gE_S<6>E)h>wH*u0w_a2pdAwkp zts>)W`Yk8LxaHkB&MdBzOh31)?9ISI(@jbDyN~iN-r}QHMIX5^!YjeI(iopQ*w*%> zJ(baxosn25anpMx&HLM4XQY`b^7vFEW$;H9~sjIg_NF-AJjMhepjVmtHYCk4u z!J@{-#>jDx=`1CG$S4V@;!SiHEms=g$Ql7z?|{6#QtG!lS~0aA09LFR1;13o7cPfJ z)PZ6Kx^(HkqVB8Pd{UFr(rO!`Bo6^Zz5D!fZ7I+d0N2-fOa#Y6b=afoKk!{>*mava zWr03~tC{H;SS^x=-Knq7bzpj7stZ%Z`#;GB-M4^$UVey^ikbnsGHOFn4uIN?ot_*l{ZSh=^v+N%0rUcbC9yNonM_kkij46N;m zsh(nytsheU+--S`U;O`?(EodyP~9#-!_Y8Y@ly@tVz=);2eq-g_7-jnZus?Gq$6!P zM>k#fKd4409xnWtToKq^l>iN~R81F34`HYeCQEF(eG$#WxWybb0<0NDeXmv-;2 z5KHDPl7}INa#Bs6>ty4Fq*QikkN0PIuD{++0+Q-Mk&u?x&v4G>*zaAqQs2XjlolJ@ z7l7H0YP`;RJVw$Z^^u>No}-CjJg~EqpfwXd!XZ0~bYj|y%gF`3rZpmp(6z09S!#dp zeBw#T%mm<0O97APOT>Js6Cq4BK1BB^7-~*X>4WZQ5Gl3jZXHU*P^#dY{AP*E5hflP z1Lr8sGq(5AhH3&R1CuX1O}%j=vLX-*YWnH(`%s#tav%we33ug3o?J$n$*JO&uZ5<@ zw;I0!k%&~{*-448d&5^qy(MvR%@*%^!Afv@4XwDD(r~1>cR1Mf&w3O-*ReHSi5n*i zd3ddIKM&pb=b^v5BRBfgu@^!{O;hzvaqYRCuBBJ5B7{BOLp@UaV4PRJ^td9GtO7gk zs5|jPQN;B$oV6!S1R?+Uq|B-}+4F1#u88~E+sA^KGm~-dBb)@%BX!}gsmF(}@2yw~khV1GSv9^y8cR-+J^t#!s+z?$!# z$bHQ8&ns;Jw4Z>AeT;}nxeP)MT^(;tLBdwo1ybNw9m-s1f^OA?JWSqxRqUy9HbwTg%=@ z)TT7QQ%&%Uw`ndB?3ehhRrLtH#K!-?7N&3t(5>7v+12|`t&z^?+~2h82>wt(`!m`q zdr1Y;*~R?^J9kH0h*2fJ9XSzmJ1y2DbFJbE(J5-^;Eu2U+;#$#K+tJGG>(;iR}k|+ zVmepE-_-KTRlwhY7M6zD`Xw;FGwuO+9QuBNHz%LP#_lEr%H>I%Y0WuJITKk7E{MRT z_H-j)+x~2D(6eY|I!{MCK2IrkVN`nY2X*+}hVTT;?jE-cdfuN3t>3icx%+_{ZB^`@ zdLfz?=B%}lPDS7@EOeLVm0(e*?m5!0?ze@C}=|iq>*!A6R35G}NBKj=f@iZ$r^Ikuq{e+>@3O=^7_#jN+*oth_ z8W;@qO<8j`ioSV5HGBc`D4WV4zIVyK%j!fzv%94sWx@DCqc(w8(d(u3j(KCnBQ{Yw zpKkBaa!?7_dW7@AS6L28YJsU{R*sd5HpsFfDioqK9*PbrAz=460t*XaiD%Am8bb-y z&6p9e{-Af?k%qqXdW-kkm1kogL9^7i@}}DejQM{tj9}WxSqCDGqpUG&Hf=F(Km^df zK{XtXXxJ4xv>(oBgeks@`&^MJu11@v_hHkHm7PzRMGbRX92SnqOI|9l_agw;K4ho< zfDc_cXUMBIutr=HTeywWC&M8x)_s+Y9!krQV(C#$^$(G?P)*@gBZ$M2>3Gi*cG{!m zBu7|B?P#8LduFRdauGT*`YqI>nNROJ4&m$LH(4@5vHEA`Su(F?ynv2Qg4gKd1s1wy zpbpc@uStkafro)>b3}rb$N(^Xt*&EjGI#3^2L|1*xvfev_X`-~kwEG6pxQ&L8AHNV zW?#IMwcsb(cIBI|dR%fkCNA{iPSf#4Ro-+ljO}OBJB_+;KOr&1SUP95y$+D{d;?;S zi}%k)>qLcJXQ8{r7g#x(vcpnJFPO=~f^Lk7l2x%u$NoE*{9bP*9zgZ1#eF$sdVOH& zt#mJ^#nYj*uJ2T$&&oQFh&++S9Hb~xmMJZh@-2cewYn6%_AzTL{f7(Y6vD)>f3ehR z5JUhhn;-#a?|%Hdj~oK|;7c;W{syhxl*X>SrTrO!T1S{mqE?dqMBE~m7qjhgb+{SN zf&2HlzrU0Kc!>TzU#$_R*w@Jc$L`OnEO{EFr{&0@_044+&j+BX37(VL#l1zgJw|}9 zji%_T_9T~;^<&u^;dWz zCjt-T+iqx&GuO>b!1`U6cbNiypYbusjU97|D5aeR5kD#V?-%vQuZR}$=f7CD?fm&K z{!z}*dHZO;dhbs!T)43D#LgoIY)IE7e)%nPG1!OTjl>UJ>nUo0py0on;vR<`(k}&u z68>JYrKM%aWx5+QQVE40CIcNmLwY(M7u1u>npX)qTL#{`TXvoml_M>rhih*B*C5D3!e*UW!-150$Jz3t`+t7 zn-sgw^sk=_*v4zK4GM*}=3InEmb4!6%ZGm{kDxXF0G{Cpk#cgi8_}pyVbN&qR~1uE zG`<{hU7V={SE5q@ugiIo`gZ9)r%?IfJA0{X)}G_#9MC@7VwpKz$N3Wxs`HTh3lO=jYoszCemBKA%vOi=L*gvp|+53zq+;xi5hrXWp0u# zTq@*?5$qb@U$|N9DHKUW`ge)*`gZ!lOVxI(+XJ8?UO<7ma39(DsS8{zGM8T8d30LF zYstah1+k6c4H^zhtc(F;?F`LDI^Rhdr?KYNcfkM_s@Kk~3gWQd;EZQ?3CK@`}4##t_uS7UqbwtI;QIP(qzv7?41 z1nbcF@G2ls*v}C*e#DdXA5&{Q7K~1IW#9sg2 zj|KjIj|2n8$jett?}DMGBl`3KZs)6vkL$%a4IdSS`C25W&OHWo;mm3=nQC(!;lvD@T&n&U{i= zSW^WMRw)AzoGV7Wa_PNN+g!|Wex2}S=nzNHwK_om4Cx8u($l-Hz8mlH@K2q5c6Eup zWQ4V64VpOWm|@o$mUhM6(^GO2j1B@5QN;pdoTFEV_1` zn5Mlr@Bs6Y6Tj|+p#xjrHT$jBZ|MU&M-`@0Z+Qc|$u4KR?0 z3_#9bOa!N$*xEfK$5Trz=xKG;B<7Rib8HcE`VE`*5@*4M^p)u5|8;*2G3QmYhb`PV zV#`|j(pp#9+F<4cb@dZ1O`MjrS~4M2dQX^?Z(O8aXC2^d9cg~J=IrEoNHi$j`{w4} ztz*NvLO1WAmEQb%6Lle7$yu#$%iTA5+g}SPD~b7rD2eJ`-nZ%0sXP3)m6p5+x!R+S z9^d$-KSz0nIy%M%M>c(qZIWH2mIgS?;J(mgJ&X%YVK`$;_=YW1){~p?rx=uXToKCY zt5+Kg5Z*$iTl(=xR=1}|>x!mcJaatM2K9zbpoCxH3)nIWj)xjvof~U07TO3d&#M$r zKz7ghAwsuXPKUUC|1rw><8foSN2z@Z-{u(Z?#|KM?FCq>)`|_RjZQc}l&qjK7`%(fEJ(^DO$78&;r%GGzVXBh# zOn-$!KeyImHD-T&R_Uyu(d4*m6zNd!{AC>dbzu-`!W**gT-GMd#<|SD)E! zU*f;Fg&Kmoec+v*QCVbg)i&d?jV?>Mr#C1qS}#kP8f@5AZf$Kn#Zg5UVvTytHq5^w zF_k7Hzx|FI=41Bc5Ez3b2tY(PfD`nf_d{Ve=Ckv6(_8o)ZU>k#RlIzBYwT02kTd2)` z{SH|hd@x2vUi|@%bYH{6!pyZ|HyxiEvg|DA#X?Ao+QwgoM*RIW#ZFqfNQ!{XUY6`xMZ)$uh zd)eB?2A*(B2ZyV$vay*h^WD(i+IoqLi;HTvN6^beIdypbaMqU+@{1B9{M6t7B(}@< z59lBA3M#|TFCS)AC>KIPLK^PwC2+0Y3LQG6dEr7N^6j^8-!!048(^Wsb#!-IqfjU+ zyoF}q0545UOi=AEDq;2YqaQu`mB4-K3?5%;WpBUu$B9+8f7wn~B`7Q`b@Jp%ytL1* zx(^@3(zIf}A%_$d6{Vr26*Rzdk@Z7EgCrJ<#Y>CEpHWxWG&4&WVA;1{UcLt$cLUPG_61lMoFlW!q^ywcsSZ)&R=#N%#)4%|8awvd~) zFzUTJIwG<-#(|_;Z4dd!QkX-3FCKzKKHs?2{8cc=kIrwqqJDDFb@=r!Wx|1(zH3R2iG%~B zK=|x`5RTgl9~BAM7Z%DqPh!uI40OYOL)J|)*Y{U`JRV#%6SuY;H!URv?aLfuh95z% z&7w82*mn22MKyTkG*6-xFNd+y!%;ES7s0>!K;f-Z^KzRMayzW1GuC*7cS|JjgO^9GJgRU1(l3swVL;57A$hcH{~qA zEmx#HIt7H(w(yMfbkTsVy!pYftgRzr6mg|7_|@f6O&1rJyk|FSM6vaP0&UBNB{xzO z^bJUGbE1iSBhz%{8Sr(jc9^ACH|U$Vzaepae}7JU$SP;&j+d90_s%e~UWPaHiX)V6 zPiDb5=#gdHcL=uLBKmG-NL zov_#fxDbM?i#}VB@m;)6H4Gp~vM!ntg*`k`U(89`RJ2YV% zVHB+a)=-V&xm_u&8o}*R(b2ZMQfg7XuN(9Q*tb%hM2;zRG(YLC+?bT+!f?20v)krb(GkGzvk3@r_^q%LKTXXDv zQCj8ont+KX@G-u2F4u&2lV9m>xCWWz^TF%}rT3I26-4 zsMJ*3dp8d-7zLwO-B^RhO|EC2B3>>=53yzy`^Nd_wP|Oozk0ed-+hmX$|mem>*wW> z)A(@rBz3G3Z{2Od@K~-NQRdIJS@lWxI^41iS~xxwt9)Glm-IN)9$Y3p+yKhr8l)a* zcl*DjVc!e@?6>C2GH7DJ%b{~nGY!%M1-MM9w(B;YD~GPS@}Q_j@fnW7a!qpjTt(t7 zMYpTAo!a^@d(GbuS(1!#j>x*gR4l-V&tRtP(;ZCvaFoGZ_`zAfIRA$B%YGJIk6IBThmSi@awjN8`IMQhX4aHRS zNF%r}?-AG8Ss0ng3vOpy!oj@mD>=A*%A1R^d`r+}v?`uE8DEA@;9DAx^-;7{nbpI+ zyxPFkqvxBhD9w{favd$oa?Ie@?VAGz_?B~tnVd6PaXPxJsMmw1Px~}Y&KnHz_bRT! zCxaQgC9PR=)|sHfkSM2(+G0GIwrmCsE-~(dI88Y`qW|v4b09dLYVqvZE``uKNfPeh!6}p}nYi=+Gf~`-+Tf!c%2n z+{El$xXZVs`BbK5cPLYb5jO5-9&ol%fTs3bJBv|DA3DI^pOL9iCpd@bv@yA4^_nZC zW+Lq8OWcR5w2y~nel!|PC2LVq=`{Jso+xT>NCK*yhql3|C$`8DwD*e@i8z;8XCtlC zr%&s|q^DkPp^*U|-ZJMgmWJExUCw(J)WTwz`6*V))<9Id_SpdDErK!9A+uJM0MwMC>=Ag?sL?|=#2>cbdJu6QvtTu@z z=SIF*?EPiRUfo-!#ADw2J8ZF18AJq)J?1N1OC_m!6QIi<%O#|x-aF#wQ0f|k7GXaU zV-_bsb{x8D%A@G@F=wxlZz@0fsDo3S(xSol9_Q5N9}p-svr5GJNFO{uUC%P3DkoRZ zEcN2)#ZMS^F2->GqwO|Xsr(v<<7iL&ceI>bf03EGvUh47RL6@zbNKk)Jru^oWF5nO z_;5ykii=Lh-QAs|TzVIyi0fJf-`Y~G6qTEK8j%)QD0AMs8PD0+WC;ogkjpkOD+8-} zHzO+xf$}MoUm_kqUg}di`)zi1y28g-qvm^Ez4x>4LE0q%-$#G%eD)rA8^mj~kpR7A z+Q`h>>dfYoh zLC_C$b*V325NA|1pkIpl9~&TKH%Abk;P-_>r?mES1#iONxd%xF(Qv!eNSMKl1VthW zoYm1FG%Vjg*$s2w^`*{}RMG|nQz8S;u4yqs@}mF*4Gaxw7iiYg-Gw8-naq`8koop? zB%x&q6#0U|X$F?}H1M8A15+CXGZ@i<+|n+xGafVq1O@TP{^}8al%1XZDyWIZD-v@< z0LLtm{&$pD#$nHsV*3F_`NO;5O`i>@LhdcA^Z^bS#U?x^#{s8pUD zXR@9Xp+_f&`tLl9eAvo#(&InyV_E&I(v|&Kxe}XY-N^BgqGmT)9_#r&j*GLWbmp4c zQ=E1|odQXO7uG-wuorwBJ9l>D zTCXFF59!tpfPKOdBBs0Rkzwl6XT@obynh!E0cXn&p@bV`9Lg`g-wJ(L-iZ?Q1aH(e0QyiP0_|ZzwtJK`w)U{JbRBD>UeUbw zD&^04U-I-U8(=xH`$v)SsA-6hrmMbp|GuWK?n8uOm`iz1p=bT7G8o_lY?_#?tSsJ@ z12w?5B3rr^jXzj2vbrasq@>GV5VhaQ8;#{2?{23=!H@587AP3Y`HlSg?v8~LlodoW zSfy&m*=^j3{XAb_qHCFI#S)o5I02@J{3hr2J93H&rU|FqPBlT!O0Oc40wOg=BuFwS zpx{$6U^i-I3?wro6C^FXNgF)ClP_?)We)CQTn}h^`m&$Lin3vC+W5+fN7*$QGG=gg zEGd|A5_>X=UZA8|R>uc&Urv;s7n@KPSP(1qK^vL=7ZkxD*Tx0?!Qr0mQM=Ivc zEKIA?uFY_soF`tlveYoXA2ecxcJ@~EGl}Y$%<+^^1ap~7X=>_Q;Ynp%jSgX2P+eq&!oFKp3_O)3X8p$YfOw?&7hH$npkNm zMFg{+naz08lY%M8!?@-%eN$1b)6Q)GCs+AnD=3vtSrw%pW5R02{Z6y2z^29qCGqxOgNLYi5LTE}CeZt^qmwh}{P)q;4QH(`B}%~tl-i(V@#8gce1v{CBenvcp+-6j@(^Ht*dtRG#M&?Kl zFM|Ofs;#M^vgfcTtoy+)$2(`mN8G+7NJm_5{W>Ojy)&k~fKZ4yDyF^G;h~Lg%ST zEaqe!(_Z7jgP9ubUVer6!L-?U9F{zuY0kc{60qJYvBc@yS?6p1`}WrsU8QZN^D_So ze|R-Y602CUXTO(7zJsHR+XCCtU7;67AqlJ*Wu8x+i%tBl4$-L>5KxcUiYzbaV|Q(( zN^G)Bq{Il``Bt13o_^zuZ+h0&w!8MwX!@c_qh<382gt-Cqe0gM$9R-Xv7N1Y z&8-7mCoA6v7tyufaaicK{8FEjanMz)Hs_~bYAY)?Y z5?jRx$>Y)G+MupwkEZN_=cPe4Y#&QMnZm4{f`x$z+8OW3y_9i7(nzoi=*L^fvTG__ z0JPSo!hQeZy|?FL+A8HPi9o$!3CftvZdN^Lu~I}PG@8c>&%uWojE!^10v|O zcJWjHV5e6Yx21O^dbF*$(di6UA_HC_W@F#t$@T{;86i`^xWDMp(P`(+#_)%R);|}z zNG$CONlqL(j;ykRHk)*7X=4spo(_X)D5ZluvzE%C$M)^NZB7u&R>{YG?lkFmWOSAGZ9*MB3rg|N{UgIf_?XOU_cXQ4 zLPEv!=Cf-633N{k2I%dPI1c>ocd~*nZ$4og9S@D&;qv7qq=xF9{N(>sg^J)Lwphz8 z4Y4otj$_qOMS6wFgKF*9CtTs2JZzf-#dDg2V})LyKPBr@tKZC}X0|nVf0TWzu)?+enz0u$DLVP2+CFm<1K(qGTF~xX%*Dl2^&f z#Ji;X2kqXyb#QKnFo|n%^qSeV*a_Ia-!yKUDO28KYuckTmUP+4*Hn8{DK|l=NrESt z{y6Tfo=&Q*s|5r|7?Xxv6E3$m$7V|Ha6KZhspE2A=(GJVeF0+7oj!3~Pm2h8Q*$&* zv(F(6wN)~^Do0+<1e+n4$1KfzOQ@aAM6etaGEOhb5S>{2eiL5qU}WMrTmC2RvPide=l3$rT9t*N$f|MH=gohxM00X4!_%x)qL3K z`N+$mtu@}KfcUur0{kmE_EZ4}HkMy9_|Y{zZMZp)VbTkZoa(bOnbRfCy|~3bM6p|V zUL8lSEDO<|W!sW5(a3;rCCciUCwS&WRX;oNDo9z`Pr1=A2un#2Qc`;r&*WL?Ef)*u zwA(#LZu(O&x`>E!mz-W8Mp(+|GF+2)B{BQEVKLYk=o$$bl_%hH*h*~Zv>BvVk<8Xt zxg>`&?mK2Y+AYGtHu;A+~@&!@$9!|IL{ z)^l0!c&c^Ki8-D=``ohm#e)9IW}U#hnLOAFskRFRO%(}dY<+Tb`n{P!lR?T#*!caK zcP7Vq#(IVH+Qk%F@B%WJ_KR>IS?uT;{wR7u>9&GufSX<|hTpuc;;0jaR|EvwSaV4p z%`OzNU;UMJu%Rv-N)t=i_K$t0U6Woml}WXBmbzG{%+}n4a@$b5oDbhM>FIBFlzcD> zaG(?ygU#lu95^7VI>@QI@tvwz-(DsjT?g8HXEY+<^#aV68e^o}gZTqi(`BSOSi-o6 z!nxV~i6N5^iTAw5L`b5IHEx$13X52+p`8=u%3OfPL%P$*F_1Ut+#+=o-5w&80m0H= z-2PF_Ddp|o`c(#;;rHK)k55KD_Cz} zO&3<Gwa`OU^Xhk*fkssHOR6s+;Kwzew> z-TA_6`35K@?|L9-ZA$bR9vv9oxDXmgeZ6(Y{r&!kkEvvBtT&p{)ihPHswZi5{`}b5 z;-+=sr+<|ForF=@KW!7IeFtC#+krbnEvbdkoid;6Z3|WzhPYrb9wvV;Je2XCSJyF~wDJ8Xn zh^T=4AXRRq@6Ipqazsd=(y+D7Mg+3V++1l}TU!uDZ$Jj!Q58rZ&W*~IKXT+qYC^&* zXy&W`!q*_nau24UxHuXTZk!AoHq$Ak0?z|WHAL+DlBN4y3i^ju8ClcoPuQ38pEce) zzi=!yJe4I@x;}=cHwwa)Vy#js$NE4k+c&Z(UYLX!8#+ZP-P_by0xU8{_Dgi`8qI55{P|7E7c=!@V zPwz9LkUe{L*m0swkidO(h@N0do6x@s1bh+FwMv6qi;k$V=w6zU@g8iTM~1voBd>ap zHAaB|fxNDLPvK{5JW7impuWWq#aP0-Tf7QkQ#~-(B8NOTXkt!fo5B^X*kh6K);FRx z|AJCfJbsH|Tl(_(GpO5){0#WNiIyh;pYPa1boe_Qaro24SYSA_g*xeD(BP2kj zlmNqeVckATz&=zj^spnYi|>KLuvtt=BkRtBaq`llCnTMKY9zZLi?|XAW5uc5xsy*J zqoXsf{g7NICQXZ1!Z)t5BxgU}Ez>__@01VA|0;C?Q4Q-=>4T8~nP}=KhoS;8#;vio zx=1LlD+j{BWd(e?;&7^q5(Yte+SU8m$otw2=(D?5!BW@mZzoLlT#(!CcuC9PWby#UIlhi@y@#Qh6GReGuZ-MQ!ER~0lu?Ti`?GWW zhPBUVywY}0vqh(@3=^vy^1gpaeMmnxgvcu!(UN{v!XcQ8DN5RG{(>LR-buqB$)c=P zKnvp1g@`^7M7ZgA?8|s9%?+{2!r3aetJGyFW@0&bP`jVXyEgj!MRl;o8W5BV$R7P} zpI9iEnvj)tJXdLlg6Sl*fcdEs1_tb*fv>ba#s zEvcvk=E!HkfY)7XcS_q8KaZ(>hFgU;?pHhY?=H}Mm;6;p2ia8P> zc>7Xjr6-(PTfex}0U$^AY++-O&Bde1j%J-OSuZyGoUH|^E(~lSCA}E4R4V%5>1;xv*5;~{9Bl}4GE%3 zLF`-nxShp4!%F(K`RUVlW6qs?S}mr42c!E`IUTI>ijwbWEaN5jUWr@$} z{#&s+0^O-uGHuYE&(=_KGZjF2RnlS;C>7O4OFquHy^#Pppxw3I(k-H~-KKr&((A$m zQhOOer~hm1#V@1ig6PA8g0@2Gdw6XyEkO?Mt$EUOO%^+A75VJhvpuiRhNO|T`6K*k zdM9I;@_-^-z9^uM1_T|~+R<^f7~u=KefnMn~J+dJSV|t`~xp zCFgB;yIsyZI~N+O*2bfhvum(NtoulAdSOeDPVte}c^NqW$4t>6b5H9#@+YE`)97_< zKS8U+sdz5T@hze$JD_KV6e}+lznHw|580$y(tg8 z9`eU5>^s|evGh_B5=(vI{=<52E@cX^D!7pc-A7MQZDICDCY@&=d1J`w|I8}DPmSva z9;7`t_b%4{d z`C4WC2G&%w=CiUIoIdqwLmXfcpxM1XkqC+?14~(^weG)G=7d)}ScX{UcK-07{4C7h zl^43^wI4zX^Sa*wqH_M1C!9t+7GJ)6K@3D$&z?z`o0}uw*45RC^YZfIr7wYhYrO3b zcT&hI-w#m*Yiw+L=FAzy9|cPjb1h*p;AaeHDy*h@o%LSz!2l>?zO*3#UQrC zcyKR4GHAU?0BqW#Fq9R+Dd&_PQwicJ?c|QLGKiV|1&-q4eQqi}>g4Fnf$1@A5HNlN zSV*7nix~UgB^$7CIZbE|7JIMZJy{wf#cKgbGSYt1@hl4Zb+d1Bjv_`{1rD!Qw^o5u z_%E7HTK^_ILjQ9@C@4_>P7!#T$yvVv2xz)h32{nK5D&RlTiENCIZIOdAzYN@P({ z(Zy9D5vCRj`l9VhGt^}=aHbGrAWDN_-zSJI=LyNq6!Cc@RjjPi(5~bSOq?7!UztW@ zrU2D_6W3ZgF*(@)9)m>Pp5hK^0GYRfxm&%*vFk~q4aRJmF>&`DrlyA$UaXnUk_fSJ~ z_LziJf1!$IyuV;bTZDYn8Yn|Od$4A4vAK?ix;Zo_nd0HHb_!?F+E*avb-#QzSrz7a zu0*uU!K8|{smKwgCANJ58?Wy{85kI7sHofmGoD|QI^u$be(Y!SF51kNC@A3wD4+GV znKh~;8B9xBSspX67ZzDvz$Txz_n&J+WTib%&XH3Tahy{f8KUag+ldp3e#T($J~}-; zUGyObM4v`XSRd4|-OtpAk~<2)03hENT_sX3!d0y|T8$G-3YkFwWxzJR%0c`Jm0tnt zoMMG#y_dP2T>|VNij*nwMJLb3gIZ55`6ic;35APgD2PGXPq3zIk0E6Wq~q9YUI&E^|zv&4FV>AihmB2{j?1U3Ulm)ti=05rnJW9fTUP zCrYw}B4=)}=MOz{mnAHtg3fp}b6T~KpLHqW=}XouFeq)+%vgZ#ATmbYwR;)f6%vD{ zfri|zqMMrS?shR;CYUg%y7~@4PTrK96G6#VO~nsRWC5~@GdU!mW{!-T z?u1L;@`vQZg9URBP$^vZ@)-m!5Bk&$-~|fGXGH zO#dLG>yz@u)N{J<_M*^N6{$W?ZJc_9aN*FCYGuDm5SCsvi^x_1MaNvGp0Jc5{ugn8 z)$-bAu?uQ7FiT$Q8XucwCUR2c>cnoZ&0Bf$UL}movn^Sr>&!#*wW-E^V%cEPZ-EHG z+Ad{YZcnuOdW><_q{@I{dIAW%U98&JfGC&Aj%7YbdxPyGujalGnMdr(DF?I?91q<9 zxOpBWZZj|KWT2A)o81DF7@dQJMr@`GE7aLjX&oKsFgiMeu5lJHbzO9+k>RX0s^i?V z^V^xn1(6vWmabm0%o9xdj6(64HK=FX=llfs5ogLpSqeG(Nsz63vstU=Tqajz1Ewln z=PcWY5}nq<-rnAe6~dn}1bPU~=iEvy?cwmVvol6fdKDIv(`a*oY)+JHp;Kx@DJ#8s z$;0$$-e|*O63dt}5j>v4vren*0`!VB^jUrVQP+(d&v-KUKta``Zmo6pCW=_oJ$R z)x{=MD`4}M3vv*l9BCWk%oi^VMJ|+-iD2{?79pyQvBqM-Ux?FrcPWvGUn$V+yCrfB zrge_LN&ZBSUPfk_nWx7Es9Bkr`J*u(6!#@o8Fv1{x_>36i>TA2WPioN!j3oR2y9}uws(km*&0qxnq-8+YJV4)^YioTf%>M}z21a~Ou!6%{?6Yd^!6HI zuvi!&*qB*R&;}Ceds}&-JWm%ZbhxreE$uM{bjxU6mR@~pEVHaM?RH* zp9fgyJpY&n=;-ae#>LHzj0T*;U@ik&1^62|l5xnyPc16yKnyL12n;sVqHVo+3|8(YCxa}&-H?W>FGWrMe6LR)` zdYo3zL2)+AZRfUo$!`f%)bnWw+YVv2+2?`qr6$AQKpw$ zj!urkCP+Q$^Z5OLpFsF0X*^{wc~)?#44gC?78Ysf$zukplVeRH-pFWZS@6ao@B&DG zstqmRl3wWoZJ4;sZiFs_WN|aCr!r=v zNLHy!*AJ}SWzPQA@6amSqa!Be9~JR1iQ6zYPF%NfOD4cQR2p+;fp2|2+36y2<*F-! zaP;{5!Q_H?nIw`_11H3$hG0tc@*3|4*te$UZvV35+MSF%5;WV}!l5ILk4bX|wkXi$ zNop)&_jc*ps}}U}3x}}Ln8=OYxTB;ZG=x{q!jR~uSv~#|Fg=R{WhCznA#NjJTXKo5 z)F_3v#+ERGrjlz$03N!SXy@K72Ut21ZfN(sseO4F+!P*?xv-XF^&5!Sh1!lGPNU8~ zn}9j6o&B{b{gWdVDexFMdbFRuHNZnDgiGUpbW>2R##4x{m4gS|{e}E35Gy%V&MWWo z(OJMt4m__c$r9P*rbwCVi7YuMhQddJcO9)(3XhB_H^6;>)OIgHvITYQ@0_8vTNEi zlWdvevIbhC$7_2-VG;D0gQ*Db*_4Gim9;uMJA0PBmumQ?3QRBz*9}JD#7rOabCcT{ zqwZ(hokOoZJ}!Bkl5PuWV?=)^gJ6^B*pcz4W91W6fh9d+KHRD1#pE=pP{jkb%RkMj z{iq&AiUk{dl0@$RQ8|cN31m?jWN&5rkd8XUZZz0wOyrZ0Md6TpA#kHI1TVG%rkM?x zo3b6`*!ls`7B{psE(Tav1B5VsC1$o75{tB|*+o0B)bSEi+I1}&v9ntQd$9@Nev>wd_ax@jwX zK5tL@$Ru(C1b5l}F%IQzPmg=`H!QQpuf|AMKY ztbDkeRc=?F*)8L9332!Xs8?*w*77+*I_=k2#$icXWfslifK+xVAvdQt`kCx#5zVk0 z!>|-*5>#D(Z7?jf4x483zMC?nW^C%3%M10E6`b}+-9^O2$lqENsECBdt#en1Q1HNC z=$zXZ|LV59h)g$iwxtS0Q(a;E)o&x(&jdU_68lm@fe^G6sxp9(s6KlJgCKeW(jh+S zXpS)`rkBiCtIPV(z-tI&T|hAh39+~Huw-R8`z`h@LUeeSIXKl6%J3XAU=&|uW7cOQ zL6Uy#wiv_dXk}Fk@bVUYm{HDs59HQ0hb+oQ(g0zg9wLloMq)>-mQE>jEW0|90M}UX zxh{4id%iCCL*X|>yJCdX|>cs=nBUaX6=|YgV&|B|#P(}br*L;f%I2H-`|YM;!}Nuj=toJ+J4IS6iJ445-$7#UpXDH4M7r zV0~@0tb*=dk$sb)uy|EY1GCIJFJE9+i1zZhvFfU3+ZQ7^501W9TrxJW_#TRLjcC3I zJhv?D)`+vO-=)X8By3qpSglKqcRii^`@4WKuD>r$d<8}wN-_TV6_}9x{DX3zHh#Pd z`_K!IxbX0zQT3ChGBG!AZr81duU9?vOv6Nfzp(bq=bMT4;8$dBz9NRlgVGr)(p18o zmBXRHW!TE^eLa_I>?~XvCXAuOmd_XwpmgLg8gnLV9wuycuu&k02oq;Ma>@s__hzt3BHvT>o?&p%he!wgz zVFL%lc4E5v3zf<42ktwgD&(6SG>?-UtVZ=F-6mt_$NlGr4xP|7-X9r7fjhE)#&%g-vUV4sBeHV)dXcaXrQg^g2k84Nl{c16 z0Ky^zuR6cmt>z#@NZRl2zi;uM*l{UAqewM``j{LT;#5H$Z7~9NnY9_1i-7d~D2D#4 z+9Rh(L)QWg&n=}0hYOLYH!CqE_nnVB^vmLKPX)OXepZ#NvhP(BJNxSGGThx>$*M^u zRj{QfJ_tMUiaz+BO^l#x|JF@Gzp#d{Ax-lBE9N3m7X`>K*-roy@?Ds?mupw^f-Vsi zUa$x5&CJZaJUjlm7ZDJU_|dYf@35lo>>h-Wg2D;P)AA%N@q>b|c}!q+zPWovOH{+=Q z#e{@;v}fTp;ZDSCX!wpPFDohbVy{Bto?UY~Q%-QhuBD;n=nl{Kr}OMhnAf4m0HC>` z4P2@Gebv>BNFXZCfAQ@F3~`?28nMA1-t8Y){@Qway=V>%Hwp>-Y(S}r&PZ}{atyjz zpc<$APJ{g}IeYTIFK+*Vz+bg|>D!ZkO_PB8_s;5nzlvbK_4y&=vM~Di_`MKo@fAk5?|sjwZD=^?1f3kfvucKh!Kjsc{(L>Dwzdmex$cRzUjc3O zQ6WY-`dBeEBqoEE_Sc1#78B0=fgAIOA2^~j^mh<_QYXK>YXjJR+;V%$dj6e%9<2RQ zxXz+qXn)gb)MP+S>&6eR@+BS6=wI%yjc}yFemTXfb_LC^$3IozoCw1(0vu5RuvC## z>CXS-0fZ=0RHhamu$&Pak6R?4yVRD;l$o{<>Ty+kG_bNj4 zKrdp>F$-VSv{7LI(o$X{PYr3!v5O6aaVVKRdEEw>qy1>Z@mGW+!~u@*Uo2r(e3NZD z$X;y`As-^qjBmiA_LY)mJR^o~EWOK0Xq0uNdH%zo3x%E=bS!GQ3^BvPgk` zpW7kB`QF;T(kSIin@23+lxlC+O0Vk6Mx!vJR}=`72HWPCYqBCc~{EITDFMOg;t8|H~a@+ z(c#78UiPP=eZ!RmXOfgNupQx&w7BSPnrlW7uGL->`))tm-K#JKoG!#!<1{=8&zou7 zFU{pac$L-3MUD5+;BkrUN&%`pb*cTn2XUN;4q~z7Bpgb@M{Nhf%`)&Sup8+(kb#C} z1f^sZ>guJiqO4K;z7s4_E4@0fv0*F(uGtJ~E>NVskn+Te$T3|-%zwH$m?y8hn>}t-!*KLat08mrp?_s`hkk3*g}DLmiAeoNxyms;-l6zERD?s zDEwhMCYS8C5~g2mTc1j~cNHz#q&n~gY+}zd>uIbiv`qmmgEQX?&O3{Bi{rJ_nXv`I3k4XJx=8E8zPR z#;*kyl5VF(7{AA`WpZ<^jv`)Q!^DDj$cNEon9bz!dtzZd_fg@AN%_8)+Fn`pq7uuT z!Ulto`~%ACAvlBq6zIm(2^RykoZ4SXO!jr?epQ&*`vUx@KD)rG(*}iSMMcFIB)|LJ z8>IdtIH$ZkvL6dZD$@!u_8)mfbMVy*7XvWR%0bc3{2!6PejCxTJn$MtCTC{zBct;p z`3XSZ_@_y&Ru@X=2!;`G?Uh%DKEM#|JkVd9SQJJ_C|iP99xdK@;B~5D;IIO2K^e1E~qarR+ry6J5qsNfzS|GjmDAKM-XQ`#I@A!`zQm$104 z--`slovQ>DEEDN*@ST;*zTdm3IPjQ90s|c#oq+N2>tGD?Kvz#M2q;?UAy5ed7f3WP zQLw*iWhM2U3ER-vI7=?_Ncd|bnuY1`2itEWmOnERS zFo3h@VIx@n15FVKM(CZ$+Vwt&PfngfHw%~|#Uv+JAtRlyA8nrJi!qlK$g2t36LivF z{EuiI@Cyqg0+0P@c|~Y(Edt*?m$1+MwyXg{O0A@=+PGB*!vc{h69Bi;$gmq-Cj~av z0Wsj?sy0!yWOcwdH7D3FIvHFBnI`H=`p7+rpmPoLpMUaA&q=-ehzZoOy`Wb5Vzx-> zBH~GdEHegH%CBx4F-b}9(L+EKz`}yUh#nJtAn!i2+Lx zK>`ffjDQvCNI)Y`#MjItLP@p!W*)qwwjM2_+en&8>ChyEkMbrty}9#KmJR514iOO3 z9|5G4dVlq7vSV-#0H%x)g}MW^>8REuJbU${MbiJ}m6_RiRUZ7oYIhGIvHqwp+*lpa z`0!gx6m^HqJHemAYZgk8!L`;^(A23TW8Y$l1ZN~1v?M0R*m)94N`c9?ta9y)Lv(^h z^6KB)^gl>^zBMIv*<5q`&OZMj7%zML)hgZEog1`grV)f(`G`-lED5Xu<{SLN3GUN@ zC{@{5PuBr??4}JtdpGQ=6XDeJP-fX6(a*L^26im^4c$69S20DW5hJvtx2L*~iYk$= zC!7bad}PrmlaURgW$0f7z_fH+Zi6g02m9ACW ziN%U<`2Tr$*Z>}8-Ayu`H%x;^I%7|(jQRq_0%;7rFlwbUAfEsg5h(B|NpKBT*f>f1SqOy{crfXlmej^p({YfY4}z7ge?uyQW{L^;0(`hk%o2N z?y@+i)MA9=s_%MW>q+P}?|h};9dY&SWl*L}AE3pDYcM!CGKUaJx-ULRvmFO$`S6$5 zTUA%gtug!OMpP4oPrPk8F@DNN%G&O@5CGjF-pnzBr7JPF5*g9|Hg!gt4&x3j`1UvCece&9158fQxbwBSA z9b2b6VD6sV+3dZTXe-q^XM9)oD+5O zVD`#gejJ^fG>StOizJtN+jYkb(e71o4&0W{CeQX6y$rCE`uCAu<-LF2;8QsP=9H-9>08UCjS=rX`wX*E$ax6 z#$etUI8nzqk-77bn-J*5y56xFbr;z{>gIHMLJ!i=;zj0Mhclap<^W^`pAlS}kIqJe zBZHcxHQ^lnH#R0n+#t%SK|FlBg5I58pY)X^C?|=8l4DN3O28(e56$le*EaRrOBAmi z@dMfiX#F-SH(V=qPRIpbNLKPzRj~Zw)oBIX%QLLnv*hG(>hqsKEUu+rdHcv2D;3YA z!<_ti)0I^OV1=%kF%L_a3xT8n;Y)xcyx{y1Ot|J40lh&2$xTSN2}YL7p1CGmc?v4N z^C~>{3H!9uVjpL7dzkZYY$JJFC43;Aj^ZF9>zuYWW)LXGlP_vt=|3#)efwhq7mw1_ zMAy2X^(E1^M!Xgv&f1|uy)wOBv3;()|8lS>r2Boo}mDLL?VWVdtG#VgD2iJcl~ zai~#XvuzU_&J(aO=1}_0JNhbk^sa1z$pM>7P;A%fXMsUYxmd(6$5}zzd5*Mj!|BNG z^V)sKNE@|uc*~F$%}F2SR#)d;XQWpEsop9L$-umV-+x^1+Wh0iytsHlzNSZXsRr^E)Do?%%&} zrQffOH8-cYP&tMk`IM=?;@90Qa$aR0_7wqewO(kBhOcWB6`pSCOW>Az5Ua7M7fp3* zBO^cCYaBjNYAv#K_-e<&Bneby`S((YfRdTwT^)9d}Of3Fkmsh)D4FYqUvs6 zzTQ`Ryn6StNpFQULJ4b01>=6Q8(p=x9)9UK)M5g8DS{nl;v8UCaTPboY{{dJ?oW^- zGv^lF?yO#Pc;(hr{OZ>e~6UtPAz z(sb*(O^cT7Z|PWi^3B_y&Tng%C<*`Rxsc!EwYSpz4#`yJZrwIIdQh&D9P8?oD5jO& z5ba8AqV0}%)O9)o{(om0NxD!B)(I{PW`z)Z9}E@at=gp#zr8dBFH}DS11qrp5U+q9O92qYI;e9G zzY&suTn+a5oOWPB+~PM1^XM%9866Zi8Izb;x$4N3n`swX_kcCZ+k1{_d3kw^%b1^< z+S@m^UtzLK=n$%M%2iqrqq=>eO+WBe%T@I0E6q@HVF|~m!rQ(W4bCe2fN@@h`8f$_Fo40( z(L*SDA3S(4Q*~$gV#QC|csvmZb7R4aRFBXEkey?&=T77^iQFeIZ`lRS(`9Gp7Mo=p z0+rU?a1!-i6;~B+!W@G62Qu2US+>RmF6PTuuDrE5eiTY<0$A{?f@q?1<@t@M2ZAv< zPEtG?+#^+C;?%kFVl=S-SFT(+Q>9dFx^Rwv=yP7doj`y-rymA8TVazG1qof!!kK#c zg9nWjd@{&`Mt1q!={xX2bHY`#>tC)Kc(cHzF1dFp8;m@+?rJNB9I)x&MMw3)^O-U* zFpeGq2F%CLKNbAbPw)b#MSc zpJoeP5hehx8?=vJXYHf#5mtO<(JuTzy&L2wRsr+*<5kFdn1152LC}~+2i2@3U{4cn zLR006^K}QoGXW=wxwZbsi{PXURAs%OOp-zAH_0e-$ofb;O3qy!)&TU^_h5c!3F|~R z_1+6l&qZ>~4^S-;&u9VR(Q!sqJay5nMV^8$>swl`AQMq^Ndx5_FnV-#>*?uTK7Rc8 z+~uRdH5!j65<8)phASs#tGh4A3h7lvzg{&rM>8PY#B1R(Ifzj2X)A=`O&-4#>{qX+ z(;qx~)Yi>ow3XK4qr1k~hx{S4 zQ=@uTyae{L+w-WfQ9yT)U^BfOir4bO=V3OWy>^c-nZL>(u z6ogca*F3Qa*Qk9Cx!u~}n3~o7%Npt3dEPLLGx(vQmxikrHT3*Tal%J)R?St*4(@g% z9%w{{tmhE#gg?6L-2n&$u-re5>|eBm9RX=?!y&^sejJ+C0k1~}5o5?OCcGnmu2hjZ zH!;n;Er@HGK4Tll{0xD+X;vErjg@t}gK~*r8Qx;I6x3!Y8aW8?s+Mr|npk?@-4f-8 z6Vt=(R{LX3Ah+NOeditR?8Oq^k2abatG(x%Kwz|F_5RZL1jWu(ov>1JF5)>mfOFf} z#P8({@7t1!0&F+^Eb>5wXJ%(ZgM|hD61XZ&=XiZT3tRUc(oY{+k5;Z9Miy;wdjm&T z$sXKW*L;>8BS}BSa=d`}^kMPb=&|`eLIK{#@)P z1)js}Zt;9wD8GX|kpS|{f*ICPFtFv&Oj)8B+8t}F5uGt-U!i7of(%fA;|4ZU(dCmW zb8AElx?)Gpcg1F~d2xH}d}9ZE_l#d70(}}U9pitP=hNKkG7gEltz9Zsv_$bb*!@yV zwm6xqJ*y6~t8oB<`3}|Wft#G7X$+o6%gT=(>ecT_nYv4Z%70r^Op&^k`nPNDhqHZ5 zu4UxRam<$7H^&o`1b$W^z(h_cxRkq0Q+YdFw3s0mcOEo;;i zx7(AjSgcLWVQ%9RZL`?}qGPJkBWN(fKx}ynQnaRVm%nuf!zI*uMyCaP`0Is)mf?jx z((i*lNT(dI`!)SuWVF0;j~-Ju@h{0ixa+i{eZL=h#1$fsh7UP2gpZSRn?rGFc(OzD zXQ46D{+%zh@5Imtb3Cfaz8Vn$xjZTHhSb5Q7Z-HtUUIx2S>A@k#YL!0?sg!`hw8rQ z)5XlZS*Our;;<~-r%^~_YMUpR-aa}o5_D#xQ|O_%O5dyJa^2=ned3c>;lWn60*TH-sdBg_F%I=X}0uLzushV!~XesH#fH= zL+0S_*E$FaaBDJ>pi@V)sTvIvH2dDrvTY<4T%HR>)NMdTW@J8JUFP5)3ZBT5{@D$T z_D5hIYRFL*vyB&s#~iC+Z;QmX^~*M6y7oy)wOVv8@qp3&<HAirC$4LgW*6 z{<=QbEt6rl!7N4CDY91LQ@RDPVst)sMGO!WKRp#C>i%gT+i9!^yyW_G5~R>7l++Li z^40q1oOJtBC-}Jq`S~+_qk>1yJ6*e$Uhs>^zitHJ3k+Ik8?*tU8r%@RP9)NH$#vA= z=k46GI$>_2E<;{H{x7N`UP%2oLrtfDpcb0djN27=!YD}IYLtAt=%7Iy>9g{)T=02wR z^ zzx;7r6>}2?tg2`}5APnl9N*X0`gK0I(*k#fL4m8(vCq*0&RvTnVY+|I@m^#MmVs#f z$o+3^+s>;ZgJ?!F`&kShSgWqt+5nn?_h1CLu5D*R?fpwKo2S5*j+RH+3j=d#0lr!@ zvTwoukBVOIe4MOrDcynNRfnlI8bF~k9%g>=si{=-5D1Wb ze0*Pm7o9Jm(dlX+ML>-Sn)e_%0UQC+ydl>OXiQ)*uyXZkI;$~ZotwI4*&LV3UFzqPc! zm0+D!WpbPUvO37o^!Xu)HOmTAmGfPv_+{&M#bCXoSH! z^flK-$J(=P2+06)yw3+8P3aaCU2~z7G9Jc}Wnc3ID(V2#w}rU$24;1lnHS-RXZXlt zDzXQ$_QpTDnU6?)^GxXS+UH|KuHR!|QM^}s77zzECG1q=moW_Mn~F{!nI2Waks65_KBjyH9O;l>Hkvyqy6Lp6Si$5I6wq2#ROErY9OSoYnz1QMdv*p%S$y=--TBEV-#L9a zwRYlxWOZ6iyi1y0pSmay2OI`BXpG(fzP^u(i;J~tz&sV2Z1fO3h9tLb+XkJmYJg~l zfu?4+jPHHUIjH`vKugjp*#rz!$(e@U+ny-^Fi3FBiGmgN>msoYwP3#jI=#J34CG_b zx8c4|Bi~IR)<__-%&s#j52oC}Bhd0~Z24enj1Z|vY=0Pv;4PU<5PjXXxE(ujswZ5! zz(=6mRWL^nROM};Q-RwXQWshrwut{E%ZdLT6q$Kg;O}{|9xWA{8H+y{e^`MzN__$y zHuSjXOTI102SrWEr5a{cIg&##)!C5TZ0{JV?4Q?=<-5x90nbbUjJ(tPw2OFG&T;?B zwsS1+^K-)sWXfV9Xc5}yH53`I7>SjS@7+A;u&YbXw#&ncWDyOvCmS`4&*@Ak4PKWa z(>(o024g8r1=@+1h<7p2f%TBxDiMutl*h2`Pen8X07J>-sh~HLmgxw zF=*2p`HAAr|aJ2*K^7DDXVp4Bc+%R)0;Y)*%Y-0h~d$^rG zQ4^0DLvzQPWC|AUSmWkF`MOKTr4&7lpicV%^(hsbI=Ad+Fqry1t))=)I2dqu#k(Vl zdR}t!p7m+*+}4zpGG>S)=gy!_Wf@Yt>`Di&0XirFTYo01iDTWC%7O~~V*qu`u0wvh zW4|mo_qz;bPUXTf@rr5FSt!4Lh9VLDROfElA@W zp?pGn5xV<5Zho%rm7~0P`W~UDsE(W%w!ya}J zv*o}7{HSz@`sg%$m#J zo-?x2c|8wL;3%F+XQdv=m)3aWXjm$v4c5;CaEbGWv3|`u*RGBWqZIHz*y4{F8A92W z^XZAodzr#7Ty`Jt?r4*o=cX-Mo((i26f-2VL^~lm`nQIrCR&w}-n+`msK)$oVs{T+{1#LPYs8RM+asz4?po#3ZVFTmUa=#2v+ zv93cT7Sy?aE*jtp+uU?a$r+-u*8D?>Ce#k@bbs|j|A=|QVzo-(;FW(U$hYhQ+#1kW zRH>lLfc4Mvy$ja@>x(UKzG9&0D~~k`pfu|{n8Psrxd!qiHDoXV6)}nf0*fc~5Zn>o zzkPcL)gDcqoi?C6WVHm~O7kyxG!%C-2mIOh!=3;yaUh|r-U}MVw=>VPB`vo{1W(Xv; z5k5}?foLR@I)ALjzSZBAm%5ywr5#Uddi6 zn6Am`>7bMpQPjvHMQ2@|Ceg)+dj|RTUb708wt9KU-%(m3;#%F9x1()uQbx4|6kCQ# zZWT|2q#IN}gC~UqY|_(Aqoug{{TT0&u3|4l9kN*_&gEclA5dI;h{zY9!`) zO6lR@aAAGUurIN-7GSSF&4h{X+YrY#^UJXIPrp|Rt;lzyTGLtM=&(!sA+FXAxFazg zt#uP9HO(=^UhkakjVWT;DTcj4HEaW|^Ql>!qOXfqO?24RuuS4nyvFpphfk~Dt>c{` zgCDtL+rHEW8|FMC3!h7wEH9Qvd?UwPsfU0b-;*cg&(j8b0sqZTjdpnjK@s|jtPB41 zYu2qGeu;ZG85Y>fF!IhqXonpzsN2Ked8^5i^hLU;hj6h49qQ|ox{aEewCM_32n4OHPB-V%4+zwG@l zV+dhfw-~v@{EUuEHn_I#(mryaQ^i3jd89v)dM3}1o>+CoO3vSW!iFneqvgL$w;e8+ zKu1}j{i|{X6BSn*L$}vz0z^*Uyvev2MG{?jF**j1_+!vB|21wPD=B)$%v~t&HlaE| zyKAz&bH2%OmoUjzwnvpp)iy79L8n#ddv?Mtwb4zT%>vA)C71C7F9d=Enx2aQ|63}5 zu>y62agZc1i+oooL)}v7T8(V>s`vM-q{9XJYuEmMdD0lnoE3`| zn^Wjj6(G5`8hfE)D6&!~Ep|RPHB>lUiwX0IdLO(5#N?t_Nw>gvW??R6(>0g6Vg*m- z_-Pimfj0c9uh}(IYaDlYsR`p?4kq#F#otLCkRF;#_h=_o)z^Z($jIRTuhAb_tjqcjJ&Pl z&r8Zw&Q#RmI1#k=ACLyjRGy1rM`YS18`b58U+HJ(b%r7s^v%eZ9mq{-7># z56B}jOjA+1)~2PhtoKq|FuO0r?SW`#4$$da#!yj|!!K*>=pkvMriL zHz`nTM2A3_DR8SU(7Zx;Ij4_Do904&*+F`}RP00yliX?_d5xob7u!8peA7o%DJzLE zAb(?%#k$jF-89(sA#0jnCgo8K1+JmW)=P$-mVleFhUDNFWdttNfTW{!`s25Oel+JC zT=8b}(>Bbe8-s?O5)fO7@1ZX9*!yj|{)b<@!Y=0=D2(YJ9)6fN#E*HEl~p$KtgnA} zp+!ng*=C0ZO*Hb(rh3yMXoy>8CFVs`hPngA07Jr_IB{aAFiR<-_SuG;RF`FC+gj(> zu35(V0HN*nxMpWILBTnJ?DZAb9LkC>QD7|r9;cwAf$t|#&e?NN3umir+K9j~c(I2; zL}6p#Y7&H}#o{G+jch#^icC3@HTq4ZY1WeMmx(<74vxRw)+@#w7aJBj2&q3b6ubtb zR?81?v4OtH;x2KY`|Q}%KO=HfAbPB7_a4d`LLL8fNV%K;~s(=5ca1h&WF!k>P> zs@uBsKn5=9gq-ZqP*+FiqK}WyT<`v~PIE(Mijq*CcaRc@grJb1h8 zzd*5|2KuYgsP4Z7s{V$1VPL|-i@!BI3m*Vrg@{tV@XtR3jCUZT@t;QJn1HnTd7L&v zCEy6a)qA*IpGTFUG`z^Dp9wBRU3{(XHg- zf`{TJ^78U1X9g8c$b=BYn5b|9q$37q2T(X^ficFiWy|I+hj;zquDrBv25w-t%Ru!+ zs-1^@IQfxSO97OLo!_@HzaOXxhH$3U@e+;_X2t{8EjfV zg-3)_FxG|+XZcV6^0BKfAp&b+({!lw$Gg3EwmlL@&H;)iL+m}z<<6@--@O;H7ohGx z`nOdnAj!iBZvF7#9I3I9g}j18lccQdfXohk@CxOkAqJEe5k~itDQK^`up}Q<$uMCH zBFuf-aSgrFA>;<{zNes?jn`<61vbwynQL<+u8hyDMdd-vI5H+d;Miu|On2`ql6ppu zC=AHce6gU@M}V4pGWgsD&(F`dm?}c9q#Y&NzYP}~=9<7I>^1qiT)^%9dq4zNMSZ!L z$9V?{Ht42{SWDKQ#u!_61&)X<^7greaB{fp{Opjx>rguw(_V4ffMz(X>6{67T9WMNZD+VG3U+kgBBNFHVL!5iZQ^QVacZ`E#VGO5Xu| z=(FXlTS9!n80c*da?tP?gv`rqa_((HF8T8z1;(UydvfJ6IS#5(xPJ;>VqQqzVS8xx zN_%@gE%`oZJ9sQOwbsPVBiu#4^knI-{7rm(ciC_VskNMWw(Cq=ZM6%W$RTHojZUDd zI#Laca=9j~Z?lkA2neR^Y7ViE!EYU_hI^ZH?Ed;z50wai<>G)`8Nie1if|IL4E;y# z0MyOZXB`BTNEs8{@7O5g;ZU*Ox8OVr5tBpp;j%~S*zCdQ7gXD(rMVe1Mt9=3Xp#nE z3`s-mxMF|uwKqXm9->niG_LlP%hB=hxo3i*kkkHR5!2c5zUU5{1^g`DU$0w zq6Y~NAiQoo4C!=|4E={DkK~0z1z7?>LR;lyM2?;oxUumZwb#zG6H?kQlp#o@L%hb}Yh4 zDoD}$9D0LujWN&Rqd;zJW@B8dR<&7^^EAu`k&&SEuPD2dtKVLu@FLJrOtGfGJ#(Bv zW`K?s^tWs{GlF{-WpFT~IQ-loFw4??8^s5(1_tIe$_;@J1`6R?GOf|*Vt>*6w@rEW zgvZ_5XG%V|Z!^BHNmslK*)Wer2w;|tZe4LUr(7agzxqJmWY??2WPU!W7@z^3`Bww9Z!o9WJ#fI@oZoembI9Eo z^9iQffAzGF9Mh&a9Wl1{tCPNV?v%BGELI{y%01iv#7Nv}ys>bh`!1PZ3naL2ixCgG zeFRd=ch8)K=?bHc)xcni>{*O8kEDKHJe=-tqy0Uj6} zBI`c7v-Io#TY&g5TSQ-%lW?i04t25Ls`GWy6GdZ1UAiq_g<8I#wer&TpoHEhH}BWG%9kbzgs^EW) zjA6C-PGYtIN+oIzP`>-|6DNTD8Ibm0GF_p%U$~0UM~MJk)<=r|`ezjQNC?Ock^KWI z>SAKHL;C0_dI$ndNeKy{e8M>WJ3B|`Kjv+$>~k2W|41lhA>&b8);yT~e?dcD=CNXs zLfI=MBm^I|c^q6d`($JgffXurN#MfbB>kR2yo98>d-|(-chajE*YH%t&xfrISC-%!@_v$ zZ(sBZJA8EQHUj>LkXgD)?mYsB(`7&2*=E@e0Z)wL@>kH+x}D$qeqR;ztqGkWU4Udt zWp`Y^4=;qE!PSa$Fx$8oot@om@8H1dIbd4@x-Jd%9LVwkMU9|S6%|#wa@8stoGM@i zOCnDXNk5)xf5hfNph^BUPl85qXZLNvurKd=zT&{aots6F>G^Sz3QNe#x+Gpc3n;@J zkkISOhD4b553s`L@!X0ZhNuB)XhFph?Uq%tB6*}-QuWGwP|^&gajbM2YWc}c!P^sA z2ryC4fl6~p1i{qAs{AwprXLEulvsi^8jQO?L<8PS7;2jZ*m9|+x)w}K3yM8ORNmYm6oZN-9tkX@CeBJl&n&dAzwt1wCx(6orq|I zq8jiIj{s&elp}>jFlOu_QY2-SdO4&byyr6uF+nV2yDF*X785Nt9AKVz+0ug4sTKnu zNexEWbzifc`9TMR`YrOQ!6!C$c10f{RYd{(NG=RJcV-ktADAu>2HI>OL82w4k=YZM2B*+F1ldigFL zEsyOS&y19u5wjV^9s1y|k|z3Phd5dqP%I@P^ag0-y>D*ptydAt?1`=1c<=CmDJ(dL zp)SBv-`2H3Bh$9KL|>mb9}NE09ui`@fkcr}gF;Tr<+IBe&(54n@3{fAi}Emfh+TF1 z#3=NFucFIAYgv#*5dAHo{G4qu+w{5R977HAdR@)XH`8r8Ue}>84p1`PSpP7QMscq# zXh&m`ys7+1swO#?cyFX4&QEJJ_~}&v$X9dTwFWu)58bGrvyIrnx{Tnjw*Wkhm$Bh0 zl(&XNjrAdNf~W6Sazg5`VEzlU7*D8K4U)jA;ig%@&huLO&mM$nn7zpPf`*9h`6UN5 z1EM9{LXZ!Mx>uA5D4l5f*%sP&z>EJPvzgDMno}(Qe2oIPeD-3w6QwzQU}$$oEpJmc z^d{|L?mIR~Tc$A_W>42i^)RM9>!;SwQq@qKHUKqM!KiX|{JyD4RZP;aFB|HfUl#3_pa z3tC%uc~CWgDTc1Axs^nUNIg^eqGN-DgZH^o(`rI9W1F?k$Eu_V0Hq1X2X5c`6DLiQ zVh;Jz!HM?*)qG``+MSZf8b5`nU_0h!Xm(|ZQVQkWI#c7jONu5V4Ub3esnEBnH49CG<1+OgOTgW)l`Jl3ZY#8oDlKw`c^y9{$N)X+JOez00n zgeED|69l;pGv;$VFDa@Drn7wyiRV<(5wvIP8%ep(#6Vs(^CE~4Kta45yO^c2Kcd~Da z$Rr8GO4isb?uSi9lxN^I2&F@x4oE!V$mfpCX+E87kg>f9Yw~>LK!5(s$NSf)VhEG`@Td1DqRe}CJX)UC=Y$<#ULHY~M30`Zk^GP?E~56(t)NpaEYTA>RW zE9@4j;=!_vkD0a<^D;t^N#WRni6Me5hTEu1xL-eeO>z4hOi%IG-f(}U?}PrNbQJlp z89(=B`ueZs{7yz4LYwv$41qtXk$v4U;6r@y3+g1g~MT z&=$f^DVYFyMhkr4SrQGiiJhP9qH9NCBiRdZU_{)_B-lp$-yNZ>#5RJ`oSRN~r#3qg z1b+=K6LFe?nf6RD*TRbh0Ppi8E1DbW26u$m9yS6(&@=-)cMz@XMcmEZY+5=a7N9N3 z0)YDhQ#)rT{v7&?tn@hrC8}2~TX-f_;4OS(ou!Wel1Q4HP-JFs(K>|Wc@}?w*Z|KH zivp_uU(yr`p;dQVk&2ol=`l#S=vmJ2xDyBNXNkw9B zL#JfWyc@=2vjvM(krMC*{P{b=?vAHLHFXAfImaP1TKLzW4u-mT-#^_Lh&D5P-pR>{ zr7?tBGpvpiqy9}$_y3bi(D?y;MB(e7*?%FI006TE`!@jjw>}g8` zWJ4OTfT*8!K%dYHc$&(U*cd!A0T=?iS1iwq#@5z~+zUa?09C#R(!jd5c#3YG)2KA` z1e1y%&5`ejB{^5Bdp3?@jAB$6TRl3})tDP=JPpW`#_Tn7eUWh&VePdEgiUB?L}+AL zLa3Zm5Qty#jZgIq4Gl}|G@lxRgtQl0;geTlx2H~ejY6m-v*)oM@QxWKam*W}2xOS0 zI>_i;tuRxx88%_Oirr23Qs$WzprP93IB zA0)4V=hnn1v+(^~Io?t{HGkZ-WI~f$iZRf5MkW}l#Ihv)^mnKYO7wm)X&MO}-O7V> zOBeJ*$3DkM2fB?KUmX~9q9=yGunz|^EVDGpU_$V6PuZSRVZM=(+C(N}J#Z|1g8;}V zX2v}*Z_gUO-1_oaOwt2)5Ah~|$<0Oz-R@F`ho?H*t__Zk%Bc7&T%mFuc&;jxI5mis zW=`7#3fi0xjEvNwiHeGxH9hAV|DGxQK;V& zTr2EzgseW%ePe}1@~5WT^Qu5D--GXXN@I*-CBb)k`c_}K@Ku`ssxg3u*W)w&8rm{_ z1DSI*zH7t0B85f#r(zXBax^mQgy;G7*PF#V!dm>sHgofQK3hP$HUX4hx}g73T5-)2 zp~|%a8s3RuDK0-z!Wli^E@z!ukX2#X_G(YJgRqFcloVHQ#g69L`x;uiwRN~v6pQM3 zgtv=q9V120zI*pU)%no9$Vg4vGw_#bpVK|E*jV-i@$i_ShNjMIi>ZN3K63Ez;}cCm zz6x#rW6q$9z&AGIEyJ8Y?u29Rs-a$Rlj!~eB8}NNhrx5=P;<^F2p;^fEQM)8pa zA6>e4?}Dz5rKDlj#p%*EowbV~*7EXv}!(=LrrkCM5wGoa}i50f84*oayGv9il+mbmo{S!6B^I6u6 z8rDIou=B}S(Y%<~PKCRTTeplUm-NJs>Bg8HGrKT5t{W2{FW)UC{4SMUKtE8kt6T!O z9MaY|DnBS#L@u9$%YD~k#e4T0f0}`(PeK1f z{L!&o0iyXaO$vH}xWl{Q*EZsRnmLxbd@e3t7=4w;@$;C4_52)BdU;kLq??7-iWxLE zW(J(WlAqKnd}zxYG^NvMgVag~_nGc<6Ajv80nsY=hxW};G>li2&V1GwogU6pRZ}}s zVq4rTB_DWD=9$ZI+tiMU+lrPNR|qEy=+;l&Q$Y@}H-a$mQ0A&7^DgcHeBe@}1=%|y z+>`3ZGaW3954%Y}^DXjqIQG%Dcsy2yoLkaJA7$3fjW~$~(?<+Vno^AY(++06y75!? z=H8wRza)MC&#{g}aWd^$1%%qL!FMORVrvSYuSn*c!&Qj1J75#uO=p%HLf>iCe0TN1 z)0T?qJt|ZjV_g2Xb+dt5ybN=zOzgYnYbmSHvdk$2AS_u`n6vK8NcxwEVZzjj|O0ib3qEXgoL6wS2(d|u~&$Ei9k`yLz zM58k=t-woM@#Kb-)9?b=!lu5IS0m$nv!EU{?R3JoSneWJrYYB?UYlw+HgEkBtJdBg zxSMJsY9&Y~@h81|H*l+5^*GPbu2}w02o8H6;&S6K@nYun&l@K?=i9qi495@15Oe6n zvRH`-V+Ts~nU>~eED!1CjT8gPBuAoL_C2+UIR!rpAxk9lWppcCWMSx#XT5BTO)#myv$3>U4Zn zmH&+!>tB5tOT>~lXM0xU?k;>%6{Y%)(94{__af`#pk@#qNTyYx0$u%$aiD=#Uc}Gy6iMg&RW$GDc%elj{4b@v5RCf`bp& zsn0l;%;dRbyH4SMS&4lc9hw2KnS6>G>3XAX^Ny8?wx<{p@^24@jr;F(Cw`(awBfKx zvh{m7TT{d`c6v{MgQLMvW(Rc&92qH&^RuPwyWj#l*wmAd_*Aj1pwm?MW$jf76+$)` z#KOHKF_=wse+P!srtm>1j~0WcRbXwHwIPWac5=E^O|N+L9REzM#liU*+O6)FJMl)_ z&vFPcKJU_}X$CL@E=q+_O%Fb+X$H##83X!H6TQYkV;^$rsWFOk7td@>l9XU{EZfyc zJ)dCp=3?qI%F9h5XTX&;!JUiY@lH@Q_}!H2HiOR+W5J^BuBKH~`_2O<@f9oQI`-}( zzDm;3ZQQ<+U_M)}b=`oHHFY!E<7lbEmp2(J7}t$WtxV@@n6ueW$7yhLEycw*_2mdZ zysbzyh5)hT6{Tj{vqnC;vg+Z(W<+x1XMZu}fbD16V!29J>y>HK)Tyz}V$8iyHQ9ce zVU#L{@W{nIJGD_h%v-;d*-j+;*QQ|4zV^DBVDw_Zv=P7~v(<}VV=XL9h|_JGhVA{I zTPtt)@Jz8Fe$}V=@oRoJl4g0IZ>*W2`_`on;+XX8wLZZo*<|$QB(3Keor3qKwvEy4 z(o^0J=YFg^fj1-Aefc=}vHC;a_&aRt^v93oR>6WZpL;U8q%FNhcMZOrQI__V*ohCy zwoVSNtv(&|Y_LdU@Q@lYrcaRhCFB+?80`)O#Z;q*eVOIj6Ma}~>=EK(JE9S8yrOT| zB9>#0VY|K23~RPpMmnn%i2V2rUy>M~v0)+3)8BW>$8iWh8O1q7Ykk)19i!Mh_~?c| zy?S+4d~nfd&C3;Y_NUc)5|pQ1s&y%HTB#9UStE8)4v9C3_U)&8;Je!+fPc}fL--t? z!DIKOl2*D%BMpPub?4O4LppbVEqP_xTxH6Jqhv`jM(Rk~e$zLiHS zfwo!NfvwgFoNseKI9l#3LP&i&SY#tX-3H=+YzdyJ<2_cOzkKk4s$zEW#z8l6vh;27Na{+;(#A3j$Pp!_#kM!NW=dT z>3yww#`*_roGGdsj$`}#!sz%RpFx*C<14FiGo0zT1M{JyH=47URh@Ij9jRs=>^oW? zh2nTV>BZ3KEv7YZXcUzrL`z$Ca&xL!26>b8ukF_EryChV8L4g7#@7!KTS$)z%YhN%(a#Rbn--RUo=7i(@v=E~*SC2*H3wf-P&wuZNDJ}8yecx23fk*jE?F`ls} zy>EFn{bEmQy?DlDMu9u=Ro(E1=5&FmXHW5ZstKRfv9Lhq?v9GlPK_#O7xRyM$dj2k zhn2nB(Ge(647}K8dVF)kj5&_xe}nXjQlJL}Z1-#I&m~+45bewUbR>TQM?YsVTYEjk z+H701r{Lhdj?`U~`}b^{FFwqFr@p5{{jT7Q5Pf(5`P=6-Ce$lanD>Q~#ROchB!jGx@ z9j%$_@=7$V#J_ZTR$9KVyY2Vd+;4fDw&{OvL!ds*kPhaf(aRWP)}WX>)}n2auhuDqK1UmTXx~W|;qRz_nT&JY<6gM0I)&H<0q*#_ZV!|R+tixN z9_prN*%K7OrwSKGtd47V-M{6*V5<2FADbZS0$-Rx&>vurOw_sK*%do|6$^_~e5I;g z)tziL$brjnZB4ay79T9~9X#r$DlfYILYDX}*sq#xEZ!vX=?t8XhyF3$yhMFVESAuI z9?!p=!>8}8*)spF#%*UuiEb%6gfs3=SHfoeyX&WKD%cfg=?rdXZt=^_bdMYra>%2~ zHGaN329@50{`*!N2~)7oPmDf2XUNFhJ$OE2FxF~wfzJo+ zDnqJ#WFLv@wMm_^PcU;2MM2}N9?#*|qghaahI|=}p|7f!JxbWAYik@bAQ_>Ym7~v3=!0#R-CEvEnk-g+e!6T7IYQA!5f?7?q%ZLU6tx7o^z9%y~Q|it3 zkXsMK2W6))@t#n+K6%?Kg%CMvO?&Ry^;2P2_gg7mnTJ`OrJG68XRD-|!-dU!l){N8 z?$(VdD%UBJv7e7mXjHy!N_I%^nu9v^SIN%{zZ_ z-bsnqUsw_UPhH;~*F?8=D@Xt-QUnBPDn&&)(z_@KCYB=-?`tt|AhSZ%3V`LVl1!koYGul2v+lRp1p=hIbXyIJ^{g9(1hU5eOz=1e9s(mcgrMBcGvJ++p}f!C}yyHNSnb^P(x~ zV!go%zF7(FCTqpw%DUExa#-Wv8jh)eyd7q*W}wu{l&=-@U7{?()THUHCZ;vvU88or z0w+q=rxXqa-j%q~_o=7R<*{BU-zam7Ov&l|)%hI{`>Nr@VN~;_O@1gf{JfRumgO0z z=`~bW7N}!bxb-Vev4M6|sq~L$(q{$k?>bxF?LR$}OMm~$l@!L}{t!*DmZcV?GR;}e zG}VqMj5w3UZLf2=LE$|%Bm)`A+&&|scBxB`bCZc^DWNUX=rdz+72`^?jn76 zQ1clw7O(wRXSJg5A(QxGi82Os%Xgox6B_!>rk9y?Tx1d^a~TE%ZVIKH8>PP*d{K*cwkw=8rc$ITBBRKdS=_?QqUP>ENT? zPUKV8;#;bj&d#*&*wClmWUqb?V=DQQLo*j{wzf&MdA&D-?zwUom|)(jOb%PH`+6i# zjbl=A_tFegTj~PR1E96f3lTfHZ%;?%CNKW<=_xvw0#Y!#vYFy-yN!<)T-a(mC8vnM zK@bP29X*HE(hGesGI_h2v$T@99k8v&i%sl3K)%R=2UBo7nZ6$WX96w&-NpA?xaQ(_ zr_NAws0$!;NSncp>^v)qXD1oQjh7+jMbmeW3gFQh7F`aqD$wlnbDZ8@G|F4n(WNzW zSKO2Hx|d_)@Ac=u^IXohv}MO!7Tzutzkuk}FF3M)p-wW?)a5jF+W_k%b;-6t&k$&-stB$Bg+@EF8T71^ex^!5=}r zB+AZx5QlmIU)GAP3?hgsd=py17{k9ZCbMX(!!m`}bgcg3g9Y?+lX_B~C( zS*&sAj$|eS)utFdLE#0aX|pZ-*jK(j7|MN19|CFeCC}f+XWA%(cf*I!aY5*faln1qNG|QD6g0` z72 zsQTBM`2e$uV4A|IEPiwewtxvTh&DP437ELFmYylgOH6tjHwCSEqJSg`2vU0uQGtnd z5Ysf1#R9;#?iy)Sp$(g?-HH~Vo(TU=3R3XqLW$=;rj-ZS>sPMn_Ul2-rahF+5#Qwc zo%w)-?Nfr^3V;fWCbXXfp0-Fq#by4dlExr3iSf+&$@q8kA`m z+ZIgC{y1FQNaM1Pt{p2~_iq30PF4_>3>qaaq$xR{{%ioxu}Cg95t|D_qD%bbC9ss# zJ5NYyD&BILEN?D6OI&y#)G#lc`W(t+9#rJe^2kZp%OKY4iySun*4`V3{LKliLfMzu z7T%=r&-?3{_>hf8Gn0I>ezd7z$9pnXv+sKfHoG@tcP9u)=xx{bpij;ol5sxp%_gr# zdNhV6mkeg{P^65_v>x%A`kb#LI3~D4bl!d4TcRdsDPrt;{z;|aV4m~^C2Kl2Y>Mn6pH>a(+cgUg+AQGq zpa|)4RCC>P1+Llh!9#IKZ?zyLhvx)oOax8&-n>7|nKmzN&srZ~>thCeeG~14;8UexuNS_vk$UhrY;EO`Uu!p31r$sDj zmsW7=*WG55y<`ql67^Y_+1Xi#Hay#_eBfgl-_3^N@u2fLX!rcO>R^Jz`5F3cF?*yZ z^sfRO=b~4iMAWM2a_{aePy1X=jQ;NJYK630l9JFGxwms>DPLUzxD#yoe6z$7cEW51 zUserx0rNx7L3YROh}RR)FJ{5@9>uHbnUs=3==K2Z`OuG^nTCmV>f$j092l;0B+WEn z<>q|i{)m?*wIZQCFrYYv4;wDjD&*f%2bx>&Ji~T@o_E^Lw3f?C<=g5*+|K4D;_?K+z!X5uvo38pYQX1j{J z9_TkQ9&(>T?qsNwSPR~wCA#*-S0xIsegPw-LzT5+gu_;-gzFz z&PBMYag|nA?1Rp${w<`3zBR|nwm^wN7H3UagD|h?a**n_96bAO@=0xlO{Z(W)MMDm zk~hzYaa51C69@iLKhI%Kyf*i{T1Y^J_Tv3Atg{AFU6~kP8d=pGF@3b#Noydtb3Fw( z5hCRU9cmU}A*zHv;yq~g(x50eFg&S;sJPZtcy>Zg^xDkPY<)p1)s#0g7TbFrI-!S0 ze3^GJ_^4s!f^4!p=qQURnQXSH(nR(O>P- z$A*c~s93{Q)YI-T3vmN|aLMQT73-h%NX{h#bOB*~z`Pz;e2->wrlwr-BM&7wQ6ck& z%CXHeSX~Ia?vTZ1`sYaH6F*gVMfpWdkYb!IXI0^HNZa3gLDa`iZs>%qS-$312SIso z6N&nJ?TK2G3V)G)7YAziZ-@z-7b~IoJ2w*H;+XSal}v;c8_~3nxp^|TN06OY#{0+I zV4I9LN~$ubX$S6+Qq6V{C;eSA{YHto>7gqyC32rHMNj>zG}tMJ%KE2%byZbI|X{iS}46g(MRIX^TP=giJh zV6^{Agg#@8`TnQ$#fncxRY-fY%9iqu(F#SfBmq5;Vo-n*fvBpw^Fm~w$#u}piLv^6 z&R}ud@G>P-%;g}3jwQ8Zux?WAO}SvSy4s(cdqm%&^bUh_7fpb!)GiL3`Z(XDNxThI z^W;)dBl~{FNeOle=f~6OcMARSy%L;*8B_fJ_TE|lhYvFmF{P4xh+*FzE7B}&k4~D= z#VgW@5L#GMydp8}$frf2ZVvmPKa_060mAvWH2?HURxKf3mwmX6AOVx$>9F(!ri7uP zH}{B?G9R$;vXm3P(q(-9G(y+@=5wTI5u2QfjvCSVSRAK(OeEqh$Cwp;9rd?wzjP(~22u?CI^_7eUi;uy-?B&^jlaax{@9`FR(#BiUq&Xl ze|i9T^7Woh(9EVdTR2fzJAJo2oKrQ*Ue2}_e(1K@F1b_(eD6d%NBRo56- zGSGwWEjdKrJ&ezIWi1}RSiB@~n!+Gk@#0D*a~s5Zp?bQkZL42JT%rv1!m>ZiA`iaa zO)Jk5?_vpJJrD-z1PYxvT508Z@NtmNt{1s0Chqab^Eg;F-M&!8eK(EZy)#ySD|~C# z-`oDf-ov1Usz&)!_OT+mZOGa6n$Mg!$dI56?b9Cv>)!){{~A$(hwkQt7Ry{EO$jm{ z`Otm6(_c26(e_jJsy6Byn@KtGY_S1HPa-_@fc=ww^DM;iR_)8b1i zDOUpNaj|&#N2T?=({JQTbk%0^LSL>HuRajX(dOx277-4nih3VUF7ukq+?v(`0Y95| z_cc*>@0GgOpM&i0a9~o=iqDao%|~|D-W4V(a}1jkYCNO($)I$=ifHBb+lEaRugQW~ zD;1xS_EhS1LN4fxaT-uaLN^@Lj=k0R{+v*ahan$X#b6cN=KE&c;ps+Ov9I{CZ+T?` zf55V8pQHmeAKqjo+Z1LkULqWxo0r~IjD%J0yRFgCPac@N^$t7}K?ygNPS?Lv6U6o$ zka3B|qqXubVI4VUE3#S0V`qgN;SV}_KqoW4C%~`2|9eOkCp4I~JrT9LOu$MA-R|u6 zdj)C-fGu6LonuiGbFGs)xYmZ_4bu<1??-9J{z7h8fh5(C zZ}w(!n8lnL-r;Ho)7OR=+^d#v?e@Gj3In1fRwkSF(w)SCX_R1T78-`At=I&O^Uomm)1ALMDj6fv@~cq^SC&X?6R^Sx){tNuVz`J5;>g|sT;?hyW?ygR z6J2{&kBR{QYv4bhyI&G>4S!fQOS09+LFi4{R}6j7^vK*p!<I5Fnsm533Ju@_a+ z+|)?wKkPwQVlfl+?R4Kimf#)R!~Bw3#}tk^c9dua?6X}DQ}{OEZ6O`v?uQD0M2XL4 zpdx;X?u(_eN_7C3&HG8RGOary0e0tq*-rZZJEn2D+_a4+dYwY3^tq(3Z6xyYj}EC7 zEk92yaX^w33Y`Ytc_Hi7GKTvwER{!oa|IQ8ovy5IPZ;aDJZAG>D7$*(EL(}{^%r1w zEM`HxoH6@|Z~RUs_IeUDHjN;S-Mc=0QAr;r_fjEhLETfWqI+wOl$Q>B%zip$1EC7b zmNYhMLK>d>PkIPTM=UPabB#4_r}1G|+gJ~d{1sH{*wY6^-6-dr+ib`1jV#-E*j?R!`S$yqx~7eeD+Bd;tFT+EO~rJAut> zDbEJ|2Ji-O(){hXKQqgCjm&x{7j3Yb=Q|SES{P?mEW?gXbI zMeF@%)AY0gbbZ^P9hFmd9`*8CKWk!!tIq-wm4>HD(#7-vBE@5o-mTW&f@E&Unba>I zu1-~ctj_Ia-`*ta26I8|LFIgSW$S^P)&W0fed0lsx~^Sx_c=4*nMri2_+;+%fvmsW z_)Q16^U{YdZ{C_^&syzkwLr{i46XQO&0ZJKesEiqLd2fHl)f&mGgYr`RTaBV#!0nZ z8uJ*N)>oTV&c|bxUaz0a3^Ya!_Gr{SI;z;hJopF2*mR>erfyc7zv<@HLKy91P`5-L zB;nt!EI6T>@9eW$50pM~hP_VQ7;D)66-)E8oe&=PPCBxx|G=D^2;FSM!$k0}5*m7d zePEgAst~ea6rF61sj7> zNyh%_p(ywfn>D_Lmr))`AIV2I zPU``W_^eTdKSh#lqc=3BDumi>=MxO;Y?nP`M}M%v_*d!X0eCGJE&k+S7Qk8_jcU$3 z#1aKRc^XGJVg3?3cKH7I6``Cfgk}X|W1)m>>Ehi}+fDN#>yx;D4-N#{BG*K>{#&3O zxjVKO_&YMl_+9I`bXlEV(Cf|Yu`qzi`fsd$rhlp9z3)7VcN@2Ffk*v-_d_oqu8B+> z^CyHFCD@qM9Q=4fwraC-w6J~?CT0mZN#R_4SS(!H!}*lHetmjp)4?Bz(1 z{a{+w?y2khaQIl{<`s)Ou*A%%=u|b&=al!dkp)(yho-A^C8iph!TOC23%=+;dEOn> zg>m=ljfw@M*cAYzj=4y~S(JeV`Vyj3==^^}w3{}O>IEa=KE1n8Z_JLyx}F?W75vpI zm~SyU;9)Q|jhoYui54RvH$fHs7QP#^|6k#B>@*P@VqnuDUf^tSW}^)LFIPK50XDvn zXsaSWe#b@e!8wTR(`>46Rajf!lR$Eo{I0i8mHVW*-j-Qof4s36G}_1+ay#X{z4}YM z22xj{9amV+`QuS5P9yTS76UIl`w8{q7gkB$!r>4b;qcz3zI3v#SOuX^En@aAhzpC* z*Pbrla#>!)U!~{X?owWpJHLk<-D|usCjR;6`v=Ni_1?Yv>hAhWn~pfroKVr&Ye|7G zI;1O+$p|Pnzoylw0qB)mzPG|XjCIey@cIcu(|fUp!K{X5QqT=?*v~ThlDBTksWTlD zB)-0|#B%$GP!R2nyBmS!pI)i4zVDD$wGKZTJONZOh46u4F;yQx_?i|T%+DX(#$O#j zi*LmAJxtuD^Z;I0TWoucp}Vr|`>MY|zSmfXsZ6%Tm$M&huI2Enge;1i9}^)M?vp=h zxQ+lIK1&TUP9&&Lk?VdjYfWWA7lj43B`XCmY(IEkxSL&X*&Y1-V<~q?QZ18)O4Nlr`({|z)CkB#*4++pa+US7n3qId|Kd!G3sr~=o-MZtv$mqQnEU$#MR&(_4QxVoACAN*6mvi3G_;6l1@wfA(JwEc$# zxS~H7H`K}MM+l)H%s4p}dM5g+i4??Zn+b_1cV?SgYZc@z+hcTTGre!YpjB~s+a=AZL1O=o}bGTYYpkdTA) zSz-q#3zXx^|4Fw`@P)%VR7y_KZL2#sZJ2 z#=b!LzcTuXlvMk-7w?2%pUA~z7z!HoH2m92h{fYKPtCjU65wtz^2@z-R;2(C+H8l! z)iTPeWEmF?aV@qU0f#1=6NHv+S4ILd7<}~rANlW`n~^~7>)^CNvyl=W!8xRAxN6jK zE#`T*+kv?Dm#g4biI|t2E9r{0-`GF%)=~dmHkFR0KQ#TCOn-xXV>!nETVs4B@%86~ zD-R1ZFt@*Ad=#A)&o3O$KA*1h_7kn&6>ch^_r}Sea3TbVh`#gC_!`8sajCn5l({u$ z;>4~$OBwu28uP+bhS%c8dsDW|Ij}i2ZrVnbY-c4{8hx5R2Vq>S4WkA_tov^rA2-+a z;K}6>8jrWE^OQPELy$1T;J1b=V%RSV(PuibH{hj){X`VnXIFhA-kt2}f)r7ouR5t1 zUa{jjpgNEtiF13cFdUN3hb02(6E8C*i{zS#q)ZP<1V!Ia(;O-L{2hqD^%w0LM_L5+ zfS7zFN)VyS$*!8*lgssH%3$Kcl%{QTkLzsV7d=R^^^`3cNmm1IwsY5Sf>Z+Bv^vgM zB6Xzi-i)EgF7*abyYfjxI^&jN2!D^n_?`|~CyK&oS)|X6N;l<{1;)!WuLNo* z45uW;t~crxUYX&L&uJ6*pu+BVt#PchYe?;N#Fci(mWIqERT5+3dkWLa^c$)~FGu;? zurIPS&U0Ot&VDNJFn1nI?HLP-w=qsPlPPDjc@QbQTJo=LI_yf>+PQWPuWk>u>vzA7 zAi1jp`F)S?J*{vImWxr_HlvUb7D!-7;=}fz*M)KLoLT=&b0#)rS8R^V- zM$}xEDZN_g_UM$+c7GjJ>eGxG>dg1-vnN|^uYE$C!pV{7=T3&Aog!?}!z&lkZFY4< zC6f?apW@7)JCo0C;?3T1O-EJ#RRa=m&B^3M4&A?j6#Gs+j!x?b4x8b0X^-h5a=*oe zLiE18o44;Cfw}EjehOQxTZ-ayF^n$;Z3YQ=6{p-VdAc~tfz#=~hcjLOnP5d_e(`ec;ArA6-tdU1DBYdw}z5`i(`Yb(5C|;Peu=B0`P%km+U$lZ#!a&_Hnr ze`TN*q1QH1ZF^NW|JYFI<}@ob1FAN5A9ppRa#B9PT7MOY2Q(G(|4cr-OMF zLx*&-%6Da1oj2sENt_PZ{tM1m);aQuZFNOe@eo7Jg0{aNMsG+qX_A^ZrIY1XPL5i* z)sI!L6K5#Oe24cl|7~^-@fHaP1y4D4(A|HA<0HMo;sN{Q_|wMMK;Y{{moBKivCzie zW~kWBg7}`gVTZ~-aX*fb4`&-?ntHb<9mDv(_-C~zvtE#_Oq++T4GCf+hsTp;-JLp~ zo6m@yQImnNX7vQ46JtyDXjvj>tAs~sR86@)uboH~VqLcKma>L!Hq$@OnP?&Qspl@# z_2re#nVb!7rB0HJ51i9$VUe1F*@uYEfv4y3Nk$Plh4ea+~ewqd(j%&<4Y@4v$Q^-jp`Zl z@ERKC#||qes>wYU%=>2&h^*Fj_c8`e>WFF`*|?nY;Q~BMy}(dn_5(ecbzwQVe;kQs zwtrp5d=VD>B(r`rVNs(XE<|!_IhR*>%VW6pqUZsZCBm)gy(vhsOIV9EE(d8pCzB_LiOrKSUy$-+bqZbBG57Ln`FFY^qWTL6X_k6YBVmhdD`A;# zzUdRdr{gmBO#&5H*i!K!tJ_D$3W3bQ%f9P~fmZAKzrYy`!j2GI@tPI#!FMaRT6Nc^ z2=))Yv@9Ai31a8jgadBA9`~`O?%J@P2Bwz^)$BJ|Gb2RE3i6PVnFX#IENbDD;9j>5 zkm7O;@&s}5b`fgz;5VTcRTn4-+@WiR!gOCOW99_OI zB>+;imIj*G$S%)&`_DlGWLT!9op9lp1oF#{_uqHAVt{Vk*e)CMfni!Qn}hbgw0aV4+I|>XUblX3C6lNPDEp6rk_QFv>ZIWE| zC~0azj2TISt2v+v%74B3Z4MFxY~!~LTBC8D8x@Y3@0hKIltL}qGxWPig8&+O04a8w zb)7ZB&HrG!y{)+AV?s3kW?gf9Vi(?NG(d4`K~5Pp7avN zeQg`HW{McbfpF&qJCvRW5YN-FV5_z*GJBP znt+n&ll^>4X4#Nwb36i92|ITN#Ah+KiNfmbxJlP3h@89So+*l=OKJzZ%xUrZV%y%Q zY|-Cws1XZDRbb-UJF>7fU_goFwZNmzt;vhmfSS=f zD%ql~#ZA6TIELr_=6Em^P{qlXKRXUIP=0{}cA%P4yf|&&g&Me@9-6RDi^Z4`*PRhg z?J+h>@V1jtE77$q`|>eT5^KK)}^b z3tqCkQKj^}n*e>s4EP?r10ugEQ4RoR^B2I3sRjdbrqwAvn}AlD`NdzFo0t*B`pZGN z$OE-iGv*cN1!uEo9vc9I%KYLS19vio3sYHNt9Y^w6ihA@4KYJ0`+&-h_S|(bWI@&+pCKH~&A!@?Sj{#Qu*9 x{?$uC3V_Q0_c1MzMx_1!ywT-#Kdf+y@1rzk9%G!^E;*CxPqm(uKYkPTzX0+W^8Ek+ literal 0 HcmV?d00001 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 index 677ea4612f..628997dfa4 100644 --- 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 @@ -16,6 +16,7 @@ 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; @@ -46,7 +47,17 @@ public int order() { @Override public List getAdditionalHelperClassNames() { return Arrays.asList( - "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AdotAwsSdkTracingRequestHandler"); + "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 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 index 4ead53f5b1..edd0d14564 100644 --- 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 @@ -15,11 +15,37 @@ 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. * @@ -27,8 +53,13 @@ * href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/release/v2.11.x/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/TracingRequestHandler.java">...
*/ public class AdotAwsSdkTracingRequestHandler extends RequestHandler2 { + private final AwsSdkExperimentalAttributesExtractor experimentalAttributesExtractor; + private final boolean captureExperimentalSpanAttributes = + getBoolean("otel.instrumentation.aws-sdk.experimental-span-attributes", true); - public AdotAwsSdkTracingRequestHandler() {} + public AdotAwsSdkTracingRequestHandler() { + this.experimentalAttributesExtractor = new AwsSdkExperimentalAttributesExtractor(); + } /** * This is the latest point we can obtain the Sdk Request after it is modified by the upstream @@ -38,7 +69,20 @@ public AdotAwsSdkTracingRequestHandler() {} * href="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">reference */ @Override - public void beforeRequest(Request request) {} + 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 @@ -52,5 +96,23 @@ public void beforeRequest(Request request) {} * href="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">reference */ @Override - public void afterAttempt(HandlerAfterAttemptContext context) {} + 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/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 + } +} From 91b6a568ad19c65ad46f3c4dc3921ffb04b287a5 Mon Sep 17 00:00:00 2001 From: Anahat Date: Wed, 13 Aug 2025 15:53:24 -0700 Subject: [PATCH 5/6] Instrumentation Patch Removal and SPI AWS SDK Test Addition (#1120) This is the final PR for the SPI aws-sdk instrumentation. It removes the [opentelemetry-java-instrumentation](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch) patch and adds comprehensive unit test coverage for AWS experimental attributes in both AWS SDK v1.11 and v2.2 instrumentation packages. The v2.2 package introduces 29 new experimental attributes while v1.11 adds 23 new experimental attributes. All attributes are now tested through unit tests and/or contract tests. ### Description of changes: #### AWS SDK v2.2 (awssdk_v2_2) New attributes being tested: 1. AWS_BUCKET_NAME - testS3ExperimentalAttributes() & contract tests 2. AWS_QUEUE_URL - testSqsExperimentalAttributes() & contract tests 3. AWS_QUEUE_NAME - contract tests 4. AWS_STREAM_NAME - testKinesisExperimentalAttributes() & contract tests 5. AWS_STREAM_ARN - testKinesisExperimentalAttributes() & contract tests 6. AWS_TABLE_NAME - testDynamoDbExperimentalAttributes() 7. AWS_GUARDRAIL_ID - contract tests 8. AWS_GUARDRAIL_ARN - contract tests 9. AWS_AGENT_ID - testBedrockAgentExperimentalAttributes() & contract tests 10. AWS_DATA_SOURCE_ID - testBedrockDataSourceExperimentalAttributes() & contract tests 11. AWS_KNOWLEDGE_BASE_ID - testBedrockKnowledgeBaseExperimentalAttributes() & contract tests 12. GEN_AI_MODEL - testBedrockExperimentalAttributes() & contract tests 13. GEN_AI_SYSTEM - contract tests 14. GEN_AI_REQUEST_MAX_TOKENS - testBedrockExperimentalAttributes() & contract tests 15. GEN_AI_REQUEST_TEMPERATURE - testBedrockExperimentalAttributes() & contract tests 16. GEN_AI_REQUEST_TOP_P - contract tests 17. GEN_AI_RESPONSE_FINISH_REASONS - contract tests 18. GEN_AI_USAGE_INPUT_TOKENS - contract tests 19. GEN_AI_USAGE_OUTPUT_TOKENS - contract tests 20. AWS_STATE_MACHINE_ARN - testStepFunctionExperimentalAttributes() & contract tests 21. AWS_STEP_FUNCTIONS_ACTIVITY_ARN - testStepFunctionExperimentalAttributes() & contract tests 22. AWS_SNS_TOPIC_ARN - testSnsExperimentalAttributes() & contract tests 23. AWS_SECRET_ARN - testSecretsManagerExperimentalAttributes() & contract tests 24. AWS_LAMBDA_NAME - testLambdaExperimentalAttributes() 25. AWS_LAMBDA_ARN - testLambdaArnExperimentalAttribute() 26. AWS_LAMBDA_RESOURCE_ID - testLambdaResourceIdExperimentalAttribute() 27. AWS_TABLE_ARN - testTableArnExperimentalAttribute() 28. AWS_AUTH_ACCESS_KEY - testAuthAccessKeyExperimentalAttribute() 29. AWS_AUTH_REGION - testAuthRegionExperimentalAttribute() - Tests leverage AWS SDK v2's getValueForField() API for clean, mockable attribute extraction - Includes comprehensive testing for: - Core AWS services (S3, DynamoDB, SQS, SNS, Kinesis, Lambda, Step Functions, Secrets Manager) - Bedrock Gen AI attributes with JSON parsing validation - Bedrock resource attributes (Agent, Knowledge Base, Data Source) - Authentication attributes (access key, region) ### AWS SDK v1.11 (awssdk_v1_11) New attributes being tested: 1. AWS_STREAM_ARN - testKinesisExperimentalAttributes() & contract tests 2. AWS_TABLE_ARN - testTableArnExperimentalAttributes() (Service identification only) 3. AWS_AGENT_ID - contract tests 4. AWS_KNOWLEDGE_BASE_ID - contract tests 5. AWS_DATA_SOURCE_ID - contract tests 6. AWS_GUARDRAIL_ID - testBedrockGuardrailAttributes() (Service identification only) & contract tests 7. AWS_GUARDRAIL_ARN - testBedrockGuardrailAttributes() (Service identification only) & contract tests 8. AWS_BEDROCK_RUNTIME_MODEL_ID - testBedrockRuntimeAttributes() (Service identification only) & contract tests 9. AWS_BEDROCK_SYSTEM - contract tests 10. GEN_AI_REQUEST_MAX_TOKENS - contract tests 11. GEN_AI_REQUEST_TEMPERATURE - contract tests 12. GEN_AI_REQUEST_TOP_P - contract tests 13. GEN_AI_RESPONSE_FINISH_REASONS - contract tests 14. GEN_AI_USAGE_INPUT_TOKENS - contract tests 15. GEN_AI_USAGE_OUTPUT_TOKENS - contract tests 16. AWS_STATE_MACHINE_ARN - testStepFunctionsExperimentalAttributes() & contract tests 17. AWS_STEP_FUNCTIONS_ACTIVITY_ARN - contract tests 18. AWS_SNS_TOPIC_ARN - testSnsExperimentalAttributes() & contract tests 19. AWS_SECRET_ARN - testSecretsManagerExperimentalAttributes() (Service identification only) & contract tests 20. AWS_LAMBDA_NAME - testLambdaNameExperimentalAttributes() 21. AWS_LAMBDA_ARN - testLambdaArnExperimentalAttributes() 22. AWS_LAMBDA_RESOURCE_ID - testLambdaResourceIdExperimentalAttributes() (Service identification only) 23. AWS_AUTH_ACCESS_KEY - testAuthAccessKeyAttributes() *V1.11 is harder to test:* V1.11 uses Java reflection to dynamically find and call methods like getFunctionName() on AWS request objects at runtime. This creates several testing challenges: - Mock Method Mismatch: When you mock an AWS request object, it doesn't have the actual methods that reflection is trying to find. The reflection silently fails and returns null, making tests pass even though no attributes were extracted. - Class Dependencies: To test properly, you'd need real AWS SDK classes instead of mocks, creating tight coupling between tests and external dependencies. - Nested Object Complexity: Many attributes require traversing nested properties, which means mocking entire object graphs with proper method chains. Contract tests sidestep these issues by using real AWS SDK objects against LocalStack, testing the complete end-to-end flow including actual reflection behavior without the complexity of mocking Java's reflection system. ### Related - PRs for aws-sdk v1.11: #1115 and #1117 - PRs for aws-sdk v2.2: #1111 and #1113 - Replaces patch: [current patch](https://github.com/aws-observability/aws-otel-java-instrumentation/blob/main/.github/patches/opentelemetry-java-instrumentation.patch) By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. # Conflicts: # .github/patches/opentelemetry-java-instrumentation.patch # dependencyManagement/build.gradle.kts # lambda-layer/patches/aws-otel-java-instrumentation.patch # lambda-layer/patches/opentelemetry-java-instrumentation.patch --- .github/actions/patch-dependencies/action.yml | 25 +- .../opentelemetry-java-instrumentation.patch | 4193 ----------------- .github/scripts/patch.sh | 14 - dependencyManagement/build.gradle.kts | 2 +- instrumentation/aws-sdk/README.md | 26 +- instrumentation/aws-sdk/build.gradle.kts | 15 +- ...dkExperimentalAttributesInjectionTest.java | 220 + ...dkExperimentalAttributesInjectionTest.java | 274 ++ lambda-layer/build-layer.sh | 3 - .../aws-otel-java-instrumentation.patch | 8 +- .../opentelemetry-java-instrumentation.patch | 4 +- scripts/local_patch.sh | 24 - 12 files changed, 533 insertions(+), 4275 deletions(-) delete mode 100644 .github/patches/opentelemetry-java-instrumentation.patch create mode 100644 instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsSdkExperimentalAttributesInjectionTest.java create mode 100644 instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkExperimentalAttributesInjectionTest.java diff --git a/.github/actions/patch-dependencies/action.yml b/.github/actions/patch-dependencies/action.yml index 048c480540..9281534275 100644 --- a/.github/actions/patch-dependencies/action.yml +++ b/.github/actions/patch-dependencies/action.yml @@ -2,7 +2,7 @@ name: "Patch dependencies" description: | Patches direct dependencies of this project leveraging maven local to publish the results. - This workflow supports patching opentelemetry-java and opentelemetry-java-instrumentation repositories by executing + This workflow supports patching opentelemetry-java and opentelemetry-java-contrib repositories by executing the `patch.sh` script that will try to patch those repositories and after that will optionally test and then publish the artifacts to maven local. To add a patch you have to add a file in the `.github/patches/` directory with the name of the repository that must @@ -49,9 +49,6 @@ runs: if [[ -f .github/patches/opentelemetry-java.patch ]]; then echo 'patch_otel_java=true' >> $GITHUB_ENV fi - if [[ -f .github/patches/opentelemetry-java-instrumentation.patch ]]; then - echo 'patch_otel_java_instrumentation=true' >> $GITHUB_ENV - fi if [[ -f .github/patches/opentelemetry-java-contrib.patch ]]; then echo 'patch_otel_java_contrib=true' >> $GITHUB_ENV fi @@ -60,7 +57,6 @@ runs: - name: Clone and patch repositories run: .github/scripts/patch.sh if: ${{ env.patch_otel_java == 'true' || - env.patch_otel_java_instrumentation == 'true' || env.patch_otel_java_contrib == 'true' }} shell: bash @@ -101,22 +97,3 @@ runs: run: rm -rf opentelemetry-java-contrib if: ${{ env.patch_otel_java_contrib == 'true' }} shell: bash - - - name: Build opentelemetry-java-instrumentation with tests - uses: gradle/gradle-build-action@v2 - if: ${{ env.patch_otel_java_instrumentation == 'true' && inputs.run_tests != 'false' }} - with: - arguments: check -x spotlessCheck publishToMavenLocal - build-root-directory: opentelemetry-java-instrumentation - - - name: Build opentelemetry java instrumentation - uses: gradle/gradle-build-action@v2 - if: ${{ env.patch_otel_java_instrumentation == 'true' && inputs.run_tests == 'false' }} - with: - arguments: publishToMavenLocal - build-root-directory: opentelemetry-java-instrumentation - - - name: cleanup opentelmetry-java-instrumentation - run: rm -rf opentelemetry-java-instrumentation - if: ${{ env.patch_otel_java_instrumentation == 'true' }} - shell: bash diff --git a/.github/patches/opentelemetry-java-instrumentation.patch b/.github/patches/opentelemetry-java-instrumentation.patch deleted file mode 100644 index 280bd01bad..0000000000 --- a/.github/patches/opentelemetry-java-instrumentation.patch +++ /dev/null @@ -1,4193 +0,0 @@ -diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt b/docs/apidiffs/current_vs_latest/opentelemetry-instrumentation-annotations.txt -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 --No changes. -\ No newline at end of file -+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..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 --No changes. -\ No newline at end of file -+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..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 --No changes. -\ No newline at end of file -+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..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 --No changes. -\ No newline at end of file -+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 ---- a/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/build.gradle.kts -@@ -47,6 +47,14 @@ dependencies { - testLibrary("com.amazonaws:aws-java-sdk-kinesis:1.11.106") - testLibrary("com.amazonaws:aws-java-sdk-dynamodb:1.11.106") - testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106") -+ testLibrary("com.amazonaws:aws-java-sdk-sqs:1.11.106") -+ testLibrary("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") -+ testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") -+ testLibrary("com.amazonaws:aws-java-sdk-lambda:1.11.678") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrock:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockagent:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockagentruntime:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockruntime:1.12.744") - - 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..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 -@@ -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) -- .hasKind(SpanKind.CLIENT) -- .hasNoParent() -- .hasAttributesSatisfyingExactly( -- 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(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)), -+ 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 -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/library-autoconfigure/build.gradle.kts -@@ -18,6 +18,13 @@ dependencies { - testLibrary("com.amazonaws:aws-java-sdk-dynamodb:1.11.106") - testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106") - testLibrary("com.amazonaws:aws-java-sdk-sqs:1.11.106") -+ testLibrary("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") -+ testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") -+ testLibrary("com.amazonaws:aws-java-sdk-lambda:1.11.678") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrock:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockagent:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockagentruntime:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockruntime:1.12.744") - - // last version that does not use json protocol - latestDepTestLibrary("com.amazonaws:aws-java-sdk-sqs:1.12.583") -diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts -index bfe844e413..dec4935b55 100644 ---- a/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/build.gradle.kts -@@ -17,6 +17,14 @@ dependencies { - testLibrary("com.amazonaws:aws-java-sdk-kinesis:1.11.106") - testLibrary("com.amazonaws:aws-java-sdk-dynamodb:1.11.106") - testLibrary("com.amazonaws:aws-java-sdk-sns:1.11.106") -+ testLibrary("com.amazonaws:aws-java-sdk-sqs:1.11.106") -+ testLibrary("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") -+ testLibrary("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") -+ testLibrary("com.amazonaws:aws-java-sdk-lambda:1.11.678") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrock:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockagent:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockagentruntime:1.12.744") -+ testLibrary("com.amazonaws:aws-java-sdk-bedrockruntime:1.12.744") - - // last version that does not use json protocol - latestDepTestLibrary("com.amazonaws:aws-java-sdk-sqs:1.12.583") -diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsBedrockResourceType.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsBedrockResourceType.java -new file mode 100644 -index 0000000000..e890cb3c0f ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AwsBedrockResourceType.java -@@ -0,0 +1,133 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v1_11; -+ -+import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_AGENT_ID; -+import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_DATA_SOURCE_ID; -+import static io.opentelemetry.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/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..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 -@@ -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"); -+ 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"); - - 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..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,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; - import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_ENDPOINT; -+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; - import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_QUEUE_URL; -+import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.AWS_SECRET_ARN; -+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; -+import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_REQUEST_TOP_P; -+import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_RESPONSE_FINISH_REASONS; -+import static io.opentelemetry.instrumentation.awssdk.v1_11.AwsExperimentalAttributes.GEN_AI_USAGE_INPUT_TOKENS; -+import static io.opentelemetry.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> { - private static final String COMPONENT_NAME = "java-aws-sdk"; -+ 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"); - - @Override - public void onStart(AttributesBuilder attributes, Context parentContext, Request request) { -@@ -32,14 +63,165 @@ class AwsSdkExperimentalAttributesExtractor - attributes.put(AWS_ENDPOINT, request.getEndpoint().toString()); - - Object originalRequest = request.getOriginalRequest(); -- setRequestAttribute(attributes, AWS_BUCKET_NAME, originalRequest, RequestAccess::getBucketName); -- setRequestAttribute(attributes, AWS_QUEUE_URL, originalRequest, RequestAccess::getQueueUrl); -- setRequestAttribute(attributes, AWS_QUEUE_NAME, originalRequest, RequestAccess::getQueueName); -- 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); -+ 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); -+ } - } - -- private static void setRequestAttribute( -+ @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); -+ } -+ -+ private static void setAttribute( - AttributesBuilder attributes, - AttributeKey key, - Object request, -@@ -49,12 +231,4 @@ class AwsSdkExperimentalAttributesExtractor - attributes.put(key, value); - } - } -- -- @Override -- public void onEnd( -- AttributesBuilder attributes, -- Context context, -- Request request, -- @Nullable Response response, -- @Nullable Throwable error) {} - } -diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/BedrockJsonParser.java b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/BedrockJsonParser.java -new file mode 100644 -index 0000000000..d1acc5768a ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/BedrockJsonParser.java -@@ -0,0 +1,267 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.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/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..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; - 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 { -@@ -20,48 +26,417 @@ final class RequestAccess { - } - }; - -+ @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 getBucketName(Object request) { -+ if (request == null) { -+ return null; -+ } - RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); - return invokeOrNull(access.getBucketName, request); - } - - @Nullable - static String getQueueUrl(Object request) { -+ if (request == null) { -+ return null; -+ } - RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); - return invokeOrNull(access.getQueueUrl, request); - } - - @Nullable - static String getQueueName(Object request) { -+ if (request == null) { -+ return null; -+ } - RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); - return invokeOrNull(access.getQueueName, request); - } - - @Nullable - static String getStreamName(Object request) { -+ if (request == null) { -+ return null; -+ } - RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); - return invokeOrNull(access.getStreamName, request); - } - - @Nullable - static String getTableName(Object request) { -+ if (request == null) { -+ return null; -+ } - RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); - 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) { -+ return null; -+ } - RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); - return invokeOrNull(access.getTopicArn, request); - } - - @Nullable - static String getTargetArn(Object request) { -+ if (request == null) { -+ return null; -+ } - RequestAccess access = REQUEST_ACCESSORS.get(request.getClass()); - return invokeOrNull(access.getTargetArn, 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); -+ } -+ - @Nullable - private static String invokeOrNull(@Nullable MethodHandle method, Object obj) { - if (method == null) { -@@ -74,31 +449,88 @@ final class RequestAccess { - } - } - -+ @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 getBucketName; - @Nullable private final MethodHandle getQueueUrl; - @Nullable private final MethodHandle getQueueName; - @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; -+ @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) { -- getBucketName = findAccessorOrNull(clz, "getBucketName"); -- getQueueUrl = findAccessorOrNull(clz, "getQueueUrl"); -- getQueueName = findAccessorOrNull(clz, "getQueueName"); -- getStreamName = findAccessorOrNull(clz, "getStreamName"); -- getTableName = findAccessorOrNull(clz, "getTableName"); -- getTopicArn = findAccessorOrNull(clz, "getTopicArn"); -- getTargetArn = findAccessorOrNull(clz, "getTargetArn"); -+ getBucketName = findAccessorOrNull(clz, "getBucketName", String.class); -+ 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); -+ 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); - } - - @Nullable -- private static MethodHandle findAccessorOrNull(Class clz, String methodName) { -+ private static MethodHandle findAccessorOrNull( -+ Class clz, String methodName, Class returnType) { - try { - return MethodHandles.publicLookup() -- .findVirtual(clz, methodName, MethodType.methodType(String.class)); -+ .findVirtual(clz, methodName, MethodType.methodType(returnType)); - } catch (Throwable t) { - return null; - } - } -+ -+ @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; -+ } - } -diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/BedrockJsonParserTest.groovy b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/BedrockJsonParserTest.groovy -new file mode 100644 -index 0000000000..03563b1d5b ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v1_11/BedrockJsonParserTest.groovy -@@ -0,0 +1,107 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.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/aws-sdk-1.11/testing/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts -index 545f5dffce..227a205ebd 100644 ---- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/build.gradle.kts -@@ -14,6 +14,14 @@ dependencies { - compileOnly("com.amazonaws:aws-java-sdk-dynamodb:1.11.106") - compileOnly("com.amazonaws:aws-java-sdk-sns:1.11.106") - compileOnly("com.amazonaws:aws-java-sdk-sqs:1.11.106") -+ compileOnly("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") -+ compileOnly("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") -+ compileOnly("com.amazonaws:aws-java-sdk-lambda:1.11.678") -+ -+ compileOnly("com.amazonaws:aws-java-sdk-bedrock:1.12.744") -+ compileOnly("com.amazonaws:aws-java-sdk-bedrockagent:1.12.744") -+ compileOnly("com.amazonaws:aws-java-sdk-bedrockagentruntime:1.12.744") -+ compileOnly("com.amazonaws:aws-java-sdk-bedrockruntime:1.12.744") - - // 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-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockAgentClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockAgentClientTest.java -new file mode 100644 -index 0000000000..a5e5a63b09 ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockAgentClientTest.java -@@ -0,0 +1,95 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v1_11; -+ -+import static io.opentelemetry.api.common.AttributeKey.stringKey; -+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -+import static java.util.Collections.singletonList; -+ -+import com.amazonaws.services.bedrockagent.AWSBedrockAgent; -+import com.amazonaws.services.bedrockagent.AWSBedrockAgentClientBuilder; -+import com.amazonaws.services.bedrockagent.model.GetAgentRequest; -+import com.amazonaws.services.bedrockagent.model.GetDataSourceRequest; -+import com.amazonaws.services.bedrockagent.model.GetKnowledgeBaseRequest; -+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 org.junit.jupiter.api.Test; -+ -+public abstract class AbstractBedrockAgentClientTest extends AbstractBaseAwsClientTest { -+ -+ public abstract AWSBedrockAgentClientBuilder configureClient(AWSBedrockAgentClientBuilder client); -+ -+ @Override -+ protected boolean hasRequestId() { -+ return true; -+ } -+ -+ @Test -+ public void sendGetAgentRequest() throws Exception { -+ AWSBedrockAgent client = createClient(); -+ -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, "{}")); -+ -+ Object response = client.getAgent(new GetAgentRequest().withAgentId("agentId")); -+ -+ assertRequestWithMockedResponse( -+ response, -+ client, -+ "AWSBedrockAgent", -+ "GetAgent", -+ "GET", -+ singletonList(equalTo(stringKey("aws.bedrock.agent.id"), "agentId"))); -+ } -+ -+ @Test -+ public void sendGetKnowledgeBaseRequest() throws Exception { -+ AWSBedrockAgent client = createClient(); -+ -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, "{}")); -+ -+ Object response = -+ client.getKnowledgeBase( -+ new GetKnowledgeBaseRequest().withKnowledgeBaseId("knowledgeBaseId")); -+ -+ assertRequestWithMockedResponse( -+ response, -+ client, -+ "AWSBedrockAgent", -+ "GetKnowledgeBase", -+ "GET", -+ singletonList(equalTo(stringKey("aws.bedrock.knowledge_base.id"), "knowledgeBaseId"))); -+ } -+ -+ @Test -+ public void sendGetDataSourceRequest() throws Exception { -+ AWSBedrockAgent client = createClient(); -+ -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, "{}")); -+ -+ Object response = -+ client.getDataSource( -+ new GetDataSourceRequest() -+ .withDataSourceId("datasourceId") -+ .withKnowledgeBaseId("knowledgeBaseId")); -+ -+ assertRequestWithMockedResponse( -+ response, -+ client, -+ "AWSBedrockAgent", -+ "GetDataSource", -+ "GET", -+ singletonList(equalTo(stringKey("aws.bedrock.data_source.id"), "datasourceId"))); -+ } -+ -+ private AWSBedrockAgent createClient() { -+ AWSBedrockAgentClientBuilder clientBuilder = AWSBedrockAgentClientBuilder.standard(); -+ return configureClient(clientBuilder) -+ .withEndpointConfiguration(endpoint) -+ .withCredentials(credentialsProvider) -+ .build(); -+ } -+} -diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockClientTest.java -new file mode 100644 -index 0000000000..a97b893055 ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockClientTest.java -@@ -0,0 +1,79 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v1_11; -+ -+import static io.opentelemetry.api.common.AttributeKey.stringKey; -+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -+import static java.util.Collections.singletonList; -+ -+import com.amazonaws.services.bedrock.AmazonBedrock; -+import com.amazonaws.services.bedrock.AmazonBedrockClientBuilder; -+import com.amazonaws.services.bedrock.model.GetGuardrailRequest; -+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 org.junit.jupiter.api.Test; -+ -+public abstract class AbstractBedrockClientTest extends AbstractBaseAwsClientTest { -+ -+ public abstract AmazonBedrockClientBuilder configureClient(AmazonBedrockClientBuilder client); -+ -+ @Override -+ protected boolean hasRequestId() { -+ return true; -+ } -+ -+ @Test -+ public void sendRequestWithMockedResponse() throws Exception { -+ AmazonBedrockClientBuilder clientBuilder = AmazonBedrockClientBuilder.standard(); -+ AmazonBedrock client = -+ configureClient(clientBuilder) -+ .withEndpointConfiguration(endpoint) -+ .withCredentials(credentialsProvider) -+ .build(); -+ -+ String body = -+ "{" -+ + " \"blockedInputMessaging\": \"string\"," -+ + " \"blockedOutputsMessaging\": \"string\"," -+ + " \"contentPolicy\": {}," -+ + " \"createdAt\": \"2024-06-12T18:31:45Z\"," -+ + " \"description\": \"string\"," -+ + " \"guardrailArn\": \"guardrailArn\"," -+ + " \"guardrailId\": \"guardrailId\"," -+ + " \"kmsKeyArn\": \"string\"," -+ + " \"name\": \"string\"," -+ + " \"sensitiveInformationPolicy\": {}," -+ + " \"status\": \"READY\"," -+ + " \"topicPolicy\": {" -+ + " \"topics\": [" -+ + " {" -+ + " \"definition\": \"string\"," -+ + " \"examples\": [ \"string\" ]," -+ + " \"name\": \"string\"," -+ + " \"type\": \"string\"" -+ + " }" -+ + " ]" -+ + " }," -+ + " \"updatedAt\": \"2024-06-12T18:31:48Z\"," -+ + " \"version\": \"DRAFT\"," -+ + " \"wordPolicy\": {}" -+ + "}"; -+ -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, body)); -+ -+ Object response = -+ client.getGuardrail(new GetGuardrailRequest().withGuardrailIdentifier("guardrailId")); -+ -+ assertRequestWithMockedResponse( -+ response, -+ client, -+ "Bedrock", -+ "GetGuardrail", -+ "GET", -+ singletonList(equalTo(stringKey("aws.bedrock.guardrail.id"), "guardrailId"))); -+ } -+} -diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockRuntimeClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockRuntimeClientTest.java -new file mode 100644 -index 0000000000..98a5873614 ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractBedrockRuntimeClientTest.java -@@ -0,0 +1,135 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v1_11; -+ -+import static io.opentelemetry.api.common.AttributeKey.stringKey; -+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -+import static java.util.Arrays.asList; -+ -+import com.amazonaws.services.bedrockruntime.AmazonBedrockRuntime; -+import com.amazonaws.services.bedrockruntime.AmazonBedrockRuntimeClientBuilder; -+import com.amazonaws.services.bedrockruntime.model.InvokeModelRequest; -+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.nio.charset.StandardCharsets; -+import java.util.List; -+import java.util.stream.Stream; -+import org.junit.jupiter.params.ParameterizedTest; -+import org.junit.jupiter.params.provider.MethodSource; -+ -+public abstract class AbstractBedrockRuntimeClientTest extends AbstractBaseAwsClientTest { -+ -+ public abstract AmazonBedrockRuntimeClientBuilder configureClient( -+ AmazonBedrockRuntimeClientBuilder client); -+ -+ @Override -+ protected boolean hasRequestId() { -+ return true; -+ } -+ -+ @ParameterizedTest -+ @MethodSource("testData") -+ public void sendRequestWithMockedResponse( -+ String modelId, -+ String requestBody, -+ String expectedResponse, -+ List expectedAttributes) -+ throws Exception { -+ AmazonBedrockRuntimeClientBuilder clientBuilder = AmazonBedrockRuntimeClientBuilder.standard(); -+ AmazonBedrockRuntime client = -+ configureClient(clientBuilder) -+ .withEndpointConfiguration(endpoint) -+ .withCredentials(credentialsProvider) -+ .build(); -+ -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.JSON_UTF_8, expectedResponse)); -+ -+ client.invokeModel( -+ new InvokeModelRequest() -+ .withModelId(modelId) -+ .withBody(StandardCharsets.UTF_8.encode(requestBody))); -+ -+ assertRequestWithMockedResponse( -+ expectedResponse, client, "BedrockRuntime", "InvokeModel", "POST", expectedAttributes); -+ } -+ -+ private static Stream testData() { -+ return Stream.of( -+ new Object[] { -+ "ai21.jamba-1-5-mini-v1:0", -+ "{\"messages\":[{\"role\":\"user\",\"message\":\"Which LLM are you?\"}],\"max_tokens\":1000,\"top_p\":0.8,\"temperature\":0.7}", -+ "{\"choices\":[{\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":42}}", -+ asList( -+ equalTo(stringKey("gen_ai.request.model"), "ai21.jamba-1-5-mini-v1:0"), -+ equalTo(stringKey("gen_ai.system"), "aws.bedrock"), -+ equalTo(stringKey("gen_ai.request.max_tokens"), "1000"), -+ equalTo(stringKey("gen_ai.request.temperature"), "0.7"), -+ equalTo(stringKey("gen_ai.request.top_p"), "0.8"), -+ equalTo(stringKey("gen_ai.response.finish_reasons"), "[stop]"), -+ equalTo(stringKey("gen_ai.usage.input_tokens"), "5"), -+ equalTo(stringKey("gen_ai.usage.output_tokens"), "42")) -+ }, -+ new Object[] { -+ "amazon.titan-text-premier-v1:0", -+ "{\"inputText\":\"Hello, world!\",\"textGenerationConfig\":{\"temperature\":0.7,\"topP\":0.9,\"maxTokenCount\":100,\"stopSequences\":[\"END\"]}}", -+ "{\"inputTextTokenCount\":5,\"results\":[{\"tokenCount\":42,\"outputText\":\"Hi! I'm Titan, an AI assistant.\",\"completionReason\":\"stop\"}]}", -+ asList( -+ equalTo(stringKey("gen_ai.request.model"), "amazon.titan-text-premier-v1:0"), -+ equalTo(stringKey("gen_ai.system"), "aws.bedrock"), -+ equalTo(stringKey("gen_ai.request.max_tokens"), "100"), -+ equalTo(stringKey("gen_ai.request.temperature"), "0.7"), -+ equalTo(stringKey("gen_ai.request.top_p"), "0.9"), -+ equalTo(stringKey("gen_ai.response.finish_reasons"), "[stop]"), -+ equalTo(stringKey("gen_ai.usage.input_tokens"), "5"), -+ equalTo(stringKey("gen_ai.usage.output_tokens"), "42")) -+ }, -+ new Object[] { -+ "anthropic.claude-3-5-sonnet-20241022-v2:0", -+ "{\"anthropic_version\":\"bedrock-2023-05-31\",\"messages\":[{\"role\":\"user\",\"content\":\"Hello, world\"}],\"max_tokens\":100,\"temperature\":0.7,\"top_p\":0.9}", -+ "{\"stop_reason\":\"end_turn\",\"usage\":{\"input_tokens\":2095,\"output_tokens\":503}}", -+ asList( -+ equalTo( -+ stringKey("gen_ai.request.model"), "anthropic.claude-3-5-sonnet-20241022-v2:0"), -+ equalTo(stringKey("gen_ai.system"), "aws.bedrock"), -+ equalTo(stringKey("gen_ai.request.max_tokens"), "100"), -+ equalTo(stringKey("gen_ai.request.temperature"), "0.7"), -+ equalTo(stringKey("gen_ai.request.top_p"), "0.9"), -+ equalTo(stringKey("gen_ai.response.finish_reasons"), "[end_turn]"), -+ equalTo(stringKey("gen_ai.usage.input_tokens"), "2095"), -+ equalTo(stringKey("gen_ai.usage.output_tokens"), "503")) -+ }, -+ new Object[] { -+ "meta.llama3-70b-instruct-v1:0", -+ "{\"prompt\":\"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\\\\nDescribe the purpose of a 'hello world' program in one line. <|eot_id|>\\\\n<|start_header_id|>assistant<|end_header_id|>\\\\n\",\"max_gen_len\":128,\"temperature\":0.1,\"top_p\":0.9}", -+ "{\"prompt_token_count\":2095,\"generation_token_count\":503,\"stop_reason\":\"stop\"}", -+ asList( -+ equalTo(stringKey("gen_ai.request.model"), "meta.llama3-70b-instruct-v1:0"), -+ equalTo(stringKey("gen_ai.system"), "aws.bedrock"), -+ equalTo(stringKey("gen_ai.request.max_tokens"), "128"), -+ equalTo(stringKey("gen_ai.request.temperature"), "0.1"), -+ equalTo(stringKey("gen_ai.request.top_p"), "0.9"), -+ equalTo(stringKey("gen_ai.response.finish_reasons"), "[stop]"), -+ equalTo(stringKey("gen_ai.usage.input_tokens"), "2095"), -+ equalTo(stringKey("gen_ai.usage.output_tokens"), "503")) -+ }, -+ new Object[] { -+ "cohere.command-r-v1:0", -+ "{\"message\":\"Convince me to write a LISP interpreter in one line.\",\"temperature\":0.8,\"max_tokens\":4096,\"p\":0.45}", -+ "{\"text\":\"test-output\",\"finish_reason\":\"COMPLETE\"}", -+ asList( -+ equalTo(stringKey("gen_ai.request.model"), "cohere.command-r-v1:0"), -+ equalTo(stringKey("gen_ai.system"), "aws.bedrock"), -+ equalTo(stringKey("gen_ai.request.max_tokens"), "4096"), -+ equalTo(stringKey("gen_ai.request.temperature"), "0.8"), -+ equalTo(stringKey("gen_ai.request.top_p"), "0.45"), -+ equalTo(stringKey("gen_ai.response.finish_reasons"), "[COMPLETE]"), -+ equalTo(stringKey("gen_ai.usage.input_tokens"), "9"), -+ equalTo(stringKey("gen_ai.usage.output_tokens"), "2")) -+ }); -+ } -+} -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 ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractLambdaClientTest.java -@@ -0,0 +1,72 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v1_11; -+ -+import static io.opentelemetry.api.common.AttributeKey.stringKey; -+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -+import static java.util.Collections.singletonList; -+ -+import com.amazonaws.services.lambda.AWSLambda; -+import com.amazonaws.services.lambda.AWSLambdaClientBuilder; -+import com.amazonaws.services.lambda.model.GetEventSourceMappingRequest; -+import com.amazonaws.services.lambda.model.GetFunctionRequest; -+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.List; -+import java.util.function.Function; -+import java.util.stream.Stream; -+import org.junit.jupiter.params.ParameterizedTest; -+import org.junit.jupiter.params.provider.Arguments; -+import org.junit.jupiter.params.provider.MethodSource; -+ -+public abstract class AbstractLambdaClientTest extends AbstractBaseAwsClientTest { -+ -+ public abstract AWSLambdaClientBuilder configureClient(AWSLambdaClientBuilder client); -+ -+ @Override -+ protected boolean hasRequestId() { -+ return false; -+ } -+ -+ @ParameterizedTest -+ @MethodSource("provideArguments") -+ public void testSendRequestWithMockedResponse( -+ String operation, -+ List additionalAttributes, -+ Function call) -+ throws Exception { -+ -+ AWSLambdaClientBuilder clientBuilder = AWSLambdaClientBuilder.standard(); -+ -+ AWSLambda client = -+ configureClient(clientBuilder) -+ .withEndpointConfiguration(endpoint) -+ .withCredentials(credentialsProvider) -+ .build(); -+ -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")); -+ -+ Object response = call.apply(client); -+ assertRequestWithMockedResponse( -+ response, client, "AWSLambda", operation, "GET", additionalAttributes); -+ } -+ -+ private static Stream provideArguments() { -+ return Stream.of( -+ Arguments.of( -+ "GetEventSourceMapping", -+ singletonList(equalTo(stringKey("aws.lambda.resource_mapping.id"), "uuid")), -+ (Function) -+ c -> c.getEventSourceMapping(new GetEventSourceMappingRequest().withUUID("uuid"))), -+ Arguments.of( -+ "GetFunction", -+ singletonList(equalTo(stringKey("aws.lambda.function.name"), "functionName")), -+ (Function) -+ 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 ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSecretsManagerClientTest.java -@@ -0,0 +1,62 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v1_11; -+ -+import static io.opentelemetry.api.common.AttributeKey.stringKey; -+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -+import static java.util.Collections.singletonList; -+ -+import com.amazonaws.services.secretsmanager.AWSSecretsManager; -+import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder; -+import com.amazonaws.services.secretsmanager.model.CreateSecretRequest; -+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 org.junit.jupiter.api.Test; -+ -+public abstract class AbstractSecretsManagerClientTest extends AbstractBaseAwsClientTest { -+ -+ public abstract AWSSecretsManagerClientBuilder configureClient( -+ AWSSecretsManagerClientBuilder client); -+ -+ @Override -+ protected boolean hasRequestId() { -+ return true; -+ } -+ -+ @Test -+ public void sendRequestWithMockedResponse() throws Exception { -+ AWSSecretsManagerClientBuilder clientBuilder = AWSSecretsManagerClientBuilder.standard(); -+ AWSSecretsManager client = -+ configureClient(clientBuilder) -+ .withEndpointConfiguration(endpoint) -+ .withCredentials(credentialsProvider) -+ .build(); -+ -+ String body = -+ "{" -+ + "\"ARN\": \"arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3\"," -+ + "\"Name\": \"MyTestDatabaseSecret\"," -+ + "\"VersionId\": \"EXAMPLE1-90ab-cdef-fedc-ba987SECRET1\"" -+ + "}"; -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)); -+ -+ Object response = -+ client.createSecret( -+ new CreateSecretRequest().withName("secretName").withSecretString("secretValue")); -+ -+ assertRequestWithMockedResponse( -+ response, -+ client, -+ "AWSSecretsManager", -+ "CreateSecret", -+ "POST", -+ singletonList( -+ equalTo( -+ stringKey("aws.secretsmanager.secret.arn"), -+ "arn:aws:secretsmanager:us-west-2:123456789012:secret:MyTestDatabaseSecret-a1b2c3"))); -+ } -+} -diff --git a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSnsClientTest.java b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSnsClientTest.java -index 3f272ba477..bea20f3d86 100644 ---- a/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSnsClientTest.java -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractSnsClientTest.java -@@ -5,8 +5,10 @@ - - package io.opentelemetry.instrumentation.awssdk.v1_11; - -+import static io.opentelemetry.api.common.AttributeKey.stringKey; - import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; - import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; -+import static java.util.Arrays.asList; - import static java.util.Collections.singletonList; - - import com.amazonaws.services.sns.AmazonSNS; -@@ -17,11 +19,7 @@ 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.List; --import java.util.function.Function; --import java.util.stream.Stream; --import org.junit.jupiter.params.ParameterizedTest; --import org.junit.jupiter.params.provider.Arguments; --import org.junit.jupiter.params.provider.MethodSource; -+import org.junit.jupiter.api.Test; - - public abstract class AbstractSnsClientTest extends AbstractBaseAwsClientTest { - -@@ -32,9 +30,8 @@ public abstract class AbstractSnsClientTest extends AbstractBaseAwsClientTest { - return true; - } - -- @ParameterizedTest -- @MethodSource("provideArguments") -- public void testSendRequestWithMockedResponse(Function call) throws Exception { -+ @Test -+ public void testSendRequestWithwithTopicArnMockedResponse() throws Exception { - AmazonSNSClientBuilder clientBuilder = AmazonSNSClientBuilder.standard(); - AmazonSNS client = - configureClient(clientBuilder) -@@ -55,24 +52,44 @@ public abstract class AbstractSnsClientTest extends AbstractBaseAwsClientTest { - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)); - - List additionalAttributes = -- singletonList(equalTo(MESSAGING_DESTINATION_NAME, "somearn")); -+ asList( -+ equalTo(stringKey(MESSAGING_DESTINATION_NAME.getKey()), "somearn"), -+ equalTo(stringKey("aws.sns.topic.arn"), "somearn")); -+ -+ Object response = -+ client.publish(new PublishRequest().withMessage("somemessage").withTopicArn("somearn")); - -- Object response = call.apply(client); - assertRequestWithMockedResponse( - response, client, "SNS", "Publish", "POST", additionalAttributes); - } - -- private static Stream provideArguments() { -- return Stream.of( -- Arguments.of( -- (Function) -- c -> -- c.publish( -- new PublishRequest().withMessage("somemessage").withTopicArn("somearn"))), -- Arguments.of( -- (Function) -- c -> -- c.publish( -- new PublishRequest().withMessage("somemessage").withTargetArn("somearn")))); -+ @Test -+ public void testSendRequestWithwithTargetArnMockedResponse() throws Exception { -+ AmazonSNSClientBuilder clientBuilder = AmazonSNSClientBuilder.standard(); -+ AmazonSNS client = -+ configureClient(clientBuilder) -+ .withEndpointConfiguration(endpoint) -+ .withCredentials(credentialsProvider) -+ .build(); -+ -+ String body = -+ "" -+ + " " -+ + " 567910cd-659e-55d4-8ccb-5aaf14679dc0" -+ + " " -+ + " " -+ + " d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" -+ + " " -+ + ""; -+ -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)); -+ -+ List additionalAttributes = -+ singletonList(equalTo(stringKey(MESSAGING_DESTINATION_NAME.getKey()), "somearn")); -+ -+ Object response = -+ client.publish(new PublishRequest().withMessage("somemessage").withTargetArn("somearn")); -+ assertRequestWithMockedResponse( -+ 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 ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-1.11/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v1_11/AbstractStepFunctionsClientTest.java -@@ -0,0 +1,78 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v1_11; -+ -+import static io.opentelemetry.api.common.AttributeKey.stringKey; -+import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -+import static java.util.Collections.singletonList; -+ -+import com.amazonaws.services.stepfunctions.AWSStepFunctions; -+import com.amazonaws.services.stepfunctions.AWSStepFunctionsClientBuilder; -+import com.amazonaws.services.stepfunctions.model.DescribeActivityRequest; -+import com.amazonaws.services.stepfunctions.model.DescribeStateMachineRequest; -+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.List; -+import java.util.function.Function; -+import java.util.stream.Stream; -+import org.junit.jupiter.params.ParameterizedTest; -+import org.junit.jupiter.params.provider.Arguments; -+import org.junit.jupiter.params.provider.MethodSource; -+ -+public abstract class AbstractStepFunctionsClientTest extends AbstractBaseAwsClientTest { -+ -+ public abstract AWSStepFunctionsClientBuilder configureClient( -+ AWSStepFunctionsClientBuilder client); -+ -+ @Override -+ protected boolean hasRequestId() { -+ return false; -+ } -+ -+ @ParameterizedTest -+ @MethodSource("provideArguments") -+ public void testSendRequestWithMockedResponse( -+ String operation, -+ List additionalAttributes, -+ Function call) -+ throws Exception { -+ -+ AWSStepFunctionsClientBuilder clientBuilder = AWSStepFunctionsClientBuilder.standard(); -+ -+ AWSStepFunctions client = -+ configureClient(clientBuilder) -+ .withEndpointConfiguration(endpoint) -+ .withCredentials(credentialsProvider) -+ .build(); -+ -+ server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")); -+ -+ Object response = call.apply(client); -+ assertRequestWithMockedResponse( -+ response, client, "AWSStepFunctions", operation, "POST", additionalAttributes); -+ } -+ -+ private static Stream provideArguments() { -+ return Stream.of( -+ Arguments.of( -+ "DescribeStateMachine", -+ singletonList( -+ equalTo(stringKey("aws.stepfunctions.state_machine.arn"), "stateMachineArn")), -+ (Function) -+ c -> -+ c.describeStateMachine( -+ new DescribeStateMachineRequest().withStateMachineArn("stateMachineArn"))), -+ Arguments.of( -+ "DescribeActivity", -+ singletonList(equalTo(stringKey("aws.stepfunctions.activity.arn"), "activityArn")), -+ (Function) -+ c -> -+ c.describeActivity( -+ new DescribeActivityRequest().withActivityArn("activityArn")))); -+ } -+} -diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts -index 7d3fa5d03c..6079232826 100644 ---- a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts -@@ -104,6 +104,9 @@ dependencies { - testLibrary("software.amazon.awssdk:sqs:2.2.0") - testLibrary("software.amazon.awssdk:sns:2.2.0") - testLibrary("software.amazon.awssdk:ses:2.2.0") -+ testLibrary("software.amazon.awssdk:sfn:2.2.0") -+ testLibrary("software.amazon.awssdk:secretsmanager:2.2.0") -+ testLibrary("software.amazon.awssdk:lambda:2.2.0") - } - - val latestDepTest = findProperty("testLatestDeps") as Boolean -diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/build.gradle.kts -index d493f83a86..0bb91a17c3 100644 ---- a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/build.gradle.kts -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/build.gradle.kts -@@ -22,6 +22,9 @@ dependencies { - testLibrary("software.amazon.awssdk:s3:2.2.0") - testLibrary("software.amazon.awssdk:sqs:2.2.0") - testLibrary("software.amazon.awssdk:sns:2.2.0") -+ testLibrary("software.amazon.awssdk:sfn:2.2.0") -+ testLibrary("software.amazon.awssdk:secretsmanager:2.2.0") -+ testLibrary("software.amazon.awssdk:lambda:2.2.0") - } - - tasks { -diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts -index 3b7381a8ba..6f77951710 100644 ---- a/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/build.gradle.kts -@@ -22,6 +22,9 @@ dependencies { - testLibrary("software.amazon.awssdk:rds:2.2.0") - testLibrary("software.amazon.awssdk:s3:2.2.0") - testLibrary("software.amazon.awssdk:ses:2.2.0") -+ testLibrary("software.amazon.awssdk:sfn:2.2.0") -+ testLibrary("software.amazon.awssdk:secretsmanager:2.2.0") -+ testLibrary("software.amazon.awssdk:lambda:2.2.0") - } - - 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..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,80 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v2_2.internal; -+ -+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/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequest.java -index 02d92ca070..aa98cd62c7 100644 ---- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequest.java -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkRequest.java -@@ -5,11 +5,20 @@ - - package io.opentelemetry.instrumentation.awssdk.v2_2.internal; - -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.BEDROCK; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.BEDROCKAGENTOPERATION; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.BEDROCKAGENTRUNTIMEOPERATION; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.BEDROCKDATASOURCEOPERATION; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.BEDROCKKNOWLEDGEBASEOPERATION; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.BEDROCKRUNTIME; - import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.DYNAMODB; - import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.KINESIS; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.LAMBDA; - import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.S3; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.SECRETSMANAGER; - import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.SNS; - import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.SQS; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkRequestType.STEPFUNCTION; - import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.FieldMapping.request; - import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.FieldMapping.response; - -@@ -34,6 +43,52 @@ enum AwsSdkRequest { - SnsRequest(SNS, "SnsRequest"), - SqsRequest(SQS, "SqsRequest"), - KinesisRequest(KINESIS, "KinesisRequest"), -+ -+ 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"), - // specific requests - 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..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,34 @@ - - package io.opentelemetry.instrumentation.awssdk.v2_2.internal; - -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_AGENT_ID; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_BUCKET_NAME; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_DATA_SOURCE_ID; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_GUARDRAIL_ARN; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_GUARDRAIL_ID; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_KNOWLEDGE_BASE_ID; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_LAMBDA_ARN; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_LAMBDA_NAME; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_LAMBDA_RESOURCE_ID; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_QUEUE_NAME; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_QUEUE_URL; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.AWS_SECRET_ARN; -+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; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.GEN_AI_REQUEST_TEMPERATURE; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.GEN_AI_REQUEST_TOP_P; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.GEN_AI_RESPONSE_FINISH_REASONS; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.GEN_AI_USAGE_INPUT_TOKENS; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsExperimentalAttributes.GEN_AI_USAGE_OUTPUT_TOKENS; - import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.FieldMapping.request; -+import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.FieldMapping.response; - - import io.opentelemetry.api.common.AttributeKey; - import java.util.Collections; -@@ -13,16 +40,64 @@ import java.util.List; - import java.util.Map; - - enum AwsSdkRequestType { -- S3(request("aws.bucket.name", "Bucket")), -- SQS(request("aws.queue.url", "QueueUrl"), request("aws.queue.name", "QueueName")), -- KINESIS(request("aws.stream.name", "StreamName")), -- DYNAMODB(request("aws.table.name", "TableName")), -+ 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(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")); - - // Wrapping in unmodifiableMap - @SuppressWarnings("ImmutableEnumChecker") -diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockJsonParser.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockJsonParser.java -new file mode 100644 -index 0000000000..9812f1afa5 ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockJsonParser.java -@@ -0,0 +1,279 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v2_2.internal; -+ -+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/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/FieldMapper.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/FieldMapper.java -index 9e7aeacbce..9a38a753ca 100644 ---- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/FieldMapper.java -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/FieldMapper.java -@@ -65,8 +65,13 @@ class FieldMapper { - for (int i = 1; i < path.size() && target != null; i++) { - target = next(target, path.get(i)); - } -+ String value; - if (target != null) { -- String value = serializer.serialize(target); -+ if (AwsExperimentalAttributes.isGenAiAttribute(fieldMapping.getAttribute())) { -+ value = serializer.serialize(fieldMapping.getAttribute(), target); -+ } else { -+ value = serializer.serialize(target); -+ } - if (!StringUtils.isEmpty(value)) { - span.setAttribute(fieldMapping.getAttribute(), value); - } -diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/Serializer.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/Serializer.java -index 7ae1590152..5b7a188914 100644 ---- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/Serializer.java -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/Serializer.java -@@ -7,11 +7,14 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.internal; - - import java.io.IOException; - import java.io.InputStream; -+import java.util.Arrays; - import java.util.Collection; - import java.util.Map; -+import java.util.Objects; - import java.util.Optional; - 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; -@@ -41,6 +44,45 @@ class Serializer { - return target.toString(); - } - -+ @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; -+ } -+ } -+ - @Nullable - private static String serialize(SdkPojo sdkPojo) { - ProtocolMarshaller marshaller = -@@ -65,4 +107,167 @@ class Serializer { - String serialized = collection.stream().map(this::serialize).collect(Collectors.joining(",")); - return (StringUtils.isEmpty(serialized) ? null : "[" + serialized + "]"); - } -+ -+ @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; -+ } - } -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..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,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; -@@ -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 { -+ private static final String GEN_AI_SYSTEM_BEDROCK = "aws.bedrock"; - - // copied from DbIncubatingAttributes - private static final AttributeKey DB_OPERATION = AttributeKey.stringKey("db.operation"); -@@ -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 { - } - } - } -+ -+ if (awsSdkRequest.type() == BEDROCKRUNTIME) { -+ span.setAttribute(GEN_AI_SYSTEM, GEN_AI_SYSTEM_BEDROCK); -+ } - } - - @Override -diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockJsonParserTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockJsonParserTest.groovy -new file mode 100644 -index 0000000000..9dff7aa804 ---- /dev/null -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockJsonParserTest.groovy -@@ -0,0 +1,107 @@ -+/* -+ * Copyright The OpenTelemetry Authors -+ * SPDX-License-Identifier: Apache-2.0 -+ */ -+ -+package io.opentelemetry.instrumentation.awssdk.v2_2.internal -+ -+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/aws-sdk-2.2/testing/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/testing/build.gradle.kts -index 08b000a05c..de0fe82638 100644 ---- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/build.gradle.kts -+++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/build.gradle.kts -@@ -20,6 +20,9 @@ dependencies { - compileOnly("software.amazon.awssdk:sqs:2.2.0") - compileOnly("software.amazon.awssdk:sns:2.2.0") - compileOnly("software.amazon.awssdk:ses:2.2.0") -+ compileOnly("software.amazon.awssdk:sfn:2.2.0") -+ compileOnly("software.amazon.awssdk:lambda:2.2.0") -+ compileOnly("software.amazon.awssdk:secretsmanager:2.2.0") - - // 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..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 - import software.amazon.awssdk.services.sns.SnsAsyncClient - import software.amazon.awssdk.services.sns.SnsClient - import software.amazon.awssdk.services.sns.model.PublishRequest -+import software.amazon.awssdk.services.sns.model.SubscribeRequest - import software.amazon.awssdk.services.sqs.SqsAsyncClient - import software.amazon.awssdk.services.sqs.SqsClient - import software.amazon.awssdk.services.sqs.model.CreateQueueRequest - import software.amazon.awssdk.services.sqs.model.SendMessageRequest -+import software.amazon.awssdk.services.sfn.SfnClient -+import software.amazon.awssdk.services.sfn.model.DescribeStateMachineRequest -+import software.amazon.awssdk.services.sfn.model.DescribeActivityRequest -+import software.amazon.awssdk.services.lambda.LambdaClient -+import software.amazon.awssdk.services.lambda.model.GetFunctionRequest -+import software.amazon.awssdk.services.lambda.model.GetEventSourceMappingRequest -+import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient -+import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest - import spock.lang.Unroll - - import java.nio.charset.StandardCharsets -@@ -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" -- } else if (service == "Sns") { -- "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somearn" -+ } else if (service == "Sns" && operation == "Publish") { -+ "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "sometargetarn" -+ } else if (service == "Sns" && operation == "Subscribe") { -+ "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "sometopicarn" -+ "aws.sns.topic.arn" "sometopicarn" -+ } else if (service == "Bedrock" && operation == "GetGuardrail") { -+ "aws.bedrock.guardrail.id" "guardrailId" -+ } else if (service == "BedrockAgent" && operation == "GetAgent") { -+ "aws.bedrock.agent.id" "agentId" -+ } else if (service == "BedrockAgent" && operation == "GetKnowledgeBase") { -+ "aws.bedrock.knowledge_base.id" "knowledgeBaseId" -+ } else if (service == "BedrockAgent" && operation == "GetDataSource") { -+ "aws.bedrock.data_source.id" "datasourceId" -+ } else if (service == "BedrockRuntime" && operation == "InvokeModel") { -+ "gen_ai.request.model" "meta.llama2-13b-chat-v1" -+ "gen_ai.system" "aws.bedrock" -+ } else if (service == "Sfn" && operation == "DescribeStateMachine") { -+ "aws.stepfunctions.state_machine.arn" "stateMachineArn" -+ } else if (service == "Sfn" && operation == "DescribeActivity") { -+ "aws.stepfunctions.activity.arn" "activityArn" -+ } else if (service == "Lambda" && operation == "GetFunction") { -+ "aws.lambda.function.name" "functionName" -+ } else if (service == "Lambda" && operation == "GetEventSourceMapping") { -+ "aws.lambda.resource_mapping.id" "sourceEventId" -+ } else if (service == "SecretsManager") { -+ "aws.secretsmanager.secret.arn" "someSecretArn" - } - } - } -@@ -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()) } | "" -- "Sns" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | SnsClient.builder() | { c -> c.publish(PublishRequest.builder().message("somemessage").topicArn("somearn").build()) } | """ -+ "Sns" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | SnsClient.builder() | { c -> c.publish(PublishRequest.builder().message("somemessage").targetArn("sometargetarn").build()) } | """ - - - 567910cd-659e-55d4-8ccb-5aaf14679dc0 -@@ -174,15 +209,15 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { - - - """ -- "Sns" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | SnsClient.builder() | { c -> c.publish(PublishRequest.builder().message("somemessage").targetArn("somearn").build()) } | """ -- -- -- 567910cd-659e-55d4-8ccb-5aaf14679dc0 -- -+ "Sns" | "Subscribe" | "POST" | "1234-5678-9101-1121" | SnsClient.builder() | { c -> c.subscribe(SubscribeRequest.builder().topicArn("sometopicarn").protocol("email").endpoint("test@example.com").build())} | """ -+ -+ -+ arn:aws:sns:us-west-2:123456789012:MyTopic:abc123 -+ - -- d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 -+ 1234-5678-9101-1121 - -- -+ - """ - "Sqs" | "CreateQueue" | "POST" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | { - if (!Boolean.getBoolean("testLatestDeps")) { -@@ -244,170 +279,193 @@ abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { - 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 - - """ -- } -- -- def "send #operation async request with builder #builder.class.getName() mocked response"() { -- assumeSupportedConfig(service, operation) -- setup: -- configureSdkClient(builder) -- def client = builder -- .endpointOverride(clientUri) -- .region(Region.AP_NORTHEAST_1) -- .credentialsProvider(CREDENTIALS_PROVIDER) -- .build() -- -- if (body instanceof Closure) { -- server.enqueue(body.call()) -- } else { -- server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) -- } -- -- def response = call.call(client) -- if (response instanceof Future) { -- response = response.get() -- } -- -- expect: -- response != null -- -- assertTraces(1) { -- trace(0, 1) { -- span(0) { -- name operation != "SendMessage" ? "$service.$operation" : "somequeue publish" -- kind operation != "SendMessage" ? CLIENT : PRODUCER -- hasNoParent() -- attributes { -- if (service == "S3") { -- // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the bucket name in case -- // the bucket name is a valid DNS label, even in the case that we are using an endpoint override. -- // Previously the sdk was only doing that if endpoint had "s3" as label in the FQDN. -- // Our test assert both cases so that we don't need to know what version is being tested. -- "$ServerAttributes.SERVER_ADDRESS" { it == "somebucket.localhost" || it == "localhost" } -- "$UrlAttributes.URL_FULL" { it.startsWith("http://somebucket.localhost:${server.httpPort()}") || it.startsWith("http://localhost:${server.httpPort()}") } -- } else { -- "$ServerAttributes.SERVER_ADDRESS" "localhost" -- "$UrlAttributes.URL_FULL" { it == "http://localhost:${server.httpPort()}" || it == "http://localhost:${server.httpPort()}/" } -- } -- "$ServerAttributes.SERVER_PORT" server.httpPort() -- "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" -- "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 -- "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" -- "$RpcIncubatingAttributes.RPC_SERVICE" "$service" -- "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" -- "aws.agent" "java-aws-sdk" -- "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" -- if (service == "S3") { -- "aws.bucket.name" "somebucket" -- } else if (service == "Sqs" && operation == "CreateQueue") { -- "aws.queue.name" "somequeue" -- } else if (service == "Sqs" && operation == "SendMessage") { -- "aws.queue.url" QUEUE_URL -- "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somequeue" -- "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" -- "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String -- "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemIncubatingValues.AWS_SQS -- } else if (service == "Kinesis") { -- "aws.stream.name" "somestream" -- } else if (service == "Sns") { -- "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somearn" -- } -- } -- } -- } -- } -- def request = server.takeRequest() -- request.request().headers().get("X-Amzn-Trace-Id") != null -- request.request().headers().get("traceparent") == null -- -- if (service == "Sns" && operation == "Publish") { -- def content = request.request().content().toStringUtf8() -- def containsId = content.contains("${traces[0][0].traceId}-${traces[0][0].spanId}") -- def containsTp = content.contains("=traceparent") -- if (isSqsAttributeInjectionEnabled()) { -- assert containsId && containsTp -- } else { -- assert !containsId && !containsTp -- } -- } -- -- where: -- service | operation | method | requestId | builder | call | body -- "S3" | "CreateBucket" | "PUT" | "UNKNOWN" | s3AsyncClientBuilder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" -- "S3" | "GetObject" | "GET" | "UNKNOWN" | s3AsyncClientBuilder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build(), AsyncResponseTransformer.toBytes()) } | "1234567890" -- // Kinesis seems to expect an http2 response which is incompatible with our test server. -- // "Kinesis" | "DeleteStream" | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" -- "Sqs" | "CreateQueue" | "POST" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsAsyncClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | { -- if (!Boolean.getBoolean("testLatestDeps")) { -- def content = """ -- -- https://queue.amazonaws.com/123456789012/MyQueue -- 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 -- -- """ -- return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) -- } -- def content = """ -- { -- "QueueUrl":"https://queue.amazonaws.com/123456789012/MyQueue" -- } -- """ -- ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) -- .contentType(MediaType.PLAIN_TEXT_UTF_8) -- .add("x-amzn-RequestId", "7a62c49f-347e-4fc4-9331-6e8e7a96aa73") -- .build() -- return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) -- } -- "Sqs" | "SendMessage" | "POST" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsAsyncClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl(QUEUE_URL).messageBody("").build()) } | { -- if (!Boolean.getBoolean("testLatestDeps")) { -- def content = """ -- -- -- d41d8cd98f00b204e9800998ecf8427e -- 3ae8f24a165a8cedc005670c81a27295 -- 5fea7756-0ea4-451a-a703-a558b933e274 -- -- 27daac76-34dd-47df-bd01-1f6e873584a0 -- -- """ -- return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) -+ "Sfn" | "DescribeStateMachine" | "POST" | "UNKNOWN" | SfnClient.builder() -+ | { c -> c.describeStateMachine(DescribeStateMachineRequest.builder().stateMachineArn("stateMachineArn").build()) } -+ | "" -+ "Sfn" | "DescribeActivity" | "POST" | "UNKNOWN" | SfnClient.builder() -+ | { c -> c.describeActivity(DescribeActivityRequest.builder().activityArn("activityArn").build()) } -+ | "" -+ "Lambda" | "GetFunction" | "GET" | "UNKNOWN" | LambdaClient.builder() -+ | { c -> c.getFunction(GetFunctionRequest.builder().functionName("functionName").build()) } -+ | "" -+ "Lambda" | "GetEventSourceMapping" | "GET" |"UNKNOWN" | LambdaClient.builder() -+ | { c -> c.getEventSourceMapping(GetEventSourceMappingRequest.builder().uuid("sourceEventId").build()) } -+ | "" -+ "SecretsManager" | "GetSecretValue" | "POST" | "UNKNOWN" | SecretsManagerClient.builder() -+ | { c -> c.getSecretValue(GetSecretValueRequest.builder().secretId("someSecret1").build()) } -+ | """ -+ { -+ "ARN":"someSecretArn", -+ "CreatedDate":1.523477145713E9, -+ "Name":"MyTestDatabaseSecret", -+ "SecretString":"{\\n \\"username\\":\\"david\\",\\n \\"password\\":\\"EXAMPLE-PASSWORD\\"\\n}\\n", -+ "VersionId":"EXAMPLE1-90ab-cdef-fedc-ba987SECRET1" - } -- def content = """ -- { -- "MD5OfMessageBody":"d41d8cd98f00b204e9800998ecf8427e", -- "MD5OfMessageAttributes":"3ae8f24a165a8cedc005670c81a27295", -- "MessageId":"5fea7756-0ea4-451a-a703-a558b933e274" -- } -- """ -- ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) -- .contentType(MediaType.PLAIN_TEXT_UTF_8) -- .add("x-amzn-RequestId", "27daac76-34dd-47df-bd01-1f6e873584a0") -- .build() -- return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) -- } -- "Ec2" | "AllocateAddress" | "POST" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2AsyncClient.builder() | { c -> c.allocateAddress() } | """ -- -- 59dbff89-35bd-4eac-99ed-be587EXAMPLE -- 192.0.2.1 -- standard -- -- """ -- "Rds" | "DeleteOptionGroup" | "POST" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsAsyncClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """ -- -- 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 -- -- """ -- "Sns" | "Publish" | "POST" | "f187a3c1-376f-11df-8963-01868b7c937a" | SnsAsyncClient.builder() | { SnsAsyncClient c -> c.publish(r -> r.message("hello").topicArn("somearn")) } | """ -- -- -- 94f20ce6-13c5-43a0-9a9e-ca52d816e90b -- -- -- f187a3c1-376f-11df-8963-01868b7c937a -- -- -- """ -+ """ - } - -+// def "send #operation async request with builder #builder.class.getName() mocked response"() { -+// assumeSupportedConfig(service, operation) -+// setup: -+// configureSdkClient(builder) -+// def client = builder -+// .endpointOverride(clientUri) -+// .region(Region.AP_NORTHEAST_1) -+// .credentialsProvider(CREDENTIALS_PROVIDER) -+// .build() -+// -+// if (body instanceof Closure) { -+// server.enqueue(body.call()) -+// } else { -+// server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) -+// } -+// -+// def response = call.call(client) -+// if (response instanceof Future) { -+// response = response.get() -+// } -+// -+// expect: -+// response != null -+// -+// assertTraces(1) { -+// trace(0, 1) { -+// span(0) { -+// name operation != "SendMessage" ? "$service.$operation" : "somequeue publish" -+// kind operation != "SendMessage" ? CLIENT : PRODUCER -+// hasNoParent() -+// attributes { -+// if (service == "S3") { -+// // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the bucket name in case -+// // the bucket name is a valid DNS label, even in the case that we are using an endpoint override. -+// // Previously the sdk was only doing that if endpoint had "s3" as label in the FQDN. -+// // Our test assert both cases so that we don't need to know what version is being tested. -+// "$ServerAttributes.SERVER_ADDRESS" { it == "somebucket.localhost" || it == "localhost" } -+// "$UrlAttributes.URL_FULL" { it.startsWith("http://somebucket.localhost:${server.httpPort()}") || it.startsWith("http://localhost:${server.httpPort()}") } -+// } else { -+// "$ServerAttributes.SERVER_ADDRESS" "localhost" -+// "$UrlAttributes.URL_FULL" { it == "http://localhost:${server.httpPort()}" || it == "http://localhost:${server.httpPort()}/" } -+// } -+// "$ServerAttributes.SERVER_PORT" server.httpPort() -+// "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" -+// "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 -+// "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" -+// "$RpcIncubatingAttributes.RPC_SERVICE" "$service" -+// "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" -+// "aws.agent" "java-aws-sdk" -+// "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" -+// if (service == "S3") { -+// "aws.bucket.name" "somebucket" -+// } else if (service == "Sqs" && operation == "CreateQueue") { -+// "aws.queue.name" "somequeue" -+// } else if (service == "Sqs" && operation == "SendMessage") { -+// "aws.queue.url" QUEUE_URL -+// "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somequeue" -+// "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" -+// "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String -+// "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemIncubatingValues.AWS_SQS -+// } else if (service == "Kinesis") { -+// "aws.stream.name" "somestream" -+// } else if (service == "Sns") { -+// "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somearn" -+// } -+// } -+// } -+// } -+// } -+// def request = server.takeRequest() -+// request.request().headers().get("X-Amzn-Trace-Id") != null -+// request.request().headers().get("traceparent") == null -+// -+// if (service == "Sns" && operation == "Publish") { -+// def content = request.request().content().toStringUtf8() -+// def containsId = content.contains("${traces[0][0].traceId}-${traces[0][0].spanId}") -+// def containsTp = content.contains("=traceparent") -+// if (isSqsAttributeInjectionEnabled()) { -+// assert containsId && containsTp -+// } else { -+// assert !containsId && !containsTp -+// } -+// } -+// -+// where: -+// service | operation | method | requestId | builder | call | body -+// "S3" | "CreateBucket" | "PUT" | "UNKNOWN" | s3AsyncClientBuilder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" -+// "S3" | "GetObject" | "GET" | "UNKNOWN" | s3AsyncClientBuilder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build(), AsyncResponseTransformer.toBytes()) } | "1234567890" -+// // Kinesis seems to expect an http2 response which is incompatible with our test server. -+// // "Kinesis" | "DeleteStream" | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" -+// "Sqs" | "CreateQueue" | "POST" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsAsyncClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | { -+// if (!Boolean.getBoolean("testLatestDeps")) { -+// def content = """ -+// -+// https://queue.amazonaws.com/123456789012/MyQueue -+// 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 -+// -+// """ -+// return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) -+// } -+// def content = """ -+// { -+// "QueueUrl":"https://queue.amazonaws.com/123456789012/MyQueue" -+// } -+// """ -+// ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) -+// .contentType(MediaType.PLAIN_TEXT_UTF_8) -+// .add("x-amzn-RequestId", "7a62c49f-347e-4fc4-9331-6e8e7a96aa73") -+// .build() -+// return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) -+// } -+// "Sqs" | "SendMessage" | "POST" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsAsyncClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl(QUEUE_URL).messageBody("").build()) } | { -+// if (!Boolean.getBoolean("testLatestDeps")) { -+// def content = """ -+// -+// -+// d41d8cd98f00b204e9800998ecf8427e -+// 3ae8f24a165a8cedc005670c81a27295 -+// 5fea7756-0ea4-451a-a703-a558b933e274 -+// -+// 27daac76-34dd-47df-bd01-1f6e873584a0 -+// -+// """ -+// return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) -+// } -+// def content = """ -+// { -+// "MD5OfMessageBody":"d41d8cd98f00b204e9800998ecf8427e", -+// "MD5OfMessageAttributes":"3ae8f24a165a8cedc005670c81a27295", -+// "MessageId":"5fea7756-0ea4-451a-a703-a558b933e274" -+// } -+// """ -+// ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) -+// .contentType(MediaType.PLAIN_TEXT_UTF_8) -+// .add("x-amzn-RequestId", "27daac76-34dd-47df-bd01-1f6e873584a0") -+// .build() -+// return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) -+// } -+// "Ec2" | "AllocateAddress" | "POST" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2AsyncClient.builder() | { c -> c.allocateAddress() } | """ -+// -+// 59dbff89-35bd-4eac-99ed-be587EXAMPLE -+// 192.0.2.1 -+// standard -+// -+// """ -+// "Rds" | "DeleteOptionGroup" | "POST" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsAsyncClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """ -+// -+// 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 -+// -+// """ -+// "Sns" | "Publish" | "POST" | "f187a3c1-376f-11df-8963-01868b7c937a" | SnsAsyncClient.builder() | { SnsAsyncClient c -> c.publish(r -> r.message("hello").topicArn("somearn")) } | """ -+// -+// -+// 94f20ce6-13c5-43a0-9a9e-ca52d816e90b -+// -+// -+// f187a3c1-376f-11df-8963-01868b7c937a -+// -+// -+// """ -+// } -+ - // 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 -+++ b/version.gradle.kts -@@ -1,5 +1,5 @@ --val stableVersion = "2.11.0" --val alphaVersion = "2.11.0-alpha" -+val stableVersion = "2.11.0-adot3" -+val alphaVersion = "2.11.0-adot3-alpha" - - allprojects { - if (findProperty("otel.stable") != "true") { diff --git a/.github/scripts/patch.sh b/.github/scripts/patch.sh index 7bbfc7356a..b6a6bba94e 100755 --- a/.github/scripts/patch.sh +++ b/.github/scripts/patch.sh @@ -6,7 +6,6 @@ set -x -e -u # This is used so that we can properly clone the upstream repositories. # This file should define the following variables: # OTEL_JAVA_VERSION. Tag of the opentelemetry-java repository to use. E.g.: JAVA_OTEL_JAVA_VERSION=v1.21.0 -# OTEL_JAVA_INSTRUMENTATION_VERSION. Tag of the opentelemetry-java-instrumentation repository to use, e.g.: OTEL_JAVA_INSTRUMENTATION_VERSION=v1.21.0 # OTEL_JAVA_CONTRIB_VERSION. Tag of the opentelemetry-java-contrib repository. E.g.: OTEL_JAVA_CONTRIB_VERSION=v1.21.0 # This script will fail if a variable that is supposed to exist is referenced. @@ -45,16 +44,3 @@ if [[ -f "$OTEL_JAVA_CONTRIB_PATCH" ]]; then else echo "Skipping patching opentelemetry-java-contrib" fi - - -OTEL_JAVA_INSTRUMENTATION_PATCH=".github/patches/opentelemetry-java-instrumentation.patch" -if [[ -f "$OTEL_JAVA_INSTRUMENTATION_PATCH" ]]; then - git clone https://github.com/open-telemetry/opentelemetry-java-instrumentation.git - cd opentelemetry-java-instrumentation - git checkout ${OTEL_JAVA_INSTRUMENTATION_VERSION} -b tag-${OTEL_JAVA_INSTRUMENTATION_VERSION} - patch -p1 < "../${OTEL_JAVA_INSTRUMENTATION_PATCH}" - git commit -a -m "ADOT Patch release" - cd - -else - echo "Skipping patching opentelemetry-java-instrumentation" -fi diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 55ed00a16c..7bb24e3543 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-adot3" +val otelVersion = "2.11.0" val otelSnapshotVersion = "2.12.0" val otelAlphaVersion = if (!testSnapshots) "$otelVersion-alpha" else "$otelSnapshotVersion-alpha-SNAPSHOT" val otelJavaAgentVersion = if (!testSnapshots) otelVersion else "$otelSnapshotVersion-SNAPSHOT" diff --git a/instrumentation/aws-sdk/README.md b/instrumentation/aws-sdk/README.md index 1b4d677d3e..9ac6c79290 100644 --- a/instrumentation/aws-sdk/README.md +++ b/instrumentation/aws-sdk/README.md @@ -152,12 +152,32 @@ _Class Functionalities:_ ### Commands for Running Groovy Tests -To run the BedrockJsonParserTest for aws-sdk v1.11: +#### aws-sdk v1.11 +To run the `BedrockJsonParserTest`: ```` ./gradlew :instrumentation:aws-sdk:test --tests "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.BedrockJsonParserTest" ```` -To run the BedrockJsonParserTest for aws-sdk v2.2: +#### aws-sdk v2.2 +To run the `BedrockJsonParserTest`: ```` ./gradlew :instrumentation:aws-sdk:test --tests "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.BedrockJsonParserTest" -```` \ No newline at end of file +```` + +### Commands for Running Java Tests + +#### aws-sdk v1.11 +To run the `AwsSdkExperimentalAttributesInjectionTest`: +```` +./gradlew :instrumentation:aws-sdk:test --tests "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AwsSdkExperimentalAttributesInjectionTest" +```` + +To run the `AdotAwsSdkClientAdviceTest`: +```` +./gradlew :instrumentation:aws-sdk:test --tests "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AdotAwsSdkClientAdviceTest" +```` + +#### aws-sdk v2.2 +To run the `AwsSdkExperimentalAttributesInjectionTest`: +```` +./gradlew :instrumentation:aws-sdk:test --tests "software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AwsSdkExperimentalAttributesInjectionTest" \ No newline at end of file diff --git a/instrumentation/aws-sdk/build.gradle.kts b/instrumentation/aws-sdk/build.gradle.kts index 5863df2a10..101e966a12 100644 --- a/instrumentation/aws-sdk/build.gradle.kts +++ b/instrumentation/aws-sdk/build.gradle.kts @@ -28,16 +28,17 @@ dependencies { 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") + testImplementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") + + testImplementation("software.amazon.awssdk:aws-core:2.2.0") + testImplementation("com.amazonaws:aws-java-sdk-lambda:1.11.678") + testImplementation("com.amazonaws:aws-java-sdk-kinesis:1.11.106") + testImplementation("com.amazonaws:aws-java-sdk-sns:1.11.106") + testImplementation("com.amazonaws:aws-java-sdk-stepfunctions:1.11.230") + testImplementation("com.amazonaws:aws-java-sdk-secretsmanager:1.11.309") } diff --git a/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsSdkExperimentalAttributesInjectionTest.java b/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsSdkExperimentalAttributesInjectionTest.java new file mode 100644 index 0000000000..d019842ce1 --- /dev/null +++ b/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v1_11/AwsSdkExperimentalAttributesInjectionTest.java @@ -0,0 +1,220 @@ +/* + * 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.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import com.amazonaws.Request; +import com.amazonaws.Response; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.handlers.HandlerContextKey; +import com.amazonaws.services.kinesis.model.PutRecordRequest; +import com.amazonaws.services.lambda.model.CreateFunctionRequest; +import com.amazonaws.services.lambda.model.FunctionConfiguration; +import com.amazonaws.services.lambda.model.GetFunctionResult; +import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest; +import com.amazonaws.services.sns.model.PublishRequest; +import com.amazonaws.services.stepfunctions.model.StartExecutionRequest; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.context.Context; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/* + * NOTE: V1.11 attribute extraction is difficult to test in unit tests due to reflection-based + * method access via MethodHandle. Many tests here only verify that the extractor correctly + * identifies different AWS service types rather than actual attribute extraction. However, these + * attributes are comprehensively tested in the contract tests which provide end-to-end validation + * of the reflection-based extraction logic. The contract tests cover most V1.11 attributes + * including all Bedrock Gen AI attributes. + */ +class AwsSdkExperimentalAttributesInjectionTest { + + private AwsSdkExperimentalAttributesExtractor extractor; + private AttributesBuilder attributes; + private Request mockRequest; + private Response mockResponse; + private static final HandlerContextKey AWS_CREDENTIALS = + new HandlerContextKey<>("AWSCredentials"); + + @BeforeEach + void setUp() { + extractor = new AwsSdkExperimentalAttributesExtractor(); + attributes = mock(AttributesBuilder.class); + mockRequest = mock(Request.class); + mockResponse = mock(Response.class); + } + + @Test + void testSnsExperimentalAttributes() { + PublishRequest snsRequest = mock(PublishRequest.class); + when(mockRequest.getServiceName()).thenReturn("AmazonSNS"); + when(mockRequest.getOriginalRequest()).thenReturn(snsRequest); + when(snsRequest.getTopicArn()).thenReturn("arn:aws:sns:region:account:topic/test"); + + extractor.onStart(attributes, Context.current(), mockRequest); + + verify(attributes) + .put( + eq(AwsExperimentalAttributes.AWS_SNS_TOPIC_ARN), + eq("arn:aws:sns:region:account:topic/test")); + } + + @Test + void testKinesisExperimentalAttributes() { + PutRecordRequest kinesisRequest = mock(PutRecordRequest.class); + when(mockRequest.getServiceName()).thenReturn("AmazonKinesis"); + when(mockRequest.getOriginalRequest()).thenReturn(kinesisRequest); + when(kinesisRequest.getStreamARN()).thenReturn("arn:aws:kinesis:region:account:stream/test"); + + extractor.onStart(attributes, Context.current(), mockRequest); + + verify(attributes) + .put( + eq(AwsExperimentalAttributes.AWS_STREAM_ARN), + eq("arn:aws:kinesis:region:account:stream/test")); + } + + @Test + void testStepFunctionsExperimentalAttributes() { + StartExecutionRequest sfnRequest = mock(StartExecutionRequest.class); + when(mockRequest.getServiceName()).thenReturn("AWSStepFunctions"); + when(mockRequest.getOriginalRequest()).thenReturn(sfnRequest); + when(sfnRequest.getStateMachineArn()) + .thenReturn("arn:aws:states:region:account:stateMachine/test"); + + extractor.onStart(attributes, Context.current(), mockRequest); + + verify(attributes) + .put( + eq(AwsExperimentalAttributes.AWS_STATE_MACHINE_ARN), + eq("arn:aws:states:region:account:stateMachine/test")); + } + + @Test + void testAuthAccessKeyAttributes() { + AWSCredentials credentials = mock(AWSCredentials.class); + when(mockRequest.getHandlerContext(AWS_CREDENTIALS)).thenReturn(credentials); + when(credentials.getAWSAccessKeyId()).thenReturn("AKIAIOSFODNN7EXAMPLE"); + when(mockRequest.getOriginalRequest()).thenReturn(mock(PublishRequest.class)); + when(mockRequest.getServiceName()).thenReturn("AmazonSNS"); + + extractor.onStart(attributes, Context.current(), mockRequest); + + verify(attributes) + .put(eq(AwsExperimentalAttributes.AWS_AUTH_ACCESS_KEY), eq("AKIAIOSFODNN7EXAMPLE")); + } + + @Test + void testSecretsManagerExperimentalAttributes() { + GetSecretValueRequest secretRequest = mock(GetSecretValueRequest.class); + when(mockRequest.getServiceName()).thenReturn("AWSSecretsManager"); + when(mockRequest.getOriginalRequest()).thenReturn(secretRequest); + + extractor.onStart(attributes, Context.current(), mockRequest); + // We're not verifying anything here since the actual attribute setting depends on reflection + } + + @Test + void testLambdaNameExperimentalAttributes() { + CreateFunctionRequest lambdaRequest = mock(CreateFunctionRequest.class); + when(mockRequest.getServiceName()).thenReturn("AWSLambda"); + when(mockRequest.getOriginalRequest()).thenReturn(lambdaRequest); + when(lambdaRequest.getFunctionName()).thenReturn("test-function"); + + extractor.onStart(attributes, Context.current(), mockRequest); + + verify(attributes).put(eq(AwsExperimentalAttributes.AWS_LAMBDA_NAME), eq("test-function")); + } + + @Test + void testLambdaArnExperimentalAttributes() { + GetFunctionResult lambdaResult = mock(GetFunctionResult.class); + FunctionConfiguration config = mock(FunctionConfiguration.class); + when(mockResponse.getAwsResponse()).thenReturn(lambdaResult); + when(lambdaResult.getConfiguration()).thenReturn(config); + when(config.getFunctionArn()).thenReturn("arn:aws:lambda:region:account:function:test"); + when(mockRequest.getServiceName()).thenReturn("AWSLambda"); + + extractor.onEnd(attributes, Context.current(), mockRequest, mockResponse, null); + + verify(attributes) + .put( + eq(AwsExperimentalAttributes.AWS_LAMBDA_ARN), + eq("arn:aws:lambda:region:account:function:test")); + } + + @Test + void testLambdaResourceIdExperimentalAttributes() { + PublishRequest originalRequest = mock(PublishRequest.class); + when(mockRequest.getServiceName()).thenReturn("AWSLambda"); + when(mockRequest.getOriginalRequest()).thenReturn(originalRequest); + + extractor.onStart(attributes, Context.current(), mockRequest); + // We can't verify the actual attribute setting since it depends on reflection + } + + @Test + void testTableArnExperimentalAttributes() { + PublishRequest originalRequest = mock(PublishRequest.class); + when(mockRequest.getServiceName()).thenReturn("AmazonDynamoDBv2"); + when(mockRequest.getOriginalRequest()).thenReturn(originalRequest); + + extractor.onStart(attributes, Context.current(), mockRequest); + // We can't verify the actual attribute setting since it depends on reflection + } + + @Test + void testBedrockRuntimeAttributes() { + PublishRequest originalRequest = mock(PublishRequest.class); + when(mockRequest.getServiceName()).thenReturn("AmazonBedrockRuntime"); + when(mockRequest.getOriginalRequest()).thenReturn(originalRequest); + + extractor.onStart(attributes, Context.current(), mockRequest); + // We can't verify the actual attribute setting since it depends on reflection and class name + } + + @Test + void testBedrockAgentAttributes() { + PublishRequest originalRequest = mock(PublishRequest.class); + when(mockRequest.getServiceName()).thenReturn("AWSBedrockAgent"); + when(mockRequest.getOriginalRequest()).thenReturn(originalRequest); + + extractor.onStart(attributes, Context.current(), mockRequest); + // We can't verify the actual attribute setting since it depends on reflection + } + + @Test + void testBedrockAgentRuntimeAttributes() { + PublishRequest originalRequest = mock(PublishRequest.class); + when(mockRequest.getServiceName()).thenReturn("AWSBedrockAgentRuntime"); + when(mockRequest.getOriginalRequest()).thenReturn(originalRequest); + + extractor.onStart(attributes, Context.current(), mockRequest); + // We can't verify the actual attribute setting since it depends on reflection + } + + @Test + void testBedrockGuardrailAttributes() { + PublishRequest originalRequest = mock(PublishRequest.class); + when(mockRequest.getServiceName()).thenReturn("AmazonBedrock"); + when(mockRequest.getOriginalRequest()).thenReturn(originalRequest); + + extractor.onStart(attributes, Context.current(), mockRequest); + // We can't verify the actual attribute setting since it depends on reflection + } +} diff --git a/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkExperimentalAttributesInjectionTest.java b/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkExperimentalAttributesInjectionTest.java new file mode 100644 index 0000000000..b5cc1a079c --- /dev/null +++ b/instrumentation/aws-sdk/src/test/java/software/amazon/opentelemetry/javaagent/instrumentation/awssdk_v2_2/AwsSdkExperimentalAttributesInjectionTest.java @@ -0,0 +1,274 @@ +/* + * 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 org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.trace.Span; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; + +/* + * NOTE: V2.2 attribute extraction uses direct field access via getValueForField() method. + * These tests can fully verify attribute extraction by mocking the field values and verifying + * the correct attributes are set on the span. This provides comprehensive coverage of the + * attribute extraction logic, supplementing the V2 contract tests. + */ +public class AwsSdkExperimentalAttributesInjectionTest { + private FieldMapper fieldMapper; + private Span mockSpan; + private SdkRequest mockRequest; + + @BeforeEach + void setUp() { + fieldMapper = new FieldMapper(); + mockSpan = mock(Span.class); + mockRequest = mock(SdkRequest.class); + } + + @Test + void testS3ExperimentalAttributes() { + when(mockRequest.getValueForField("Bucket", Object.class)) + .thenReturn(Optional.of("test-bucket")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.S3Request, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_BUCKET_NAME.getKey()), eq("test-bucket")); + } + + @Test + void testSqsExperimentalAttributes() { + String queueUrl = "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue"; + when(mockRequest.getValueForField("QueueUrl", Object.class)).thenReturn(Optional.of(queueUrl)); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.SqsRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_QUEUE_URL.getKey()), eq(queueUrl)); + } + + @Test + void testDynamoDbExperimentalAttributes() { + when(mockRequest.getValueForField("TableName", Object.class)) + .thenReturn(Optional.of("test-table")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.DynamoDbRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_TABLE_NAME.getKey()), eq("test-table")); + } + + @Test + void testSnsExperimentalAttributes() { + String topicArn = "arn:aws:sns:us-east-1:123456789012:test-topic"; + when(mockRequest.getValueForField("TopicArn", Object.class)).thenReturn(Optional.of(topicArn)); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.SnsRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_SNS_TOPIC_ARN.getKey()), eq(topicArn)); + } + + @Test + void testKinesisExperimentalAttributes() { + when(mockRequest.getValueForField("StreamName", Object.class)) + .thenReturn(Optional.of("test-stream")); + when(mockRequest.getValueForField("StreamARN", Object.class)) + .thenReturn(Optional.of("arn:aws:kinesis:region:account:stream/test-stream")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.KinesisRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_STREAM_NAME.getKey()), eq("test-stream")); + verify(mockSpan) + .setAttribute( + eq(AwsExperimentalAttributes.AWS_STREAM_ARN.getKey()), + eq("arn:aws:kinesis:region:account:stream/test-stream")); + } + + @Test + void testStepFunctionExperimentalAttributes() { + when(mockRequest.getValueForField("stateMachineArn", Object.class)) + .thenReturn(Optional.of("arn:aws:states:region:account:stateMachine/test")); + when(mockRequest.getValueForField("activityArn", Object.class)) + .thenReturn(Optional.of("arn:aws:states:region:account:activity/test")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.SfnRequest, mockSpan); + + verify(mockSpan) + .setAttribute( + eq(AwsExperimentalAttributes.AWS_STATE_MACHINE_ARN.getKey()), + eq("arn:aws:states:region:account:stateMachine/test")); + verify(mockSpan) + .setAttribute( + eq(AwsExperimentalAttributes.AWS_STEP_FUNCTIONS_ACTIVITY_ARN.getKey()), + eq("arn:aws:states:region:account:activity/test")); + } + + @Test + void testAuthAccessKeyExperimentalAttribute() { + mockSpan.setAttribute( + AwsExperimentalAttributes.AWS_AUTH_ACCESS_KEY.getKey(), "AKIAIOSFODNN7EXAMPLE"); + + verify(mockSpan) + .setAttribute( + eq(AwsExperimentalAttributes.AWS_AUTH_ACCESS_KEY.getKey()), eq("AKIAIOSFODNN7EXAMPLE")); + } + + @Test + void testAuthRegionExperimentalAttribute() { + mockSpan.setAttribute(AwsExperimentalAttributes.AWS_AUTH_REGION.getKey(), "us-east-1"); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_AUTH_REGION.getKey()), eq("us-east-1")); + } + + @Test + void testSecretsManagerExperimentalAttributes() { + SdkResponse mockResponse = mock(SdkResponse.class); + when(mockResponse.getValueForField("ARN", Object.class)) + .thenReturn(Optional.of("arn:aws:secretsmanager:region:account:secret:test")); + + fieldMapper.mapToAttributes(mockResponse, AwsSdkRequest.SecretsManagerRequest, mockSpan); + + verify(mockSpan) + .setAttribute( + eq(AwsExperimentalAttributes.AWS_SECRET_ARN.getKey()), + eq("arn:aws:secretsmanager:region:account:secret:test")); + } + + @Test + void testLambdaNameExperimentalAttribute() { + when(mockRequest.getValueForField("FunctionName", Object.class)) + .thenReturn(Optional.of("test-function")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.LambdaRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_LAMBDA_NAME.getKey()), eq("test-function")); + } + + @Test + void testLambdaResourceIdExperimentalAttribute() { + when(mockRequest.getValueForField("UUID", Object.class)) + .thenReturn(Optional.of("12345678-1234-1234-1234-123456789012")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.LambdaRequest, mockSpan); + + verify(mockSpan) + .setAttribute( + eq(AwsExperimentalAttributes.AWS_LAMBDA_RESOURCE_ID.getKey()), + eq("12345678-1234-1234-1234-123456789012")); + } + + @Test + void testLambdaArnExperimentalAttribute() { + mockSpan.setAttribute( + AwsExperimentalAttributes.AWS_LAMBDA_ARN.getKey(), + "arn:aws:lambda:us-east-1:123456789012:function:test-function"); + + verify(mockSpan) + .setAttribute( + eq(AwsExperimentalAttributes.AWS_LAMBDA_ARN.getKey()), + eq("arn:aws:lambda:us-east-1:123456789012:function:test-function")); + } + + @Test + void testTableArnExperimentalAttribute() { + mockSpan.setAttribute( + AwsExperimentalAttributes.AWS_TABLE_ARN.getKey(), + "arn:aws:dynamodb:us-east-1:123456789012:table/test-table"); + + verify(mockSpan) + .setAttribute( + eq(AwsExperimentalAttributes.AWS_TABLE_ARN.getKey()), + eq("arn:aws:dynamodb:us-east-1:123456789012:table/test-table")); + } + + @Test + void testBedrockExperimentalAttributes() { + String modelId = "anthropic.claude-v2"; + SdkBytes requestBody = SdkBytes.fromUtf8String("{\"max_tokens\": 100, \"temperature\": 0.7}"); + + when(mockRequest.getValueForField("modelId", Object.class)).thenReturn(Optional.of(modelId)); + when(mockRequest.getValueForField("body", Object.class)).thenReturn(Optional.of(requestBody)); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.BedrockRuntimeRequest, mockSpan); + + verify(mockSpan).setAttribute(eq(AwsExperimentalAttributes.GEN_AI_MODEL.getKey()), eq(modelId)); + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.GEN_AI_REQUEST_MAX_TOKENS.getKey()), eq("100")); + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.GEN_AI_REQUEST_TEMPERATURE.getKey()), eq("0.7")); + } + + @Test + void testBedrockAgentExperimentalAttributes() { + when(mockRequest.getValueForField("agentId", Object.class)) + .thenReturn(Optional.of("test-agent")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.BedrockBedrockAgentRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_AGENT_ID.getKey()), eq("test-agent")); + } + + @Test + void testBedrockAgentRuntimeExperimentalAttributes() { + when(mockRequest.getValueForField("agentId", Object.class)) + .thenReturn(Optional.of("test-agent")); + when(mockRequest.getValueForField("knowledgeBaseId", Object.class)) + .thenReturn(Optional.of("test-kb")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.BedrockAgentRuntimeRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_AGENT_ID.getKey()), eq("test-agent")); + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_KNOWLEDGE_BASE_ID.getKey()), eq("test-kb")); + } + + @Test + void testBedrockDataSourceExperimentalAttributes() { + when(mockRequest.getValueForField("dataSourceId", Object.class)) + .thenReturn(Optional.of("test-ds")); + + fieldMapper.mapToAttributes(mockRequest, AwsSdkRequest.BedrockGetDataSourceRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_DATA_SOURCE_ID.getKey()), eq("test-ds")); + } + + @Test + void testBedrockKnowledgeBaseExperimentalAttributes() { + when(mockRequest.getValueForField("knowledgeBaseId", Object.class)) + .thenReturn(Optional.of("test-kb")); + + fieldMapper.mapToAttributes( + mockRequest, AwsSdkRequest.BedrockGetKnowledgeBaseRequest, mockSpan); + + verify(mockSpan) + .setAttribute(eq(AwsExperimentalAttributes.AWS_KNOWLEDGE_BASE_ID.getKey()), eq("test-kb")); + } +} diff --git a/lambda-layer/build-layer.sh b/lambda-layer/build-layer.sh index 36350cd5b1..8c944191de 100755 --- a/lambda-layer/build-layer.sh +++ b/lambda-layer/build-layer.sh @@ -22,9 +22,6 @@ git clone https://github.com/open-telemetry/opentelemetry-java-instrumentation.g pushd opentelemetry-java-instrumentation git checkout v${version} -b tag-v${version} -# There is another patch in the .github/patches directory for other changes. We should apply them too for consistency. -patch -p1 < "$SOURCEDIR"/../.github/patches/opentelemetry-java-instrumentation.patch - # This patch is for Lambda related context propagation patch -p1 < "$SOURCEDIR"/patches/opentelemetry-java-instrumentation.patch diff --git a/lambda-layer/patches/aws-otel-java-instrumentation.patch b/lambda-layer/patches/aws-otel-java-instrumentation.patch index 2c6dfa70dd..6b1f5eb9d5 100644 --- a/lambda-layer/patches/aws-otel-java-instrumentation.patch +++ b/lambda-layer/patches/aws-otel-java-instrumentation.patch @@ -3,11 +3,11 @@ index 9493189..6090207 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 TEST_SNAPSHOTS = rootProject.findProperty("testUpstreamSnapshots") == "true" + val testSnapshots = rootProject.findProperty("testUpstreamSnapshots") == "true" // This is the version of the upstream instrumentation BOM --val otelVersion = "2.11.0-adot3" +-val otelVersion = "2.11.0" +val otelVersion = "2.11.0-adot-lambda1" val otelSnapshotVersion = "2.12.0" - val otelAlphaVersion = if (!TEST_SNAPSHOTS) "$otelVersion-alpha" else "$otelSnapshotVersion-alpha-SNAPSHOT" - val otelJavaAgentVersion = if (!TEST_SNAPSHOTS) otelVersion else "$otelSnapshotVersion-SNAPSHOT" + val otelAlphaVersion = if (!testSnapshots) "$otelVersion-alpha" else "$otelSnapshotVersion-alpha-SNAPSHOT" + val otelJavaAgentVersion = if (!testSnapshots) otelVersion else "$otelSnapshotVersion-SNAPSHOT" diff --git a/lambda-layer/patches/opentelemetry-java-instrumentation.patch b/lambda-layer/patches/opentelemetry-java-instrumentation.patch index 127751f5b0..a4004e3330 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-adot3" --val alphaVersion = "2.11.0-adot3-alpha" +-val stableVersion = "2.11.0" +-val alphaVersion = "2.11.0-alpha" +val stableVersion = "2.11.0-adot-lambda1" +val alphaVersion = "2.11.0-adot-lambda1-alpha" diff --git a/scripts/local_patch.sh b/scripts/local_patch.sh index d1c01c5d8b..079d4516b9 100755 --- a/scripts/local_patch.sh +++ b/scripts/local_patch.sh @@ -56,28 +56,4 @@ if [[ -f "$OTEL_JAVA_CONTRIB_PATCH" ]]; then rm -rf opentelemetry-java-contrib else echo "Skipping patching opentelemetry-java-contrib" -fi - - -# Patching opentelemetry-java-instrumentation -OTEL_JAVA_INSTRUMENTATION_PATCH=".github/patches/opentelemetry-java-instrumentation.patch" -if [[ -f "$OTEL_JAVA_INSTRUMENTATION_PATCH" ]]; then - echo "Patching opentelemetry-java-instrumentation" - git clone https://github.com/open-telemetry/opentelemetry-java-instrumentation.git - cd opentelemetry-java-instrumentation - - echo "Checking out tag ${OTEL_JAVA_INSTRUMENTATION_VERSION}" - git checkout ${OTEL_JAVA_INSTRUMENTATION_VERSION} -b tag-${OTEL_JAVA_INSTRUMENTATION_VERSION} - patch -p1 < "../${OTEL_JAVA_INSTRUMENTATION_PATCH}" - git commit -a -m "ADOT Patch release" - - echo "Building patched opentelemetry-java-instrumentation" - ./gradlew clean assemble - ./gradlew publishToMavenLocal - cd - - - echo "Cleaning up opentelemetry-java-instrumentation" - rm -rf opentelemetry-java-instrumentation -else - echo "Skipping patching opentelemetry-java-instrumentation" fi \ No newline at end of file From 529bebe4f4ba1530d1da957fb5d5bf2990b4476d Mon Sep 17 00:00:00 2001 From: Steve Liu Date: Thu, 14 Aug 2025 15:26:57 -0700 Subject: [PATCH 6/6] Add Netty BOM (#1148) *Description of changes:* Builds are failing image scanning for `CVE-2025-55163` which recently was added as a vulnerability. https://github.com/advisories/GHSA-prj3-ccx8-p6x4 Should revert this once we upgrade our aws-sdk dependency to version that has this PR added: https://github.com/aws/aws-sdk-java-v2/pull/6344 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: Thomas Pierce --- dependencyManagement/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 7bb24e3543..11a6441070 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -40,6 +40,9 @@ val dependencyBoms = listOf( "com.google.protobuf:protobuf-bom:3.25.1", "com.linecorp.armeria:armeria-bom:1.26.4", "io.grpc:grpc-bom:1.59.1", + // netty-bom is a fix for CVE-2025-55163 (https://github.com/advisories/GHSA-prj3-ccx8-p6x4). + // Remove once https://github.com/aws/aws-sdk-java-v2/pull/6344 is released. + "io.netty:netty-bom:4.1.124.Final", "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha:$otelAlphaVersion", "org.apache.logging.log4j:log4j-bom:2.21.1", "org.junit:junit-bom:5.10.1",