-
Notifications
You must be signed in to change notification settings - Fork 828
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
Enhance AWS SDK Instrumentation with Detailed HTTP Error Information #9448
Changes from all commits
34a7171
91a5f2c
b8dc2b8
1a68d51
3a347a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2ClientRecordHttpErrorTest; | ||
import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; | ||
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; | ||
import org.junit.jupiter.api.extension.RegisterExtension; | ||
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; | ||
|
||
public class Aws2ClientRecordHttpErrorTest extends AbstractAws2ClientRecordHttpErrorTest { | ||
@RegisterExtension | ||
private final AgentInstrumentationExtension testing = AgentInstrumentationExtension.create(); | ||
|
||
@Override | ||
public ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { | ||
return ClientOverrideConfiguration.builder(); | ||
} | ||
|
||
@Override | ||
protected InstrumentationExtension getTesting() { | ||
return testing; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
|
||
import static io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkRequestType.DYNAMODB; | ||
|
||
import io.opentelemetry.api.common.AttributeKey; | ||
import io.opentelemetry.api.common.Attributes; | ||
import io.opentelemetry.api.common.AttributesBuilder; | ||
import io.opentelemetry.api.trace.Span; | ||
|
@@ -15,7 +16,14 @@ | |
import io.opentelemetry.contrib.awsxray.propagator.AwsXrayPropagator; | ||
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; | ||
import io.opentelemetry.semconv.SemanticAttributes; | ||
import java.io.BufferedReader; | ||
import java.io.ByteArrayInputStream; | ||
import java.io.InputStream; | ||
import java.io.InputStreamReader; | ||
import java.nio.charset.Charset; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.stream.Collectors; | ||
import javax.annotation.Nullable; | ||
import software.amazon.awssdk.auth.signer.AwsSignerExecutionAttribute; | ||
import software.amazon.awssdk.awscore.AwsResponse; | ||
|
@@ -50,6 +58,10 @@ final class TracingExecutionInterceptor implements ExecutionInterceptor { | |
private final Instrumenter<ExecutionAttributes, SdkHttpResponse> consumerInstrumenter; | ||
private final boolean captureExperimentalSpanAttributes; | ||
|
||
static final AttributeKey<String> HTTP_ERROR_MSG = | ||
AttributeKey.stringKey("aws.http.error_message"); | ||
static final String HTTP_FAILURE_EVENT = "HTTP request failure"; | ||
|
||
Instrumenter<ExecutionAttributes, SdkHttpResponse> getConsumerInstrumenter() { | ||
return consumerInstrumenter; | ||
} | ||
|
@@ -65,19 +77,22 @@ boolean shouldUseXrayPropagator() { | |
|
||
@Nullable private final TextMapPropagator messagingPropagator; | ||
private final boolean useXrayPropagator; | ||
private final boolean recordIndividualHttpError; | ||
private final FieldMapper fieldMapper; | ||
|
||
TracingExecutionInterceptor( | ||
Instrumenter<ExecutionAttributes, SdkHttpResponse> requestInstrumenter, | ||
Instrumenter<ExecutionAttributes, SdkHttpResponse> consumerInstrumenter, | ||
boolean captureExperimentalSpanAttributes, | ||
TextMapPropagator messagingPropagator, | ||
boolean useXrayPropagator) { | ||
boolean useXrayPropagator, | ||
boolean recordIndividualHttpError) { | ||
this.requestInstrumenter = requestInstrumenter; | ||
this.consumerInstrumenter = consumerInstrumenter; | ||
this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; | ||
this.messagingPropagator = messagingPropagator; | ||
this.useXrayPropagator = useXrayPropagator; | ||
this.recordIndividualHttpError = recordIndividualHttpError; | ||
this.fieldMapper = new FieldMapper(); | ||
} | ||
|
||
|
@@ -222,6 +237,19 @@ public SdkHttpRequest modifyHttpRequest( | |
return builder.build(); | ||
} | ||
|
||
@Override | ||
public Optional<InputStream> modifyHttpResponseContent( | ||
Context.ModifyHttpResponse context, ExecutionAttributes executionAttributes) { | ||
Optional<InputStream> responseBody = context.responseBody(); | ||
if (recordIndividualHttpError) { | ||
String errorMsg = extractHttpErrorAsEvent(context, executionAttributes); | ||
if (errorMsg != null) { | ||
return Optional.of(new ByteArrayInputStream(errorMsg.getBytes(Charset.defaultCharset()))); | ||
} | ||
} | ||
return responseBody; | ||
} | ||
|
||
private void populateRequestAttributes( | ||
Span span, | ||
AwsSdkRequest awsSdkRequest, | ||
|
@@ -289,6 +317,37 @@ private void onSdkResponse( | |
} | ||
} | ||
|
||
private static String extractHttpErrorAsEvent( | ||
Context.AfterTransmission context, ExecutionAttributes executionAttributes) { | ||
io.opentelemetry.context.Context otelContext = getContext(executionAttributes); | ||
if (otelContext != null) { | ||
Span span = Span.fromContext(otelContext); | ||
SdkHttpResponse response = context.httpResponse(); | ||
|
||
if (response != null && !response.isSuccessful()) { | ||
int errorCode = response.statusCode(); | ||
// we want to record the error message from http response | ||
Optional<InputStream> responseBody = context.responseBody(); | ||
if (responseBody.isPresent()) { | ||
String errorMsg = | ||
new BufferedReader( | ||
new InputStreamReader(responseBody.get(), Charset.defaultCharset())) | ||
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. Is the underlying 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. Thank you for catching this! I think there is no guarantee that the InputStream will support To verify, I did an simple experiment. Based on the pipeline stage in aws java sdk: https://github.com/aws/aws-sdk-java-v2/blob/d020d37138eee9d4d74e814086143d26d923fee0/core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/AfterTransmissionExecutionInterceptorsStage.java#L43-L46, it seems like It's a valid concern that the inputStream for the response body will become useless after we read it out in I will make the change in next commit. 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. Thanks @pxaws Is there a way to verify in the test that the 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 modified the test to do that. I basically defined another execution interceptor which is registered with AWS SDK after the TracingExecutionInterceptor used for SDK instrumentation and then verify from there to make sure the response body contains the expected content. Please see the next commit. Thank you! |
||
.lines() | ||
.collect(Collectors.joining("\n")); | ||
Attributes attributes = | ||
Attributes.of( | ||
SemanticAttributes.HTTP_RESPONSE_STATUS_CODE, | ||
Long.valueOf(errorCode), | ||
HTTP_ERROR_MSG, | ||
errorMsg); | ||
span.addEvent(HTTP_FAILURE_EVENT, attributes); | ||
return errorMsg; | ||
} | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
@Override | ||
public void onExecutionFailure( | ||
Context.FailedExecution context, ExecutionAttributes executionAttributes) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.awssdk.v2_2; | ||
|
||
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; | ||
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; | ||
import org.junit.jupiter.api.extension.RegisterExtension; | ||
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; | ||
|
||
public class Aws2ClientNotRecordHttpErrorTest extends AbstractAws2ClientRecordHttpErrorTest { | ||
@RegisterExtension | ||
public final LibraryInstrumentationExtension testing = LibraryInstrumentationExtension.create(); | ||
|
||
@Override | ||
public ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { | ||
return ClientOverrideConfiguration.builder() | ||
.addExecutionInterceptor( | ||
AwsSdkTelemetry.builder(testing.getOpenTelemetry()) | ||
.setCaptureExperimentalSpanAttributes(true) | ||
.setRecordIndividualHttpError(isRecordIndividualHttpErrorEnabled()) | ||
.build() | ||
.newExecutionInterceptor()); | ||
} | ||
|
||
@Override | ||
public boolean isRecordIndividualHttpErrorEnabled() { | ||
return false; | ||
} | ||
|
||
@Override | ||
protected InstrumentationExtension getTesting() { | ||
return testing; | ||
} | ||
} |
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.
any plans to make this attribute part of the semantic conventions for the aws sdk?
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.
https://github.com/open-telemetry/semantic-conventions/blob/203691d99612452df0c951640b04521e34969628/docs/cloud-providers/aws-sdk.md?plain=1#L2
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, it is in the long term plan. Actually I am not quite sure which part of semantic conventions that I should put into because this is the event attribute not the span attribute (the previous link that you give seems to be the conventions for span attributes). Any suggestions?