-
Notifications
You must be signed in to change notification settings - Fork 64
AWS SDK v2.2 SPI Implementation for ADOT Java SDK #1091
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
Changes from all commits
f4af01e
d784099
8c6036d
e9b26d1
8effb7a
c7f59f7
40d5d0b
3061c4a
1e54bc2
f405de6
bf1b5db
837a63c
e53795f
82da476
0cfcf97
1d72356
8b30ec1
44ec960
e79bfea
c4bad5a
de877d3
96bbdcd
81c95b2
0bcd848
b721ecd
4d45e7f
494e0f6
56620b4
85ab778
9d92388
d06b996
16a73ca
a4c2d9d
1bc2c1d
e4f6b8c
9f2ffc7
e9b2fc3
69cfec5
f3dee27
6b3f471
0d54bca
2c4c7bf
a0fa5c9
f3f4f37
c2e53a4
c204c42
88a1fad
ddf3e6d
8cb3cc6
0b44af3
0749dfa
2e1c8f0
b3b4b18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
## ADOT AWS SDK Instrumentation | ||
|
||
### Overview | ||
The aws-sdk instrumentation is an SPI-based implementation to extend the AWS SDK within the upstream OpenTelemetry Instrumentation for Java. | ||
This instrumentation: | ||
|
||
1. Leverages OpenTelemetry's InstrumentationModule extension mechanism | ||
2. Ensures AWS-specific instrumentation runs after core OpenTelemetry instrumentation | ||
3. Maintains all existing functionality while improving ADOT maintainability | ||
|
||
Benefits: | ||
|
||
- **Better Extensibility:** Users can easily extend or modify AWS-specific behavior | ||
- **Enhanced Compatibility:** Better alignment with OpenTelemetry's extension mechanisms | ||
- **Clearer Code Organization:** More intuitive structure for future contributions | ||
|
||
### AWS SDK v2 Instrumentation Summary | ||
The AdotAwsSdkInstrumentationModule extends the InstrumentationModule from OpenTelemetry's SPI extensions and registers | ||
the AdotTracingExecutionInterceptor to provide custom instrumentation which runs after the upstream OTel agent runs. | ||
- **registerHelperResources** (within AdotAwsSdkInstrumentationModule) registers our resource (AdotTracingExecutionInterceptor) to inject into the user's class loader. The order the names are registered impactes the oder of instrumentation. It is key to first register the upstream aws-sdk execution interceptor, and then this interceptor. | ||
|
||
The AdotTracingExecutionInterceptor extends the AWS SDK ExecutionInterceptor. It uses the **beforeTransmission** and **modifyResponse** methods | ||
to hook onto the OTel spans during specific phases of the SDK request and response life cycle. This allows it to enrich the | ||
upstream spans with custom **AwsExperimentalAttributes**. These points in the life cycle are crucial as they impact the order in which attributes | ||
are added to the OTel span. | ||
|
||
- **beforeTransmission** is the latest point where the sdk request can be obtained after it is modified by the upstream aws-sdk v2.2 interceptor. This ensures the upstream handles the request and applies its changes first. | ||
- **modifyResponse** is the latest point to access the sdk response before the span closes in the upstream afterExecution method. This ensures we capture attributes from the final, fully modified response after all upstream interceptors have processed it. | ||
|
||
The AdotTracingExecutionInterceptor is the main point which connects the other classes within awssdk_v2_2 | ||
through the fieldMapper variable. | ||
|
||
_**Important Note:**_ | ||
Since the upstream interceptor closes the span in afterExecution, that method and any subsequent points are inaccessible for span modification. | ||
We use modifyResponse as our final interception point, as it provides access to both the complete SDK response (after upstream applies it's modifications first) and the still-active span. | ||
This ensures our attributes are added before span completion while still capturing the final state of the response after OTel. | ||
|
||
_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 **AdotTracingExecutionInterceptor** (order > 0) | ||
|
||
<ins>Note:</ins> The files that enhance OTel functionality are: | ||
* AwsExperimentalAttributes | ||
* AwsSdkRequest | ||
* AwsSdkRequestType | ||
* BedrockJsonParser | ||
* FieldMapper | ||
* Serializer | ||
* AdotTracingExecutionInterceptor | ||
* BedrockJsonParserTest | ||
* AbstractAws2ClientTest | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* 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("groovy") | ||
id("com.gradleup.shadow") | ||
} | ||
|
||
base.archivesBaseName = "aws-instrumentation-awssdk" | ||
|
||
dependencies { | ||
|
||
compileOnly("software.amazon.awssdk:json-utils:2.17.0") | ||
compileOnly("com.google.code.findbugs:jsr305:3.0.2") | ||
|
||
// OpenTelemetry dependencies | ||
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") | ||
compileOnly("io.opentelemetry.instrumentation:opentelemetry-instrumentation-api") | ||
|
||
// AWS SDK dependencies for version 2.2.0 | ||
compileOnly("software.amazon.awssdk:aws-core:2.2.0") | ||
compileOnly("software.amazon.awssdk:lambda:2.2.0") | ||
compileOnly("software.amazon.awssdk:aws-json-protocol:2.2.0") | ||
compileOnly("software.amazon.awssdk:sfn:2.2.0") | ||
compileOnly("software.amazon.awssdk:secretsmanager:2.2.0") | ||
|
||
// Test dependencies | ||
testImplementation(project(":instrumentation:aws-sdk:testing")) | ||
testImplementation("io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api") | ||
|
||
// AWS SDK test dependencies for version 2.2.0 | ||
testImplementation("software.amazon.awssdk:dynamodb:2.2.0") | ||
testImplementation("software.amazon.awssdk:ec2:2.2.0") | ||
testImplementation("software.amazon.awssdk:kinesis:2.2.0") | ||
testImplementation("software.amazon.awssdk:rds:2.2.0") | ||
testImplementation("software.amazon.awssdk:s3:2.2.0") | ||
testImplementation("software.amazon.awssdk:ses:2.2.0") | ||
testImplementation("software.amazon.awssdk:sfn:2.2.0") | ||
testImplementation("software.amazon.awssdk:secretsmanager:2.2.0") | ||
testImplementation("software.amazon.awssdk:lambda:2.2.0") | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please review the dependencies to remove the non-effective ones. Thanks. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
|
||
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 io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; | ||
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector; | ||
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode; | ||
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 | ||
implements ExperimentalInstrumentationModule { | ||
|
||
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<String> getAdditionalHelperClassNames() { | ||
return Arrays.asList( | ||
"software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotTracingExecutionInterceptor", | ||
// other helper classes as needed | ||
"software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotTracingExecutionInterceptor$RequestSpanFinisher", | ||
"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"); | ||
} | ||
|
||
// This registers the upstream execution interceptor first, and then the | ||
// AdotTracingExecutionInterceptor. This ensures the upstream interceptor runs before ADOT's | ||
// interceptor and maintains order. | ||
@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<ClassLoader> classLoaderMatcher() { | ||
return hasClassesNamed("software.amazon.awssdk.core.interceptor.ExecutionInterceptor"); | ||
} | ||
|
||
@Override | ||
public void injectClasses(final ClassInjector injector) { | ||
injector | ||
.proxyBuilder( | ||
"software.amazon.opentelemetry.javaagent.instrumentation.awssdk_v2_2.AdotTracingExecutionInterceptor") | ||
.inject(InjectionMode.CLASS_ONLY); | ||
} | ||
|
||
@Override | ||
public List<TypeInstrumentation> typeInstrumentations() { | ||
return Collections.singletonList(new ResourceInjectingTypeInstrumentation()); | ||
} | ||
|
||
public static class ResourceInjectingTypeInstrumentation implements TypeInstrumentation { | ||
@Override | ||
public ElementMatcher<TypeDescription> typeMatcher() { | ||
return named("software.amazon.awssdk.core.SdkClient"); | ||
} | ||
|
||
@Override | ||
public void transform(TypeTransformer transformer) { | ||
// Empty as we use ExecutionInterceptor | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
/* | ||
* 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 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.core.SdkRequest; | ||
import software.amazon.awssdk.core.SdkResponse; | ||
import software.amazon.awssdk.core.interceptor.*; | ||
|
||
public class AdotTracingExecutionInterceptor implements ExecutionInterceptor { | ||
|
||
private static final String GEN_AI_SYSTEM_BEDROCK = "aws.bedrock"; | ||
|
||
private static final ExecutionAttribute<AwsSdkRequest> AWS_SDK_REQUEST_ATTRIBUTE = | ||
new ExecutionAttribute<>(AdotTracingExecutionInterceptor.class.getName() + ".AwsSdkRequest"); | ||
|
||
private final FieldMapper fieldMapper = new FieldMapper(); | ||
|
||
// This is the latest point we can obtain the Sdk Request after it is modified by the upstream | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel this may not be the right understanding and we can double check with Ping. My understanding: The upstream instrumentation precedes our patching, as defined by the sequence in |
||
// TracingInterceptor. It ensures upstream handles the request and applies its changes first. | ||
@Override | ||
public void beforeTransmission( | ||
Context.BeforeTransmission context, ExecutionAttributes executionAttributes) { | ||
|
||
SdkRequest request = context.request(); | ||
Span currentSpan = Span.current(); | ||
|
||
try { | ||
// Skip injection if Otel span is invalid | ||
if (currentSpan != null && currentSpan.getSpanContext().isValid()) { | ||
AwsSdkRequest awsSdkRequest = AwsSdkRequest.ofSdkRequest(request); | ||
if (awsSdkRequest != null) { | ||
executionAttributes.putAttribute(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 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move this action to Update: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I had tried to instrument afterExecution initially but the upstream clears all the execution attributes. I think using afterUnmarshalling ensures that the interceptor that put in the execution attributes last (i.e. AdotTracingExecutionInterceptor) should be the first to handle the execution attributes. This also ensures the otel span is available to modify at this point of interception. |
||
public SdkResponse modifyResponse( | ||
Context.ModifyResponse context, ExecutionAttributes executionAttributes) { | ||
Span currentSpan = Span.current(); | ||
AwsSdkRequest sdkRequest = executionAttributes.getAttribute(AWS_SDK_REQUEST_ATTRIBUTE); | ||
|
||
if (sdkRequest != null) { | ||
fieldMapper.mapToAttributes(context.response(), sdkRequest, currentSpan); | ||
executionAttributes.putAttribute(AWS_SDK_REQUEST_ATTRIBUTE, null); | ||
} | ||
return context.response(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, lines 26 and 27 are needed for AwsJsonProtocolFactoryAccess and BedrockJsonParser