Skip to content

Commit ac3c0c7

Browse files
authored
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.
1 parent 572215e commit ac3c0c7

File tree

7 files changed

+363
-2
lines changed

7 files changed

+363
-2
lines changed

instrumentation/aws-sdk/README.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,21 @@
33
### Overview
44
The aws-sdk instrumentation is an SPI-based implementation that extends the upstream OpenTelemetry AWS Java SDK instrumentation.
55

6-
_Initialization Workflow_
6+
##### _v1.11 Initialization Workflow_
7+
1. OpenTelemetry Agent Starts
8+
- Loads default instrumentations
9+
- Loads aws-sdk v1.11 instrumentations
10+
- Injects **TracingRequestHandler** into constructor
11+
2. Scans for other SPI implementations
12+
- Finds ADOT’s **AdotAwsSdkInstrumentationModule**
13+
- Injects code that:
14+
- Checks for TracingRequestHandler
15+
- If present, adds **AdotAwsSdkTracingRequestHandler**
16+
3. AWS SDK Client Created
17+
- Constructor runs with injected code:
18+
[AWS Handlers] → TracingRequestHandler → AdotAwsSdkTracingRequestHandler
19+
20+
##### _v2.2 Initialization Workflow_
721

822
1. OpenTelemetry Agent starts
923
- Loads default instrumentations
@@ -13,6 +27,40 @@ _Initialization Workflow_
1327
- Finds ADOT’s **AdotAwsSdkInstrumentationModule**
1428
- Registers **AdotAwsSdkTracingExecutionInterceptor** (order > 0)
1529

30+
### AWS SDK v1 Instrumentation Summary
31+
The AdotAwsSdkInstrumentationModule uses the instrumentation (specified in AdotAwsClientInstrumentation) to register the AdotAwsSdkTracingRequestHandler through `typeInstrumentations`.
32+
33+
Key aspects of handler registration:
34+
- `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.
35+
- `AdotAwsSdkClientInstrumentation` class adds ADOT handler to list of request handlers
36+
37+
**AdotAwsSdkClientInstrumentation**
38+
39+
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.
40+
41+
- `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
42+
initialized).
43+
- Ensures the OpenTelemetry handler is registered first.
44+
45+
**AdotAwsSdkTracingRequestHandler**
46+
47+
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.
48+
49+
1. `beforeRequest`: the latest point where the SDK request can be obtained after it is modified by the upstream aws-sdk v1.11 handler
50+
2. `afterAttempt`: the latest point to access the SDK response before the span closes in the upstream afterResponse/afterError methods
51+
- _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.
52+
- This is a trade-off:
53+
- We get to add our attributes before span closure
54+
- But our code runs redundantly on each retry attempt
55+
- We're constrained by when upstream closes the span
56+
57+
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)
58+
59+
_**Important Notes:**_
60+
- 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).
61+
- 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.
62+
`afterAttempt` is our final hook point, giving us access to both the fully processed response and active span.
63+
1664
### AWS SDK v2 Instrumentation Summary
1765

1866
**AdotAwsSdkInstrumentationModule**

instrumentation/aws-sdk/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ base.archivesBaseName = "aws-instrumentation-aws-sdk"
2222

