diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java index 21d9d7093bdf..875947661e8f 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java @@ -49,12 +49,14 @@ class GoldenSignalsMetricsTracer implements ApiTracer { private final Stopwatch clientRequestTimer; private final GoldenSignalsMetricsRecorder metricsRecorder; private final Map attributes; + private final ApiTracerContext.Transport transport; GoldenSignalsMetricsTracer( GoldenSignalsMetricsRecorder metricsRecorder, ApiTracerContext apiTracerContext) { this.clientRequestTimer = Stopwatch.createStarted(); this.metricsRecorder = metricsRecorder; this.attributes = apiTracerContext.getMetricsAttributes(); + this.transport = apiTracerContext.transport(); } @VisibleForTesting @@ -65,6 +67,7 @@ class GoldenSignalsMetricsTracer implements ApiTracer { this.clientRequestTimer = Stopwatch.createStarted(ticker); this.metricsRecorder = metricsRecorder; this.attributes = new HashMap<>(apiTracerContext.getMetricsAttributes()); + this.transport = apiTracerContext.transport(); } /** @@ -88,7 +91,7 @@ public void operationCancelled() { @Override public void operationFailed(Throwable error) { - attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + ObservabilityUtils.populateStatusAttributes(attributes, error, transport); metricsRecorder.recordOperationLatency( clientRequestTimer.elapsed(TimeUnit.NANOSECONDS) / 1_000_000_000.0, attributes); } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java index ec1a39c33337..d270259087ed 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java @@ -80,7 +80,7 @@ void recordActionableError(Throwable error) { logContext.put( ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, - ObservabilityUtils.extractStatus(error)); + ObservabilityUtils.extractStatus(error).toString()); ErrorInfo errorInfo = ObservabilityUtils.extractErrorInfo(error); if (errorInfo != null) { diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java index e9ad908c2194..13aac8986274 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java @@ -120,7 +120,8 @@ public void operationFailed(Throwable error) { if (operationFinished.getAndSet(true)) { throw new IllegalStateException(OPERATION_FINISHED_STATUS_MESSAGE); } - attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + // Uses the GRPC status code representation. + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error).toString()); metricsRecorder.recordOperationLatency( operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordOperationCount(1, attributes); @@ -172,7 +173,7 @@ public void attemptCancelled() { */ @Override public void attemptFailedDuration(Throwable error, java.time.Duration delay) { - attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error).toString()); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } @@ -196,7 +197,7 @@ public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) { */ @Override public void attemptFailedRetriesExhausted(Throwable error) { - attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error).toString()); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } @@ -210,7 +211,7 @@ public void attemptFailedRetriesExhausted(Throwable error) { */ @Override public void attemptPermanentFailure(Throwable error) { - attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error).toString()); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java index 02453b4f1210..097c4cf3d139 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java @@ -88,6 +88,9 @@ public class ObservabilityAttributes { /** Size of the response body in bytes. */ public static final String HTTP_RESPONSE_BODY_SIZE = "http.response.body.size"; + /** The HTTP status code of the request (e.g., 200, 404). */ + public static final String HTTP_RESPONSE_STATUS_ATTRIBUTE = "http.response.status_code"; + /** The resend count of the request. Only used in HTTP transport. */ public static final String HTTP_RESEND_COUNT_ATTRIBUTE = "http.request.resend_count"; diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index 275d13c041d3..a50643f3c780 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -43,7 +43,18 @@ final class ObservabilityUtils { - private ObservabilityUtils() {} + /** Function to extract the status of the error as a canonical code. */ + static StatusCode.Code extractStatus(@Nullable Throwable error) { + if (error == null) { + return StatusCode.Code.OK; + } else if (error instanceof CancellationException) { + return StatusCode.Code.CANCELLED; + } else if (error instanceof ApiException) { + return ((ApiException) error).getStatusCode().getCode(); + } else { + return StatusCode.Code.UNKNOWN; + } + } /** Constant for redacted values. */ private static final String REDACTED_VALUE = "REDACTED"; @@ -150,26 +161,17 @@ private static String redactSensitiveQueryValues(final String rawQuery) { return Joiner.on('&').join(redactedParams); } - /** - * Function to extract the status of the error as a string. - * - * @param error the thrown throwable error - * @return the extracted status string - */ - static String extractStatus(@Nullable final Throwable error) { - final String statusString; - - if (error == null) { - return StatusCode.Code.OK.toString(); - } else if (error instanceof CancellationException) { - statusString = StatusCode.Code.CANCELLED.toString(); - } else if (error instanceof ApiException) { - statusString = ((ApiException) error).getStatusCode().getCode().toString(); + static void populateStatusAttributes( + Map attributes, + @Nullable Throwable error, + ApiTracerContext.Transport transport) { + StatusCode.Code code = extractStatus(error); + if (transport == ApiTracerContext.Transport.HTTP) { + attributes.put( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, (long) code.getHttpStatusCode()); } else { - statusString = StatusCode.Code.UNKNOWN.toString(); + attributes.put(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, code.toString()); } - - return statusString; } /** Function to extract the ErrorInfo payload from the error, if available */ diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index eae14b1d9dd1..8998b3692e15 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -38,6 +38,7 @@ import io.opentelemetry.api.trace.Tracer; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CancellationException; /** An implementation of {@link ApiTracer} that uses OpenTelemetry to record traces. */ @BetaApi @@ -133,7 +134,7 @@ public void attemptStarted(Object request, int attemptNumber) { @Override public void attemptSucceeded() { - endAttempt(); + endAttempt(null); } @Override @@ -180,26 +181,34 @@ private long extractContentLength(java.util.Map headers) { @Override public void attemptCancelled() { - endAttempt(); + endAttempt(new CancellationException()); } @Override public void attemptFailedDuration(Throwable error, java.time.Duration delay) { - endAttempt(); + endAttempt(error); } @Override public void attemptFailedRetriesExhausted(Throwable error) { - endAttempt(); + endAttempt(error); } @Override public void attemptPermanentFailure(Throwable error) { - endAttempt(); + endAttempt(error); } - private void endAttempt() { + private void endAttempt(Throwable error) { if (attemptSpan != null) { + Map endAttributes = new HashMap<>(); + ObservabilityUtils.populateStatusAttributes( + endAttributes, error, this.apiTracerContext.transport()); + + if (!endAttributes.isEmpty()) { + attemptSpan.setAllAttributes(ObservabilityUtils.toOtelAttributes(endAttributes)); + } + attemptSpan.end(); attemptSpan = null; } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java index cd3acee41a8d..bebbc89dd16c 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java @@ -47,23 +47,23 @@ void testExtractStatus_errorConversion_apiExceptions() { ApiException error = new ApiException( "fake_error", null, new FakeStatusCode(StatusCode.Code.INVALID_ARGUMENT), false); - String errorCode = ObservabilityUtils.extractStatus(error); - assertThat(errorCode).isEqualTo(StatusCode.Code.INVALID_ARGUMENT.toString()); + StatusCode.Code errorCode = ObservabilityUtils.extractStatus(error); + assertThat(errorCode).isEqualTo(StatusCode.Code.INVALID_ARGUMENT); } @Test void testExtractStatus_errorConversion_noError() { // test "OK", which corresponds to a "null" error. - String successCode = ObservabilityUtils.extractStatus(null); - assertThat(successCode).isEqualTo(StatusCode.Code.OK.toString()); + StatusCode.Code successCode = ObservabilityUtils.extractStatus(null); + assertThat(successCode).isEqualTo(StatusCode.Code.OK); } @Test void testExtractStatus_errorConversion_unknownException() { // test "UNKNOWN" Throwable unknownException = new RuntimeException(); - String errorCode2 = ObservabilityUtils.extractStatus(unknownException); - assertThat(errorCode2).isEqualTo(StatusCode.Code.UNKNOWN.toString()); + StatusCode.Code errorCode2 = ObservabilityUtils.extractStatus(unknownException); + assertThat(errorCode2).isEqualTo(StatusCode.Code.UNKNOWN); } @Test @@ -114,6 +114,106 @@ void testToOtelAttributes_shouldMapIntAttributes() { .isEqualTo((long) attribute2Value); } + @Test + void testPopulateStatusAttributes_grpc_success() { + Map attributes = new java.util.HashMap<>(); + ObservabilityUtils.populateStatusAttributes(attributes, null, ApiTracerContext.Transport.GRPC); + assertThat(attributes) + .containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "OK"); + } + + @Test + void testPopulateStatusAttributes_grpc_apiException() { + Map attributes = new java.util.HashMap<>(); + ApiException error = + new ApiException("fake_error", null, new FakeStatusCode(StatusCode.Code.NOT_FOUND), false); + ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.GRPC); + assertThat(attributes) + .containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "NOT_FOUND"); + } + + @Test + void testPopulateStatusAttributes_grpc_cancellationException() { + Map attributes = new java.util.HashMap<>(); + Throwable error = new java.util.concurrent.CancellationException(); + ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.GRPC); + assertThat(attributes) + .containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "CANCELLED"); + } + + @Test + void testPopulateStatusAttributes_http_success() { + Map attributes = new java.util.HashMap<>(); + ObservabilityUtils.populateStatusAttributes(attributes, null, ApiTracerContext.Transport.HTTP); + assertThat(attributes) + .containsEntry( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, + (long) StatusCode.Code.OK.getHttpStatusCode()); + } + + @Test + void testPopulateStatusAttributes_http_apiExceptionWithIntegerTransportCode() { + Map attributes = new java.util.HashMap<>(); + ApiException error = + new ApiException( + "fake_error", + null, + new com.google.api.gax.rpc.StatusCode() { + @Override + public Code getCode() { + return Code.NOT_FOUND; + } + + @Override + public Object getTransportCode() { + return StatusCode.Code.NOT_FOUND.getHttpStatusCode(); + } + }, + false); + ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP); + assertThat(attributes) + .containsEntry( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, + (long) StatusCode.Code.NOT_FOUND.getHttpStatusCode()); + } + + @Test + void testPopulateStatusAttributes_http_apiExceptionWithNonIntegerTransportCode() { + Map attributes = new java.util.HashMap<>(); + ApiException error = + new ApiException( + "fake_error", + null, + new com.google.api.gax.rpc.StatusCode() { + @Override + public Code getCode() { + return Code.NOT_FOUND; + } + + @Override + public Object getTransportCode() { + return "Not Found"; + } + }, + false); + ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP); + assertThat(attributes) + .containsEntry( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, + (long) StatusCode.Code.NOT_FOUND.getHttpStatusCode()); + } + + @Test + void testPopulateStatusAttributes_http_cancellationException() { + Map attributes = new java.util.HashMap<>(); + Throwable error = new java.util.concurrent.CancellationException(); + ObservabilityUtils.populateStatusAttributes(attributes, error, ApiTracerContext.Transport.HTTP); + assertThat(attributes) + .containsEntry( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, + (long) StatusCode.Code.CANCELLED.getHttpStatusCode()); + } + @Test void testToOtelAttributes_shouldReturnEmptyAttributes_nullInput() { assertThat(ObservabilityUtils.toOtelAttributes(null)).isEqualTo(Attributes.empty()); diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index f4e44b10685c..30f07e23a6c3 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -89,6 +89,52 @@ void testAttemptStarted_includesLanguageAttribute() { SpanTracer.DEFAULT_LANGUAGE); } + @Test + void testAttemptSucceeded_grpc() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.GRPC) + .build(); + spanTracer = new SpanTracer(tracer, context, ATTEMPT_SPAN_NAME); + + spanTracer.attemptStarted(new Object(), 1); + spanTracer.attemptSucceeded(); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + verify(span).end(); + + assertThat(attrsCaptor.getValue().asMap()) + .containsEntry( + io.opentelemetry.api.common.AttributeKey.stringKey( + ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE), + "OK"); + } + + @Test + void testAttemptSucceeded_http() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.HTTP) + .build(); + spanTracer = new SpanTracer(tracer, context, ATTEMPT_SPAN_NAME); + + spanTracer.attemptStarted(new Object(), 1); + spanTracer.attemptSucceeded(); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + verify(span).end(); + + assertThat(attrsCaptor.getValue().asMap()) + .containsEntry( + io.opentelemetry.api.common.AttributeKey.longKey( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE), + 200L); + } + @Test void testResponseHeadersReceived_setsContentLengthAttribute() { spanTracer.attemptStarted(new Object(), 1); @@ -172,6 +218,46 @@ void testAttemptStarted_noRetryAttributes_grpc() { ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE)); } + @Test + void testAttemptFailed_grpc() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.GRPC) + .build(); + spanTracer = new SpanTracer(tracer, context, ATTEMPT_SPAN_NAME); + + com.google.api.gax.rpc.ApiException exception = + new com.google.api.gax.rpc.ApiException( + "error", + null, + new com.google.api.gax.rpc.StatusCode() { + @Override + public Code getCode() { + return Code.NOT_FOUND; + } + + @Override + public Object getTransportCode() { + return null; + } + }, + false); + + spanTracer.attemptStarted(new Object(), 1); + spanTracer.attemptFailedRetriesExhausted(exception); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + verify(span).end(); + + assertThat(attrsCaptor.getValue().asMap()) + .containsEntry( + io.opentelemetry.api.common.AttributeKey.stringKey( + ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE), + "NOT_FOUND"); + } + @Test void testAttemptStarted_retryAttributes_grpc() { ApiTracerContext grpcContext = @@ -198,6 +284,46 @@ void testAttemptStarted_retryAttributes_grpc() { ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE)); } + @Test + void testAttemptFailed_http() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.HTTP) + .build(); + spanTracer = new SpanTracer(tracer, context, ATTEMPT_SPAN_NAME); + + com.google.api.gax.rpc.ApiException exception = + new com.google.api.gax.rpc.ApiException( + "error", + null, + new com.google.api.gax.rpc.StatusCode() { + @Override + public Code getCode() { + return Code.NOT_FOUND; + } + + @Override + public Object getTransportCode() { + return 404; + } + }, + false); + + spanTracer.attemptStarted(new Object(), 1); + spanTracer.attemptFailedRetriesExhausted(exception); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + verify(span).end(); + + assertThat(attrsCaptor.getValue().asMap()) + .containsEntry( + io.opentelemetry.api.common.AttributeKey.longKey( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE), + 404L); + } + @Test void testAttemptStarted_noRetryAttributes_http() { ApiTracerContext httpContext = diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index ab2af9111f47..53127857c9fb 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -48,6 +48,7 @@ import com.google.showcase.v1beta1.EchoRequest; import com.google.showcase.v1beta1.EchoResponse; import com.google.showcase.v1beta1.EchoSettings; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; import com.google.showcase.v1beta1.stub.EchoStub; import com.google.showcase.v1beta1.stub.EchoStubSettings; import io.grpc.ManagedChannelBuilder; @@ -404,6 +405,86 @@ void testTracing_retry_httpjson() throws Exception { assertThat(resendCounts).containsExactlyElementsIn(expectedCounts).inOrder(); } + @Test + void testTracing_statusCodes_grpc() throws Exception { + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); + EchoRequest errorRequest = + EchoRequest.newBuilder() + .setError( + Status.newBuilder().setCode(StatusCode.Code.INVALID_ARGUMENT.ordinal()).build()) + .build(); + EchoRequest successRequest = EchoRequest.newBuilder().setContent("tracing-test").build(); + + try (EchoClient grpcClient = + TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) { + + grpcClient.echo(successRequest); + assertThrows( + com.google.api.gax.rpc.InvalidArgumentException.class, + () -> grpcClient.echo(errorRequest)); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).hasSize(2); + + SpanData grpcSuccessSpan = spans.get(0); + assertThat( + grpcSuccessSpan + .getAttributes() + .get( + AttributeKey.stringKey( + ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo("OK"); + + SpanData grpcErrorSpan = spans.get(1); + assertThat( + grpcErrorSpan + .getAttributes() + .get( + AttributeKey.stringKey( + ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo("INVALID_ARGUMENT"); + } + } + + @Test + void testTracing_statusCodes_httpjson() throws Exception { + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); + EchoRequest errorRequest = + EchoRequest.newBuilder() + .setError( + Status.newBuilder().setCode(StatusCode.Code.INVALID_ARGUMENT.ordinal()).build()) + .build(); + EchoRequest successRequest = EchoRequest.newBuilder().setContent("tracing-test").build(); + + try (EchoClient httpClient = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) { + + httpClient.echo(successRequest); + assertThrows( + com.google.api.gax.rpc.InvalidArgumentException.class, + () -> httpClient.echo(errorRequest)); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).hasSize(2); + + SpanData httpSuccessSpan = spans.get(0); + assertThat( + httpSuccessSpan + .getAttributes() + .get( + AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo(200L); + + SpanData httpErrorSpan = spans.get(1); + assertThat( + httpErrorSpan + .getAttributes() + .get( + AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo((long) StatusCode.Code.INVALID_ARGUMENT.getHttpStatusCode()); + } + } + private EchoSettings createEchoSettings(boolean isHttpJson) throws Exception { if (isHttpJson) { return EchoSettings.newHttpJsonBuilder()