diff --git a/aws-xray/src/main/java/io/opentelemetry/contrib/awsxray/AwsSpanMetricsProcessor.java b/aws-xray/src/main/java/io/opentelemetry/contrib/awsxray/AwsSpanMetricsProcessor.java index 4fec1686e..fb2c2a103 100644 --- a/aws-xray/src/main/java/io/opentelemetry/contrib/awsxray/AwsSpanMetricsProcessor.java +++ b/aws-xray/src/main/java/io/opentelemetry/contrib/awsxray/AwsSpanMetricsProcessor.java @@ -15,7 +15,11 @@ import io.opentelemetry.sdk.trace.ReadWriteSpan; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SpanProcessor; +import io.opentelemetry.sdk.trace.data.EventData; import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData; +import java.lang.reflect.Method; +import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** @@ -92,7 +96,7 @@ public void onEnd(ReadableSpan span) { // Only record metrics if non-empty attributes are returned. if (!attributes.isEmpty()) { - recordErrorOrFault(span, attributes); + recordErrorOrFault(spanData, attributes); recordLatency(span, attributes); } } @@ -102,10 +106,14 @@ public boolean isEndRequired() { return true; } - private void recordErrorOrFault(ReadableSpan span, Attributes attributes) { - Long httpStatusCode = span.getAttribute(HTTP_STATUS_CODE); + private void recordErrorOrFault(SpanData spanData, Attributes attributes) { + Long httpStatusCode = spanData.getAttributes().get(HTTP_STATUS_CODE); if (httpStatusCode == null) { - return; + httpStatusCode = getAwsStatusCode(spanData); + + if (httpStatusCode == null || httpStatusCode < 100L || httpStatusCode > 599L) { + return; + } } if (httpStatusCode >= ERROR_CODE_LOWER_BOUND && httpStatusCode <= ERROR_CODE_UPPER_BOUND) { @@ -116,6 +124,52 @@ private void recordErrorOrFault(ReadableSpan span, Attributes attributes) { } } + /** + * Attempt to pull status code from spans produced by AWS SDK instrumentation (both v1 and v2). + * AWS SDK instrumentation does not populate http.status_code when non-200 status codes are + * returned, as the AWS SDK throws exceptions rather than returning responses with status codes. + * To work around this, we are attempting to get the exception out of the events, then calling + * getStatusCode (for AWS SDK V1) and statusCode (for AWS SDK V2) to get the status code fromt the + * exception. We rely on reflection here because we cannot cast the throwable to + * AmazonServiceExceptions (V1) or AwsServiceExceptions (V2) because the throwable comes from a + * separate class loader and attempts to cast will fail with ClassCastException. + * + *
TODO: Short term workaround. This can be completely removed once
+ * https://github.com/open-telemetry/opentelemetry-java-contrib/issues/919 is resolved.
+ */
+ @Nullable
+ private static Long getAwsStatusCode(SpanData spanData) {
+ String scopeName = spanData.getInstrumentationScopeInfo().getName();
+ if (!scopeName.contains("aws-sdk")) {
+ return null;
+ }
+
+ for (EventData event : spanData.getEvents()) {
+ if (event instanceof ExceptionEventData) {
+ ExceptionEventData exceptionEvent = (ExceptionEventData) event;
+ Throwable throwable = exceptionEvent.getException();
+
+ try {
+ Method method = throwable.getClass().getMethod("getStatusCode", new Class>[] {});
+ Object code = method.invoke(throwable, new Object[] {});
+ return Long.valueOf((Integer) code);
+ } catch (Exception e) {
+ // Take no action
+ }
+
+ try {
+ Method method = throwable.getClass().getMethod("statusCode", new Class>[] {});
+ Object code = method.invoke(throwable, new Object[] {});
+ return Long.valueOf((Integer) code);
+ } catch (Exception e) {
+ // Take no action
+ }
+ }
+ }
+
+ return null;
+ }
+
private void recordLatency(ReadableSpan span, Attributes attributes) {
long nanos = span.getLatencyNanos();
double millis = nanos / NANOS_TO_MILLIS;
diff --git a/aws-xray/src/test/java/io/opentelemetry/contrib/awsxray/AwsSpanMetricsProcessorTest.java b/aws-xray/src/test/java/io/opentelemetry/contrib/awsxray/AwsSpanMetricsProcessorTest.java
index 6f6fd2b97..c2109b267 100644
--- a/aws-xray/src/test/java/io/opentelemetry/contrib/awsxray/AwsSpanMetricsProcessorTest.java
+++ b/aws-xray/src/test/java/io/opentelemetry/contrib/awsxray/AwsSpanMetricsProcessorTest.java
@@ -22,16 +22,21 @@
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
+import io.opentelemetry.sdk.trace.data.EventData;
import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.internal.data.ExceptionEventData;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link AwsSpanMetricsProcessor}. */
class AwsSpanMetricsProcessorTest {
-
// Test constants
private static final boolean CONTAINS_ATTRIBUTES = true;
private static final boolean CONTAINS_NO_ATTRIBUTES = false;
@@ -56,6 +61,32 @@ private enum ExpectedStatusMetric {
private AwsSpanMetricsProcessor awsSpanMetricsProcessor;
+ static class ThrowableWithMethodGetStatusCode extends Throwable {
+ private final int httpStatusCode;
+
+ ThrowableWithMethodGetStatusCode(int httpStatusCode) {
+ this.httpStatusCode = httpStatusCode;
+ }
+
+ public int getStatusCode() {
+ return this.httpStatusCode;
+ }
+ }
+
+ static class ThrowableWithMethodStatusCode extends Throwable {
+ private final int httpStatusCode;
+
+ ThrowableWithMethodStatusCode(int httpStatusCode) {
+ this.httpStatusCode = httpStatusCode;
+ }
+
+ public int statusCode() {
+ return this.httpStatusCode;
+ }
+ }
+
+ static class ThrowableWithoutStatusCode extends Throwable {}
+
@BeforeEach
public void setUpMocks() {
errorCounterMock = mock(LongCounter.class);
@@ -149,6 +180,16 @@ public void testOnEndMetricsGenerationWithLatency() {
verify(latencyHistogramMock, times(1)).record(eq(5.5), eq(metricAttributes));
}
+ @Test
+ public void testOnEndMetricsGenerationWithAwsStatusCodes() {
+ validateMetricsGeneratedForAwsStatusCode(399L, ExpectedStatusMetric.NEITHER);
+ validateMetricsGeneratedForAwsStatusCode(400L, ExpectedStatusMetric.ERROR);
+ validateMetricsGeneratedForAwsStatusCode(499L, ExpectedStatusMetric.ERROR);
+ validateMetricsGeneratedForAwsStatusCode(500L, ExpectedStatusMetric.FAULT);
+ validateMetricsGeneratedForAwsStatusCode(599L, ExpectedStatusMetric.FAULT);
+ validateMetricsGeneratedForAwsStatusCode(600L, ExpectedStatusMetric.NEITHER);
+ }
+
@Test
public void testOnEndMetricsGenerationWithStatusCodes() {
// Invalid HTTP status codes
@@ -192,6 +233,9 @@ private static ReadableSpan buildReadableSpanMock(Attributes spanAttributes) {
// Configure spanData
SpanData mockSpanData = mock(SpanData.class);
+ InstrumentationScopeInfo awsSdkScopeInfo =
+ InstrumentationScopeInfo.builder("aws-sdk").setVersion("version").build();
+ when(mockSpanData.getInstrumentationScopeInfo()).thenReturn(awsSdkScopeInfo);
when(mockSpanData.getAttributes()).thenReturn(spanAttributes);
when(mockSpanData.getTotalAttributeCount()).thenReturn(spanAttributes.size());
when(readableSpanMock.toSpanData()).thenReturn(mockSpanData);
@@ -199,6 +243,34 @@ private static ReadableSpan buildReadableSpanMock(Attributes spanAttributes) {
return readableSpanMock;
}
+ private static ReadableSpan buildReadableSpanWithThrowableMock(Throwable throwable) {
+ // config http status code as null
+ Attributes spanAttributes = Attributes.of(HTTP_STATUS_CODE, null);
+ ReadableSpan readableSpanMock = mock(ReadableSpan.class);
+ SpanData mockSpanData = mock(SpanData.class);
+ InstrumentationScopeInfo awsSdkScopeInfo =
+ InstrumentationScopeInfo.builder("aws-sdk").setVersion("version").build();
+ ExceptionEventData mockEventData = mock(ExceptionEventData.class);
+ List