2323
dependencies {
2424
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
25+
compileOnly("com.amazonaws:aws-java-sdk-core:1.11.0")
2526
compileOnly("software.amazon.awssdk:aws-core:2.2.0")
2627
compileOnly("net.bytebuddy:byte-buddy")
28+
29+
testImplementation("com.amazonaws:aws-java-sdk-core:1.11.0")
30+
testImplementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api")
31+
testImplementation("org.mockito:mockito-core:5.14.2")
2732
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11;
17+
18+
import static net.bytebuddy.matcher.ElementMatchers.*;
19+
20+
import com.amazonaws.handlers.RequestHandler2;
21+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
22+
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
23+
import java.util.List;
24+
import net.bytebuddy.asm.Advice;
25+
import net.bytebuddy.description.type.TypeDescription;
26+
import net.bytebuddy.matcher.ElementMatcher;
27+
28+
/**
29+
* This class provides instrumentation by injecting our request handler into the AWS client's
30+
* handler chain. Key components:
31+
*
32+
* <p>1. Type Matching: Targets AmazonWebServiceClient (base class for all AWS SDK v1.11 clients).
33+
* Ensures handler injection during client initialization.
34+
*
35+
* <p>2. Transformation: Uses ByteBuddy to modify the client constructor. Injects our handler
36+
* registration code.
37+
*
38+
* <p>3. Handler Registration (via Advice): Checks for existing OpenTelemetry handler and adds ADOT
39+
* handler only if: a) OpenTelemetry handler is present (ensuring base instrumentation) b) ADOT
40+
* handler isn't already added (preventing duplicates)
41+
*
42+
* <p>Based on OpenTelemetry Java Instrumentation's AWS SDK v1.11 AwsClientInstrumentation
43+
* (release/v2.11.x). Adapts the base instrumentation pattern to add ADOT-specific functionality.
44+
*
45+
* <p>Source: <a
46+
* 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>
47+
*/
48+
public class AdotAwsSdkClientInstrumentation implements TypeInstrumentation {
49+
@Override
50+
public ElementMatcher<TypeDescription> typeMatcher() {
51+
// AmazonWebServiceClient is the base interface for all AWS SDK clients.
52+
// Type matching against it ensures our interceptor is injected as soon as any AWS SDK client is
53+
// initialized.
54+
return named("com.amazonaws.AmazonWebServiceClient")
55+
.and(declaresField(named("requestHandler2s")));
56+
}
57+
58+
@Override
59+
public void transform(TypeTransformer transformer) {
60+
transformer.applyAdviceToMethod(
61+
isConstructor(),
62+
AdotAwsSdkClientInstrumentation.class.getName() + "$AdotAwsSdkClientAdvice");
63+
}
64+
65+
/**
66+
* Upstream handler registration: @see <a
67+
* 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>
68+
*/
69+
@SuppressWarnings("unused")
70+
public static class AdotAwsSdkClientAdvice {
71+
72+
@Advice.OnMethodExit(suppress = Throwable.class)
73+
public static void addHandler(
74+
@Advice.FieldValue(value = "requestHandler2s") List<RequestHandler2> handlers) {
75+
76+
if (handlers == null) {
77+
return;
78+
}
79+
80+
boolean hasOtelHandler = false;
81+
boolean hasAdotHandler = false;
82+
83+
// Checks if aws-sdk spans are enabled
84+
for (RequestHandler2 handler : handlers) {
85+
if (handler
86+
.toString()
87+
.contains(
88+
"io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.TracingRequestHandler")) {
89+
hasOtelHandler = true;
90+
}
91+
if (handler instanceof AdotAwsSdkTracingRequestHandler) {
92+
hasAdotHandler = true;
93+
break;
94+
}
95+
}
96+
97+
// Only adds our handler if aws-sdk spans are enabled. This also ensures upstream
98+
// instrumentation is applied first.
99+
if (hasOtelHandler && !hasAdotHandler) {
100+
handlers.add(new AdotAwsSdkTracingRequestHandler());
101+
}
102+
}
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11;
17+
18+
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.hasClassesNamed;
19+
20+
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
21+
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
22+
import java.util.Arrays;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import net.bytebuddy.matcher.ElementMatcher;
26+
27+
/**
28+
* Based on OpenTelemetry Java Instrumentation's AWS SDK v1.11 AbstractAwsSdkInstrumentationModule
29+
* (release/v2.11.x). Adapts the base instrumentation pattern to add ADOT-specific functionality.
30+
*
31+
* <p>Source: <a
32+
* 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>
33+
*/
34+
public class AdotAwsSdkInstrumentationModule extends InstrumentationModule {
35+
36+
public AdotAwsSdkInstrumentationModule() {
37+
super("aws-sdk-adot", "aws-sdk-1.11-adot");
38+
}
39+
40+
@Override
41+
public int order() {
42+
// Ensure this runs after OTel (> 0)
43+
return Integer.MAX_VALUE;
44+
}
45+
46+
@Override
47+
public List<String> getAdditionalHelperClassNames() {
48+
return Arrays.asList(
49+
"software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AdotAwsSdkTracingRequestHandler");
50+
}
51+
52+
@Override
53+
public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
54+
return hasClassesNamed("com.amazonaws.AmazonWebServiceClient");
55+
}
56+
57+
@Override
58+
public List<TypeInstrumentation> typeInstrumentations() {
59+
return Collections.singletonList(new AdotAwsSdkClientInstrumentation());
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11;
17+
18+
import com.amazonaws.Request;
19+
import com.amazonaws.handlers.HandlerAfterAttemptContext;
20+
import com.amazonaws.handlers.RequestHandler2;
21+
22+
/**
23+
* Based on OpenTelemetry Java Instrumentation's AWS SDK v1.11 TracingRequestHandler
24+
* (release/v2.11.x). Adapts the base instrumentation pattern to add ADOT-specific functionality.
25+
*
26+
* <p>Source: <a
27+
* 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>
28+
*/
29+
public class AdotAwsSdkTracingRequestHandler extends RequestHandler2 {
30+
31+
public AdotAwsSdkTracingRequestHandler() {}
32+
33+
/**
34+
* This is the latest point we can obtain the Sdk Request after it is modified by the upstream
35+
* TracingInterceptor. It ensures upstream handles the request and applies its changes first.
36+
*
37+
* <p>Upstream's last Sdk Request modification: @see <a
38+
* 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>
39+
*/
40+
@Override
41+
public void beforeRequest(Request<?> request) {}
42+
43+
/**
44+
* This is the latest point to access the sdk response before the span closes in the upstream
45+
* afterResponse/afterError methods. This ensures we capture attributes from the final, fully
46+
* modified response after all upstream interceptors have processed it.
47+
*
48+
* <p>Upstream's last Sdk Response modification before span closure: @see <a
49+
* 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>
50+
*
51+
* @see <a
52+
* 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>
53+
*/
54+
@Override
55+
public void afterAttempt(HandlerAfterAttemptContext context) {}
56+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkInstrumentationModule
1+
software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotAwsSdkInstrumentationModule
2+
software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11.AdotAwsSdkInstrumentationModule
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v1_11;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.when;
21+
22+
import com.amazonaws.handlers.RequestHandler2;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import org.junit.jupiter.api.BeforeEach;
26+
import org.junit.jupiter.api.Test;
27+
28+
class AdotAwsSdkClientAdviceTest {
29+
30+
private AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice advice;
31+
private List<RequestHandler2> handlers;
32+
33+
@BeforeEach
34+
void setUp() {
35+
advice = new AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice();
36+
handlers = new ArrayList<>();
37+
}
38+
39+
@Test
40+
void testAddHandlerWhenHandlersIsNull() {
41+
AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice.addHandler(null);
42+
assertThat(handlers).hasSize(0);
43+
}
44+
45+
@Test
46+
void testAddHandlerWhenNoOtelHandler() {
47+
RequestHandler2 someOtherHandler = mock(RequestHandler2.class);
48+
handlers.add(someOtherHandler);
49+
50+
AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice.addHandler(handlers);
51+
52+
assertThat(handlers).hasSize(1);
53+
assertThat(handlers).containsExactly(someOtherHandler);
54+
}
55+
56+
@Test
57+
void testAddHandlerWhenOtelHandlerPresent() {
58+
RequestHandler2 otelHandler = mock(RequestHandler2.class);
59+
when(otelHandler.toString())
60+
.thenReturn(
61+
"io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.TracingRequestHandler");
62+
handlers.add(otelHandler);
63+
64+
AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice.addHandler(handlers);
65+
66+
assertThat(handlers).hasSize(2);
67+
assertThat(handlers.get(0)).isEqualTo(otelHandler);
68+
assertThat(handlers.get(1)).isInstanceOf(AdotAwsSdkTracingRequestHandler.class);
69+
}
70+
71+
@Test
72+
void testAddHandlerWhenAdotHandlerAlreadyPresent() {
73+
RequestHandler2 otelHandler = mock(RequestHandler2.class);
74+
when(otelHandler.toString())
75+
.thenReturn(
76+
"io.opentelemetry.javaagent.instrumentation.awssdk.v1_11.TracingRequestHandler");
77+
handlers.add(otelHandler);
78+
handlers.add(new AdotAwsSdkTracingRequestHandler());
79+
80+
AdotAwsSdkClientInstrumentation.AdotAwsSdkClientAdvice.addHandler(handlers);
81+
82+
assertThat(handlers).hasSize(2);
83+
assertThat(handlers.get(0)).isEqualTo(otelHandler);
84+
assertThat(handlers.get(1)).isInstanceOf(AdotAwsSdkTracingRequestHandler.class);
85+
}
86+
}

0 commit comments

Comments
 (0)