Skip to content

Base of AWS SDK v1.11 SPI Implementation #1115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion instrumentation/aws-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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**
Expand Down
5 changes: 5 additions & 0 deletions instrumentation/aws-sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
@@ -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:
*
* <p>1. Type Matching: Targets AmazonWebServiceClient (base class for all AWS SDK v1.11 clients).
* Ensures handler injection during client initialization.
*
* <p>2. Transformation: Uses ByteBuddy to modify the client constructor. Injects our handler
* registration code.
*
* <p>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)
*
* <p>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.
*
* <p>Source: <a
* href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/release/v2.11.x/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsClientInstrumentation.java">...</a>
*/
public class AdotAwsSdkClientInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> 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 <a
* href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AwsClientInstrumentation.java#L39">...</a>
*/
@SuppressWarnings("unused")
public static class AdotAwsSdkClientAdvice {

@Advice.OnMethodExit(suppress = Throwable.class)
public static void addHandler(
@Advice.FieldValue(value = "requestHandler2s") List<RequestHandler2> 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());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Source: <a
* href="https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/release/v2.11.x/instrumentation/aws-sdk/aws-sdk-1.11/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/awssdk/v1_11/AbstractAwsSdkInstrumentationModule.java">...</a>
*/
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<String> getAdditionalHelperClassNames() {
return Arrays.asList(
"software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AdotAwsSdkTracingRequestHandler");
}

@Override
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
return hasClassesNamed("com.amazonaws.AmazonWebServiceClient");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(new AdotAwsSdkClientInstrumentation());
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Source: <a
* 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">...</a>
*/
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.
*
* <p>Upstream's last Sdk Request modification: @see <a
* 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</a>
*/
@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.
*
* <p>Upstream's last Sdk Response modification before span closure: @see <a
* 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#L116">reference</a>
*
* @see <a
* 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</a>
*/
@Override
public void afterAttempt(HandlerAfterAttemptContext context) {}
}
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkInstrumentationModule
software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkInstrumentationModule
software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AdotAwsSdkInstrumentationModule
Original file line number Diff line number Diff line change
@@ -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<RequestHandler2> 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);
}
}
Loading