From ee42bf19a793489958ad3e251864b13944218af8 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Mon, 16 Mar 2026 16:35:14 -0400 Subject: [PATCH 01/10] impl(o11y): introduce error.type --- .../gax/tracing/ObservabilityAttributes.java | 6 + .../api/gax/tracing/ObservabilityUtils.java | 99 +++++++ .../tracing/OpenTelemetryTraceManager.java | 5 + .../google/api/gax/tracing/SpanTracer.java | 31 ++ .../google/api/gax/tracing/TraceManager.java | 2 + .../api/gax/tracing/SpanTracerTest.java | 264 ++++++++++++++++++ .../showcase/v1beta1/it/ITOtelTracing.java | 68 +++++ 7 files changed, 475 insertions(+) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java index b8b4dc2373..07321282ee 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java @@ -73,4 +73,10 @@ public class ObservabilityAttributes { /** The url template of the request (e.g. /v1/{name}:access). */ public static final String URL_TEMPLATE_ATTRIBUTE = "url.template"; + + /** + * The specific error type. Value will be google.rpc.ErrorInfo.reason, a specific Server Error + * Code, Client-Side Network/Operational Error (e.g., CLIENT_TIMEOUT) or internal fallback. + */ + public static final String ERROR_TYPE_ATTRIBUTE = "error.type"; } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index f2a787fc95..8d0547bfa8 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -39,6 +39,105 @@ class ObservabilityUtils { + enum ErrorType { + CLIENT_TIMEOUT, + CLIENT_CONNECTION_ERROR, + CLIENT_REQUEST_ERROR, + CLIENT_REQUEST_BODY_ERROR, + CLIENT_RESPONSE_DECODE_ERROR, + CLIENT_REDIRECT_ERROR, + CLIENT_AUTHENTICATION_ERROR, + CLIENT_UNKNOWN_ERROR, + INTERNAL; + + @Override + public String toString() { + return name(); + } + } + + /** + * Function to extract the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE } attribute from a + * Throwable + */ + static String extractErrorType(@Nullable Throwable error) { + if (error == null) { + return null; + } + + if (error instanceof ApiException) { + ApiException apiException = (ApiException) error; + + // 1. Check for ErrorInfo.reason + String reason = apiException.getReason(); + if (reason != null && !reason.isEmpty()) { + return reason; + } + + // 2. Specific Server Error Code + if (apiException.getStatusCode() != null) { + Object transportCode = apiException.getStatusCode().getTransportCode(); + if (transportCode instanceof Integer) { + // HTTP Status Code + return String.valueOf(transportCode); + } else if (apiException.getStatusCode().getCode() != null) { + // gRPC Status Code name + return apiException.getStatusCode().getCode().name(); + } + } + } + + // 3. Client-Side Network/Operational Errors + String exceptionName = error.getClass().getSimpleName(); + + if (error instanceof java.util.concurrent.TimeoutException + || error instanceof java.net.SocketTimeoutException + || exceptionName.equals("WatchdogTimeoutException")) { + return ErrorType.CLIENT_TIMEOUT.toString(); + } + + if (error instanceof java.net.ConnectException + || error instanceof java.net.UnknownHostException + || error instanceof java.nio.channels.UnresolvedAddressException + || exceptionName.equals("ConnectException")) { + return ErrorType.CLIENT_CONNECTION_ERROR.toString(); + } + + if (exceptionName.contains("CredentialsException") + || exceptionName.contains("AuthenticationException")) { + return ErrorType.CLIENT_AUTHENTICATION_ERROR.toString(); + } + + if (exceptionName.contains("ProtocolBufferParsingException") + || exceptionName.contains("DecodeException")) { + return ErrorType.CLIENT_RESPONSE_DECODE_ERROR.toString(); + } + + if (exceptionName.contains("RedirectException")) { + return ErrorType.CLIENT_REDIRECT_ERROR.toString(); + } + + if (exceptionName.contains("RequestBodyException")) { + return ErrorType.CLIENT_REQUEST_BODY_ERROR.toString(); + } + + if (exceptionName.contains("RequestException")) { + return ErrorType.CLIENT_REQUEST_ERROR.toString(); + } + + if (exceptionName.contains("UnknownClientException")) { + return ErrorType.CLIENT_UNKNOWN_ERROR.toString(); + } + + // 4. Language-specific error type fallback + if (exceptionName != null && !exceptionName.isEmpty()) { + return exceptionName; + } + + // 5. Internal Fallback + return ErrorType.INTERNAL.toString(); + } + /** Function to extract the status of the error as a string */ static String extractStatus(@Nullable Throwable error) { final String statusString; diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java index 833e56fda4..eb44fef2e2 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTraceManager.java @@ -71,6 +71,11 @@ private OtelSpan(io.opentelemetry.api.trace.Span span) { this.span = span; } + @Override + public void addAttribute(String key, String value) { + span.setAttribute(key, value); + } + @Override public void end() { span.end(); diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index c5c28aebe0..122e2a66f3 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -85,6 +85,37 @@ public void attemptSucceeded() { endAttempt(); } + @Override + public void attemptCancelled() { + endAttempt(); + } + + @Override + public void attemptFailedDuration(Throwable error, java.time.Duration delay) { + recordErrorAndEndAttempt(error); + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + recordErrorAndEndAttempt(error); + } + + @Override + public void attemptPermanentFailure(Throwable error) { + recordErrorAndEndAttempt(error); + } + + private void recordErrorAndEndAttempt(Throwable error) { + if (attemptHandle != null) { + if (error != null) { + attemptHandle.addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.extractErrorType(error)); + } + endAttempt(); + } + } + private void endAttempt() { if (attemptHandle != null) { attemptHandle.end(); diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java index 8572d1ce11..12169ce937 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TraceManager.java @@ -45,6 +45,8 @@ public interface TraceManager { Span createSpan(String name, Map attributes); interface Span { + void addAttribute(String key, String value); + void end(); } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index b5e6100fe6..9439327dbc 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -35,7 +35,15 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.ErrorDetails; +import com.google.api.gax.rpc.StatusCode; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Any; +import com.google.rpc.ErrorInfo; +import java.net.ConnectException; import java.util.Map; +import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -76,4 +84,260 @@ void testAttemptStarted_includesLanguageAttribute() { assertThat(attributesCaptor.getValue()) .containsEntry(SpanTracer.LANGUAGE_ATTRIBUTE, SpanTracer.DEFAULT_LANGUAGE); } + + @Test + void testAttemptFailed_errorInfoReason() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + ErrorInfo errorInfo = ErrorInfo.newBuilder().setReason("RATE_LIMIT_EXCEEDED").build(); + ErrorDetails errorDetails = + ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(Any.pack(errorInfo))).build(); + Throwable cause = new Throwable("message"); + + ApiException apiException = + new ApiException( + cause, + new StatusCode() { + @Override + public Code getCode() { + return Code.UNAVAILABLE; + } + + @Override + public Object getTransportCode() { + return null; + } + }, + true, + errorDetails); + + tracer.attemptFailedRetriesExhausted(apiException); + + verify(attemptHandle) + .addAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "RATE_LIMIT_EXCEEDED"); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_specificServerErrorCodeGrpc() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + ApiException apiException = + new ApiException( + "message", + null, + new StatusCode() { + @Override + public Code getCode() { + return Code.PERMISSION_DENIED; + } + + @Override + public Object getTransportCode() { + return "PERMISSION_DENIED"; + } + }, + true); + + tracer.attemptFailedRetriesExhausted(apiException); + + verify(attemptHandle) + .addAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "PERMISSION_DENIED"); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_specificServerErrorCodeHttp() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + ApiException apiException = + new ApiException( + "message", + null, + new StatusCode() { + @Override + public Code getCode() { + return Code.PERMISSION_DENIED; + } + + @Override + public Object getTransportCode() { + return 403; + } + }, + true); + + tracer.attemptFailedRetriesExhausted(apiException); + + verify(attemptHandle).addAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "403"); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_clientTimeout() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new TimeoutException("timed out")); + + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.CLIENT_TIMEOUT.toString()); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_clientConnectionError() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new ConnectException("connection failed")); + + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_clientAuthenticationError() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new CredentialsException()); + + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.CLIENT_AUTHENTICATION_ERROR.toString()); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_clientResponseDecodeError() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new DecodeException()); + + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.CLIENT_RESPONSE_DECODE_ERROR.toString()); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_clientRedirectError() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new RedirectException()); + + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.CLIENT_REDIRECT_ERROR.toString()); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_clientRequestBodyError() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new RequestBodyException()); + + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.CLIENT_REQUEST_BODY_ERROR.toString()); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_clientRequestError() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new RequestException()); + + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.CLIENT_REQUEST_ERROR.toString()); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_clientUnknownError() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new UnknownClientException()); + + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.CLIENT_UNKNOWN_ERROR.toString()); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_languageSpecificFallback() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new IllegalStateException("illegal state")); + + verify(attemptHandle) + .addAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "IllegalStateException"); + verify(attemptHandle).end(); + } + + @Test + void testAttemptFailed_internalFallback() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(new Throwable() {}); + + // For an anonymous inner class Throwable, getSimpleName() is empty string, which triggers the + // fallback + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ObservabilityUtils.ErrorType.INTERNAL.toString()); + verify(attemptHandle).end(); + } + + private static class CredentialsException extends RuntimeException {} + + private static class DecodeException extends RuntimeException {} + + private static class RedirectException extends RuntimeException {} + + private static class RequestBodyException extends RuntimeException {} + + private static class RequestException extends RuntimeException {} + + private static class UnknownClientException extends RuntimeException {} } diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java index 18594429a0..d2290b4fc4 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracing.java @@ -31,11 +31,15 @@ package com.google.showcase.v1beta1.it; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.api.gax.rpc.UnavailableException; import com.google.api.gax.tracing.ObservabilityAttributes; import com.google.api.gax.tracing.OpenTelemetryTraceManager; import com.google.api.gax.tracing.SpanTracer; import com.google.api.gax.tracing.SpanTracerFactory; +import com.google.rpc.Status; import com.google.showcase.v1beta1.EchoClient; import com.google.showcase.v1beta1.EchoRequest; import com.google.showcase.v1beta1.it.util.TestClientInitializer; @@ -185,4 +189,68 @@ void testTracing_successfulEcho_httpjson() throws Exception { .isEqualTo(SHOWCASE_ARTIFACT); } } + + @Test + void testTracing_failedEcho_grpc_recordsErrorType() throws Exception { + SpanTracerFactory tracingFactory = + new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) { + + EchoRequest echoRequest = + EchoRequest.newBuilder() + .setError(Status.newBuilder().setCode(Code.UNAVAILABLE.ordinal()).build()) + .build(); + + assertThrows(UnavailableException.class, () -> client.echo(echoRequest)); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("google.showcase.v1beta1.Echo/Echo")) + .findFirst() + .orElseThrow(() -> new AssertionError("Incorrect span name")); + + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE))) + .isEqualTo("UNAVAILABLE"); + } + } + + @Test + void testTracing_failedEcho_httpjson_recordsErrorType() throws Exception { + SpanTracerFactory tracingFactory = + new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + + try (EchoClient client = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) { + + EchoRequest echoRequest = + EchoRequest.newBuilder() + .setError(Status.newBuilder().setCode(Code.UNAVAILABLE.ordinal()).build()) + .build(); + + assertThrows(UnavailableException.class, () -> client.echo(echoRequest)); + + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData attemptSpan = + spans.stream() + .filter(span -> span.getName().equals("Echo/Echo/attempt")) + .findFirst() + .orElseThrow(() -> new AssertionError("Attempt span 'Echo/Echo/attempt' not found")); + + assertThat( + attemptSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE))) + .isEqualTo("503"); // For HTTP/JSON, the transport code 503 is used for UNAVAILABLE + } + } } From 4269ccdd7fea903c72c6009d324f581b29d12876 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Mon, 16 Mar 2026 16:51:27 -0400 Subject: [PATCH 02/10] docs: add javadoc for error type logic --- .../api/gax/tracing/ObservabilityUtils.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index 8d0547bfa8..5bef1fda05 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -57,8 +57,49 @@ public String toString() { } /** - * Function to extract the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE } attribute from a - * Throwable + * Extracts a low-cardinality string representing the specific classification of the error to be + * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. + * + *

This value is determined based on the following priority: + * + *

    + *
  1. {@code google.rpc.ErrorInfo.reason}: If the error response from the service + * includes {@code google.rpc.ErrorInfo} details, the reason field (e.g., + * "RATE_LIMIT_EXCEEDED", "SERVICE_DISABLED") will be used. This offers the most precise + * error cause. + *
  2. Specific Server Error Code: If no {@code ErrorInfo.reason} is available, but a + * server error code was received: + *
      + *
    • For HTTP: The HTTP status code (e.g., "403", "503"). + *
    • For gRPC: The gRPC status code name (e.g., "PERMISSION_DENIED", "UNAVAILABLE"). + *
    + *
  3. Client-Side Network/Operational Errors: For errors occurring within the client + * library or network stack, mapping to specific enum representations from {@link + * ErrorType}: + *
      + *
    • {@code CLIENT_TIMEOUT}: A client-configured timeout was reached. + *
    • {@code CLIENT_CONNECTION_ERROR}: Failure to establish the network connection (DNS, + * TCP, TLS). + *
    • {@code CLIENT_REQUEST_ERROR}: Client-side issue forming or sending the request. + *
    • {@code CLIENT_REQUEST_BODY_ERROR}: Error streaming the request body. + *
    • {@code CLIENT_RESPONSE_DECODE_ERROR}: Client-side error decoding the response body. + *
    • {@code CLIENT_REDIRECT_ERROR}: Problem handling HTTP redirects. + *
    • {@code CLIENT_AUTHENTICATION_ERROR}: Error during credential acquisition or + * application. + *
    • {@code CLIENT_UNKNOWN_ERROR}: Other unclassified client-side network or protocol + * errors. + *
    + *
  4. Language-specific error type: The class or struct name of the exception or error + * if available. This must be low-cardinality, meaning it returns the short name of the + * exception class (e.g. {@code "IllegalStateException"}) rather than its message. + *
  5. Internal Fallback: If the error doesn't fit any of the above categories, {@code + * "INTERNAL"} will be used, indicating an unexpected issue within the client library's own + * logic. + *
+ * + * @param error the Throwable from which to extract the error type string. + * @return a low-cardinality string representing the specific error type, or {@code null} if the + * provided error is {@code null}. */ static String extractErrorType(@Nullable Throwable error) { if (error == null) { From e63af7f7a30a33d5315a282c9ce528d40be0fed4 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 17 Mar 2026 13:27:55 -0400 Subject: [PATCH 03/10] fix: reduce code complexity --- .../api/gax/tracing/ObservabilityUtils.java | 152 +++++++++++++----- 1 file changed, 108 insertions(+), 44 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index 5bef1fda05..ef0c9462b3 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -31,8 +31,12 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode; +import com.google.common.base.Strings; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import javax.annotation.Nullable; @@ -56,6 +60,38 @@ public String toString() { } } + private static final List> CLIENT_TIMEOUT_CLASSES = + Arrays.asList( + java.util.concurrent.TimeoutException.class, java.net.SocketTimeoutException.class); + private static final List CLIENT_TIMEOUT_NAMES = + Collections.singletonList("WatchdogTimeoutException"); + + private static final List> CLIENT_CONNECTION_ERROR_CLASSES = + Arrays.asList( + java.net.ConnectException.class, + java.net.UnknownHostException.class, + java.nio.channels.UnresolvedAddressException.class); + private static final List CLIENT_CONNECTION_ERROR_NAMES = + Collections.singletonList("ConnectException"); + + private static final List CLIENT_AUTH_ERROR_SUBSTRINGS = + Arrays.asList("CredentialsException", "AuthenticationException"); + + private static final List CLIENT_RESPONSE_DECODE_ERROR_SUBSTRINGS = + Arrays.asList("ProtocolBufferParsingException", "DecodeException"); + + private static final List CLIENT_REDIRECT_ERROR_SUBSTRINGS = + Collections.singletonList("RedirectException"); + + private static final List CLIENT_REQUEST_BODY_ERROR_SUBSTRINGS = + Collections.singletonList("RequestBodyException"); + + private static final List CLIENT_REQUEST_ERROR_SUBSTRINGS = + Collections.singletonList("RequestException"); + + private static final List CLIENT_UNKNOWN_ERROR_SUBSTRINGS = + Collections.singletonList("UnknownClientException"); + /** * Extracts a low-cardinality string representing the specific classification of the error to be * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. @@ -107,76 +143,104 @@ static String extractErrorType(@Nullable Throwable error) { } if (error instanceof ApiException) { - ApiException apiException = (ApiException) error; - - // 1. Check for ErrorInfo.reason - String reason = apiException.getReason(); - if (reason != null && !reason.isEmpty()) { - return reason; + String errorType = extractFromApiException((ApiException) error); + if (errorType != null) { + return errorType; } + } - // 2. Specific Server Error Code - if (apiException.getStatusCode() != null) { - Object transportCode = apiException.getStatusCode().getTransportCode(); - if (transportCode instanceof Integer) { - // HTTP Status Code - return String.valueOf(transportCode); - } else if (apiException.getStatusCode().getCode() != null) { - // gRPC Status Code name - return apiException.getStatusCode().getCode().name(); - } - } + String clientError = getClientSideError(error); + if (clientError != null) { + return clientError; } - // 3. Client-Side Network/Operational Errors + // 4. Language-specific error type fallback String exceptionName = error.getClass().getSimpleName(); + if (exceptionName != null && !exceptionName.isEmpty()) { + return exceptionName; + } - if (error instanceof java.util.concurrent.TimeoutException - || error instanceof java.net.SocketTimeoutException - || exceptionName.equals("WatchdogTimeoutException")) { - return ErrorType.CLIENT_TIMEOUT.toString(); + // 5. Internal Fallback + return ErrorType.INTERNAL.toString(); + } + + @Nullable + private static String extractFromApiException(ApiException apiException) { + // 1. Check for ErrorInfo.reason + String reason = apiException.getReason(); + if (!Strings.isNullOrEmpty(reason)) { + return reason; + } + + // 2. Specific Server Error Code + if (apiException.getStatusCode() != null) { + Object transportCode = apiException.getStatusCode().getTransportCode(); + if (transportCode instanceof Integer) { + // HTTP Status Code + return String.valueOf(transportCode); + } else if (apiException.getStatusCode().getCode() != null) { + // gRPC Status Code name + return apiException.getStatusCode().getCode().name(); + } } + return null; + } - if (error instanceof java.net.ConnectException - || error instanceof java.net.UnknownHostException - || error instanceof java.nio.channels.UnresolvedAddressException - || exceptionName.equals("ConnectException")) { + @Nullable + private static String getClientSideError(Throwable error) { + if (isInstanceof(error, CLIENT_TIMEOUT_CLASSES)) { + return ErrorType.CLIENT_TIMEOUT.toString(); + } + if (isInstanceof(error, CLIENT_CONNECTION_ERROR_CLASSES)) { return ErrorType.CLIENT_CONNECTION_ERROR.toString(); } - if (exceptionName.contains("CredentialsException") - || exceptionName.contains("AuthenticationException")) { + String exceptionName = error.getClass().getSimpleName(); + + if (CLIENT_TIMEOUT_NAMES.contains(exceptionName)) { + return ErrorType.CLIENT_TIMEOUT.toString(); + } + if (CLIENT_CONNECTION_ERROR_NAMES.contains(exceptionName)) { + return ErrorType.CLIENT_CONNECTION_ERROR.toString(); + } + if (nameContains(exceptionName, CLIENT_AUTH_ERROR_SUBSTRINGS)) { return ErrorType.CLIENT_AUTHENTICATION_ERROR.toString(); } - - if (exceptionName.contains("ProtocolBufferParsingException") - || exceptionName.contains("DecodeException")) { + if (nameContains(exceptionName, CLIENT_RESPONSE_DECODE_ERROR_SUBSTRINGS)) { return ErrorType.CLIENT_RESPONSE_DECODE_ERROR.toString(); } - - if (exceptionName.contains("RedirectException")) { + if (nameContains(exceptionName, CLIENT_REDIRECT_ERROR_SUBSTRINGS)) { return ErrorType.CLIENT_REDIRECT_ERROR.toString(); } - - if (exceptionName.contains("RequestBodyException")) { + if (nameContains(exceptionName, CLIENT_REQUEST_BODY_ERROR_SUBSTRINGS)) { return ErrorType.CLIENT_REQUEST_BODY_ERROR.toString(); } - - if (exceptionName.contains("RequestException")) { + if (nameContains(exceptionName, CLIENT_REQUEST_ERROR_SUBSTRINGS)) { return ErrorType.CLIENT_REQUEST_ERROR.toString(); } - - if (exceptionName.contains("UnknownClientException")) { + if (nameContains(exceptionName, CLIENT_UNKNOWN_ERROR_SUBSTRINGS)) { return ErrorType.CLIENT_UNKNOWN_ERROR.toString(); } - // 4. Language-specific error type fallback - if (exceptionName != null && !exceptionName.isEmpty()) { - return exceptionName; + return null; + } + + private static boolean isInstanceof(Throwable error, List> classes) { + for (Class clazz : classes) { + if (clazz.isInstance(error)) { + return true; + } } + return false; + } - // 5. Internal Fallback - return ErrorType.INTERNAL.toString(); + private static boolean nameContains(String name, List substrings) { + for (String sub : substrings) { + if (name.contains(sub)) { + return true; + } + } + return false; } /** Function to extract the status of the error as a string */ From b215900265e6db0eee9c92f41a5b70b35031d584 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 17 Mar 2026 13:29:05 -0400 Subject: [PATCH 04/10] chore: add comments --- .../java/com/google/api/gax/tracing/ObservabilityUtils.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index ef0c9462b3..6e5bafd653 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -142,6 +142,7 @@ static String extractErrorType(@Nullable Throwable error) { return null; } + // 1. & 2. Extract error info reason or server status code if (error instanceof ApiException) { String errorType = extractFromApiException((ApiException) error); if (errorType != null) { @@ -149,6 +150,7 @@ static String extractErrorType(@Nullable Throwable error) { } } + // 3. Attempt client side error String clientError = getClientSideError(error); if (clientError != null) { return clientError; From bfcad8b2d65514eefab217dd6e63926f2a80b637 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 17 Mar 2026 14:39:25 -0400 Subject: [PATCH 05/10] refactor: put error type logic in new class --- .../google/api/gax/tracing/ErrorTypeUtil.java | 211 ++++++++++++++++++ .../api/gax/tracing/ObservabilityUtils.java | 161 +------------ .../api/gax/tracing/SpanTracerTest.java | 43 ++-- 3 files changed, 235 insertions(+), 180 deletions(-) create mode 100644 gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java new file mode 100644 index 0000000000..1e5c11ee7a --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -0,0 +1,211 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.WatchdogTimeoutException; +import com.google.common.base.Strings; +import java.net.ConnectException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.nio.channels.UnresolvedAddressException; +import javax.annotation.Nullable; +import javax.net.ssl.SSLHandshakeException; + +public class ErrorTypeUtil { + + enum ErrorType { + CLIENT_TIMEOUT, + CLIENT_CONNECTION_ERROR, + CLIENT_REQUEST_ERROR, + CLIENT_REQUEST_BODY_ERROR, + CLIENT_RESPONSE_DECODE_ERROR, + CLIENT_REDIRECT_ERROR, + CLIENT_AUTHENTICATION_ERROR, + CLIENT_UNKNOWN_ERROR, + INTERNAL; + + @Override + public String toString() { + return name(); + } + } + + /** + * Extracts a low-cardinality string representing the specific classification of the error to be + * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. + * + *

This value is determined based on the following priority: + * + *

    + *
  1. {@code google.rpc.ErrorInfo.reason}: If the error response from the service + * includes {@code google.rpc.ErrorInfo} details, the reason field (e.g., + * "RATE_LIMIT_EXCEEDED", "SERVICE_DISABLED") will be used. This offers the most precise + * error cause. + *
  2. Specific Server Error Code: If no {@code ErrorInfo.reason} is available, but a + * server error code was received: + *
      + *
    • For HTTP: The HTTP status code (e.g., "403", "503"). + *
    • For gRPC: The gRPC status code name (e.g., "PERMISSION_DENIED", "UNAVAILABLE"). + *
    + *
  3. Client-Side Network/Operational Errors: For errors occurring within the client + * library or network stack, mapping to specific enum representations from {@link + * ErrorType}: + *
      + *
    • {@code CLIENT_TIMEOUT}: A client-configured timeout was reached. + *
    • {@code CLIENT_CONNECTION_ERROR}: Failure to establish the network connection (DNS, + * TCP, TLS). + *
    • {@code CLIENT_REQUEST_ERROR}: Client-side issue forming or sending the request. + *
    • {@code CLIENT_REQUEST_BODY_ERROR}: Error streaming the request body. + *
    • {@code CLIENT_RESPONSE_DECODE_ERROR}: Client-side error decoding the response body. + *
    • {@code CLIENT_REDIRECT_ERROR}: Problem handling HTTP redirects. + *
    • {@code CLIENT_AUTHENTICATION_ERROR}: Error during credential acquisition or + * application. + *
    • {@code CLIENT_UNKNOWN_ERROR}: Other unclassified client-side network or protocol + * errors. + *
    + *
  4. Language-specific error type: The class or struct name of the exception or error + * if available. This must be low-cardinality, meaning it returns the short name of the + * exception class (e.g. {@code "IllegalStateException"}) rather than its message. + *
  5. Internal Fallback: If the error doesn't fit any of the above categories, {@code + * "INTERNAL"} will be used, indicating an unexpected issue within the client library's own + * logic. + *
+ * + * @param error the Throwable from which to extract the error type string. + * @return a low-cardinality string representing the specific error type, or {@code null} if the + * provided error is {@code null}. + */ + public static String extractErrorType(@Nullable Throwable error) { + if (error == null) { + return null; + } + + // 1. & 2. Extract error info reason or server status code + if (error instanceof ApiException) { + String errorType = extractFromApiException((ApiException) error); + if (errorType != null) { + return errorType; + } + } + + // 3. Attempt client side error + String clientError = getClientSideError(error); + if (clientError != null) { + return clientError; + } + + // 4. Language-specific error type fallback + String exceptionName = error.getClass().getSimpleName(); + if (!Strings.isNullOrEmpty(exceptionName)) { + return exceptionName; + } + + // 5. Internal Fallback + return ErrorType.INTERNAL.toString(); + } + + @Nullable + private static String extractFromApiException(ApiException apiException) { + // 1. Check for ErrorInfo.reason + String reason = apiException.getReason(); + if (!Strings.isNullOrEmpty(reason)) { + return reason; + } + + // 2. Specific Server Error Code + if (apiException.getStatusCode() != null) { + Object transportCode = apiException.getStatusCode().getTransportCode(); + if (transportCode instanceof Integer) { + // HTTP Status Code + return String.valueOf(transportCode); + } else if (apiException.getStatusCode().getCode() != null) { + // gRPC Status Code name + return apiException.getStatusCode().getCode().name(); + } + } + return null; + } + + @Nullable + private static String getClientSideError(Throwable error) { + if (isClientTimeout(error)) { + return ErrorType.CLIENT_TIMEOUT.toString(); + } + if (isClientConnectionError(error)) { + return ErrorType.CLIENT_CONNECTION_ERROR.toString(); + } + if (isClientAuthenticationError(error)) { + return ErrorType.CLIENT_AUTHENTICATION_ERROR.toString(); + } + if (isClientResponseDecodeError(error)) { + return ErrorType.CLIENT_RESPONSE_DECODE_ERROR.toString(); + } + if (isClientRedirectError(error)) { + return ErrorType.CLIENT_REDIRECT_ERROR.toString(); + } + if (error instanceof IllegalArgumentException) { // This covers CLIENT_REQUEST_ERROR + return ErrorType.CLIENT_REQUEST_ERROR.toString(); + } + if (error.getClass().getSimpleName().contains("RequestBodyException")) { + return ErrorType.CLIENT_REQUEST_BODY_ERROR.toString(); + } + if (error.getClass().getSimpleName().contains("UnknownClientException")) { + return ErrorType.CLIENT_UNKNOWN_ERROR.toString(); + } + + return null; + } + + private static boolean isClientTimeout(Throwable e) { + return e instanceof SocketTimeoutException || e instanceof WatchdogTimeoutException; + } + + private static boolean isClientConnectionError(Throwable e) { + return e instanceof ConnectException + || e instanceof UnknownHostException + || e instanceof SSLHandshakeException + || e instanceof UnresolvedAddressException; + } + + private static boolean isClientResponseDecodeError(Throwable e) { + return e.getClass().getName().contains("Json") + || e.getClass().getName().contains("Gson") + || (e.getCause() != null && e.getCause().getClass().getName().contains("Gson")); + } + + private static boolean isClientRedirectError(Throwable e) { + return e.getMessage() != null && e.getMessage().contains("redirect"); + } + + private static boolean isClientAuthenticationError(Throwable e) { + return e.getClass().getName().contains("GoogleAuthException"); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index 6e5bafd653..ca5a58e496 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -31,67 +31,14 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode; -import com.google.common.base.Strings; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import javax.annotation.Nullable; class ObservabilityUtils { - enum ErrorType { - CLIENT_TIMEOUT, - CLIENT_CONNECTION_ERROR, - CLIENT_REQUEST_ERROR, - CLIENT_REQUEST_BODY_ERROR, - CLIENT_RESPONSE_DECODE_ERROR, - CLIENT_REDIRECT_ERROR, - CLIENT_AUTHENTICATION_ERROR, - CLIENT_UNKNOWN_ERROR, - INTERNAL; - - @Override - public String toString() { - return name(); - } - } - - private static final List> CLIENT_TIMEOUT_CLASSES = - Arrays.asList( - java.util.concurrent.TimeoutException.class, java.net.SocketTimeoutException.class); - private static final List CLIENT_TIMEOUT_NAMES = - Collections.singletonList("WatchdogTimeoutException"); - - private static final List> CLIENT_CONNECTION_ERROR_CLASSES = - Arrays.asList( - java.net.ConnectException.class, - java.net.UnknownHostException.class, - java.nio.channels.UnresolvedAddressException.class); - private static final List CLIENT_CONNECTION_ERROR_NAMES = - Collections.singletonList("ConnectException"); - - private static final List CLIENT_AUTH_ERROR_SUBSTRINGS = - Arrays.asList("CredentialsException", "AuthenticationException"); - - private static final List CLIENT_RESPONSE_DECODE_ERROR_SUBSTRINGS = - Arrays.asList("ProtocolBufferParsingException", "DecodeException"); - - private static final List CLIENT_REDIRECT_ERROR_SUBSTRINGS = - Collections.singletonList("RedirectException"); - - private static final List CLIENT_REQUEST_BODY_ERROR_SUBSTRINGS = - Collections.singletonList("RequestBodyException"); - - private static final List CLIENT_REQUEST_ERROR_SUBSTRINGS = - Collections.singletonList("RequestException"); - - private static final List CLIENT_UNKNOWN_ERROR_SUBSTRINGS = - Collections.singletonList("UnknownClientException"); - /** * Extracts a low-cardinality string representing the specific classification of the error to be * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. @@ -111,7 +58,7 @@ public String toString() { * *
  • Client-Side Network/Operational Errors: For errors occurring within the client * library or network stack, mapping to specific enum representations from {@link - * ErrorType}: + * ErrorTypeUtil.ErrorType}: *
      *
    • {@code CLIENT_TIMEOUT}: A client-configured timeout was reached. *
    • {@code CLIENT_CONNECTION_ERROR}: Failure to establish the network connection (DNS, @@ -138,111 +85,7 @@ public String toString() { * provided error is {@code null}. */ static String extractErrorType(@Nullable Throwable error) { - if (error == null) { - return null; - } - - // 1. & 2. Extract error info reason or server status code - if (error instanceof ApiException) { - String errorType = extractFromApiException((ApiException) error); - if (errorType != null) { - return errorType; - } - } - - // 3. Attempt client side error - String clientError = getClientSideError(error); - if (clientError != null) { - return clientError; - } - - // 4. Language-specific error type fallback - String exceptionName = error.getClass().getSimpleName(); - if (exceptionName != null && !exceptionName.isEmpty()) { - return exceptionName; - } - - // 5. Internal Fallback - return ErrorType.INTERNAL.toString(); - } - - @Nullable - private static String extractFromApiException(ApiException apiException) { - // 1. Check for ErrorInfo.reason - String reason = apiException.getReason(); - if (!Strings.isNullOrEmpty(reason)) { - return reason; - } - - // 2. Specific Server Error Code - if (apiException.getStatusCode() != null) { - Object transportCode = apiException.getStatusCode().getTransportCode(); - if (transportCode instanceof Integer) { - // HTTP Status Code - return String.valueOf(transportCode); - } else if (apiException.getStatusCode().getCode() != null) { - // gRPC Status Code name - return apiException.getStatusCode().getCode().name(); - } - } - return null; - } - - @Nullable - private static String getClientSideError(Throwable error) { - if (isInstanceof(error, CLIENT_TIMEOUT_CLASSES)) { - return ErrorType.CLIENT_TIMEOUT.toString(); - } - if (isInstanceof(error, CLIENT_CONNECTION_ERROR_CLASSES)) { - return ErrorType.CLIENT_CONNECTION_ERROR.toString(); - } - - String exceptionName = error.getClass().getSimpleName(); - - if (CLIENT_TIMEOUT_NAMES.contains(exceptionName)) { - return ErrorType.CLIENT_TIMEOUT.toString(); - } - if (CLIENT_CONNECTION_ERROR_NAMES.contains(exceptionName)) { - return ErrorType.CLIENT_CONNECTION_ERROR.toString(); - } - if (nameContains(exceptionName, CLIENT_AUTH_ERROR_SUBSTRINGS)) { - return ErrorType.CLIENT_AUTHENTICATION_ERROR.toString(); - } - if (nameContains(exceptionName, CLIENT_RESPONSE_DECODE_ERROR_SUBSTRINGS)) { - return ErrorType.CLIENT_RESPONSE_DECODE_ERROR.toString(); - } - if (nameContains(exceptionName, CLIENT_REDIRECT_ERROR_SUBSTRINGS)) { - return ErrorType.CLIENT_REDIRECT_ERROR.toString(); - } - if (nameContains(exceptionName, CLIENT_REQUEST_BODY_ERROR_SUBSTRINGS)) { - return ErrorType.CLIENT_REQUEST_BODY_ERROR.toString(); - } - if (nameContains(exceptionName, CLIENT_REQUEST_ERROR_SUBSTRINGS)) { - return ErrorType.CLIENT_REQUEST_ERROR.toString(); - } - if (nameContains(exceptionName, CLIENT_UNKNOWN_ERROR_SUBSTRINGS)) { - return ErrorType.CLIENT_UNKNOWN_ERROR.toString(); - } - - return null; - } - - private static boolean isInstanceof(Throwable error, List> classes) { - for (Class clazz : classes) { - if (clazz.isInstance(error)) { - return true; - } - } - return false; - } - - private static boolean nameContains(String name, List substrings) { - for (String sub : substrings) { - if (name.contains(sub)) { - return true; - } - } - return false; + return ErrorTypeUtil.extractErrorType(error); } /** Function to extract the status of the error as a string */ diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index 9439327dbc..e987ecf34d 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -39,11 +39,12 @@ import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.StatusCode; import com.google.common.collect.ImmutableList; +import com.google.gson.JsonSyntaxException; import com.google.protobuf.Any; import com.google.rpc.ErrorInfo; import java.net.ConnectException; +import java.net.SocketTimeoutException; import java.util.Map; -import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -185,12 +186,12 @@ void testAttemptFailed_clientTimeout() { tracer.attemptStarted(new Object(), 1); - tracer.attemptFailedRetriesExhausted(new TimeoutException("timed out")); + tracer.attemptFailedRetriesExhausted(new SocketTimeoutException()); verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.CLIENT_TIMEOUT.toString()); + ErrorTypeUtil.ErrorType.CLIENT_TIMEOUT.toString()); verify(attemptHandle).end(); } @@ -205,7 +206,7 @@ void testAttemptFailed_clientConnectionError() { verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); verify(attemptHandle).end(); } @@ -215,12 +216,12 @@ void testAttemptFailed_clientAuthenticationError() { tracer.attemptStarted(new Object(), 1); - tracer.attemptFailedRetriesExhausted(new CredentialsException()); + tracer.attemptFailedRetriesExhausted(new TestGoogleAuthException()); verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.CLIENT_AUTHENTICATION_ERROR.toString()); + ErrorTypeUtil.ErrorType.CLIENT_AUTHENTICATION_ERROR.toString()); verify(attemptHandle).end(); } @@ -230,12 +231,12 @@ void testAttemptFailed_clientResponseDecodeError() { tracer.attemptStarted(new Object(), 1); - tracer.attemptFailedRetriesExhausted(new DecodeException()); + tracer.attemptFailedRetriesExhausted(new JsonSyntaxException("bad json")); verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.CLIENT_RESPONSE_DECODE_ERROR.toString()); + ErrorTypeUtil.ErrorType.CLIENT_RESPONSE_DECODE_ERROR.toString()); verify(attemptHandle).end(); } @@ -245,12 +246,12 @@ void testAttemptFailed_clientRedirectError() { tracer.attemptStarted(new Object(), 1); - tracer.attemptFailedRetriesExhausted(new RedirectException()); + tracer.attemptFailedRetriesExhausted(new RedirectException("redirect failed")); verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.CLIENT_REDIRECT_ERROR.toString()); + ErrorTypeUtil.ErrorType.CLIENT_REDIRECT_ERROR.toString()); verify(attemptHandle).end(); } @@ -265,7 +266,7 @@ void testAttemptFailed_clientRequestBodyError() { verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.CLIENT_REQUEST_BODY_ERROR.toString()); + ErrorTypeUtil.ErrorType.CLIENT_REQUEST_BODY_ERROR.toString()); verify(attemptHandle).end(); } @@ -275,12 +276,12 @@ void testAttemptFailed_clientRequestError() { tracer.attemptStarted(new Object(), 1); - tracer.attemptFailedRetriesExhausted(new RequestException()); + tracer.attemptFailedRetriesExhausted(new IllegalArgumentException()); verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.CLIENT_REQUEST_ERROR.toString()); + ErrorTypeUtil.ErrorType.CLIENT_REQUEST_ERROR.toString()); verify(attemptHandle).end(); } @@ -295,7 +296,7 @@ void testAttemptFailed_clientUnknownError() { verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.CLIENT_UNKNOWN_ERROR.toString()); + ErrorTypeUtil.ErrorType.CLIENT_UNKNOWN_ERROR.toString()); verify(attemptHandle).end(); } @@ -325,19 +326,19 @@ void testAttemptFailed_internalFallback() { verify(attemptHandle) .addAttribute( ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.ErrorType.INTERNAL.toString()); + ErrorTypeUtil.ErrorType.INTERNAL.toString()); verify(attemptHandle).end(); } - private static class CredentialsException extends RuntimeException {} + private static class TestGoogleAuthException extends RuntimeException {} - private static class DecodeException extends RuntimeException {} - - private static class RedirectException extends RuntimeException {} + private static class RedirectException extends RuntimeException { + public RedirectException(String message) { + super(message); + } + } private static class RequestBodyException extends RuntimeException {} - private static class RequestException extends RuntimeException {} - private static class UnknownClientException extends RuntimeException {} } From d3cd69d600020178be22733a74584aaeab53403f Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 17 Mar 2026 14:46:59 -0400 Subject: [PATCH 06/10] fix: handle request body and unknown error --- .../com/google/api/gax/tracing/ErrorTypeUtil.java | 15 ++++++++++----- .../google/api/gax/tracing/SpanTracerTest.java | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java index 1e5c11ee7a..c930cb355e 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -88,8 +88,6 @@ public String toString() { *
    • {@code CLIENT_REDIRECT_ERROR}: Problem handling HTTP redirects. *
    • {@code CLIENT_AUTHENTICATION_ERROR}: Error during credential acquisition or * application. - *
    • {@code CLIENT_UNKNOWN_ERROR}: Other unclassified client-side network or protocol - * errors. *
    *
  • Language-specific error type: The class or struct name of the exception or error * if available. This must be low-cardinality, meaning it returns the short name of the @@ -174,13 +172,12 @@ private static String getClientSideError(Throwable error) { if (error instanceof IllegalArgumentException) { // This covers CLIENT_REQUEST_ERROR return ErrorType.CLIENT_REQUEST_ERROR.toString(); } - if (error.getClass().getSimpleName().contains("RequestBodyException")) { + if (isRequestBodyError(error)) { return ErrorType.CLIENT_REQUEST_BODY_ERROR.toString(); } - if (error.getClass().getSimpleName().contains("UnknownClientException")) { + if (isClientUnknownError(error)) { return ErrorType.CLIENT_UNKNOWN_ERROR.toString(); } - return null; } @@ -208,4 +205,12 @@ private static boolean isClientRedirectError(Throwable e) { private static boolean isClientAuthenticationError(Throwable e) { return e.getClass().getName().contains("GoogleAuthException"); } + + private static boolean isRequestBodyError(Throwable e) { + return e.getClass().getName().contains("RestSerializationException"); + } + + private static boolean isClientUnknownError(Throwable e) { + return e.getClass().getName().toLowerCase().contains("unknown"); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index e987ecf34d..69014be353 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -261,7 +261,7 @@ void testAttemptFailed_clientRequestBodyError() { tracer.attemptStarted(new Object(), 1); - tracer.attemptFailedRetriesExhausted(new RequestBodyException()); + tracer.attemptFailedRetriesExhausted(new TestRestSerializationException()); verify(attemptHandle) .addAttribute( @@ -338,7 +338,7 @@ public RedirectException(String message) { } } - private static class RequestBodyException extends RuntimeException {} + private static class TestRestSerializationException extends RuntimeException {} private static class UnknownClientException extends RuntimeException {} } From f38a66d30550fc3c208d975aff5c1b8dcebdd783 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 17 Mar 2026 14:55:23 -0400 Subject: [PATCH 07/10] fix: improve handling for INTERNAL --- .../google/api/gax/tracing/ErrorTypeUtil.java | 4 +- .../api/gax/tracing/ObservabilityUtils.java | 44 +------------------ .../google/api/gax/tracing/SpanTracer.java | 7 +-- .../api/gax/tracing/SpanTracerTest.java | 17 +++++++ 4 files changed, 24 insertions(+), 48 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java index c930cb355e..6e3691ef7c 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -88,6 +88,7 @@ public String toString() { *
  • {@code CLIENT_REDIRECT_ERROR}: Problem handling HTTP redirects. *
  • {@code CLIENT_AUTHENTICATION_ERROR}: Error during credential acquisition or * application. + *
  • {@code CLIENT_UNKNOWN_ERROR}: For all other errors unknown to the client. * *
  • Language-specific error type: The class or struct name of the exception or error * if available. This must be low-cardinality, meaning it returns the short name of the @@ -103,7 +104,8 @@ public String toString() { */ public static String extractErrorType(@Nullable Throwable error) { if (error == null) { - return null; + // No information about the error; we default to INTERNAL. + return ErrorType.INTERNAL.toString(); } // 1. & 2. Extract error info reason or server status code diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index ca5a58e496..7cc3bf0e41 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -41,48 +41,8 @@ class ObservabilityUtils { /** * Extracts a low-cardinality string representing the specific classification of the error to be - * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. - * - *

    This value is determined based on the following priority: - * - *

      - *
    1. {@code google.rpc.ErrorInfo.reason}: If the error response from the service - * includes {@code google.rpc.ErrorInfo} details, the reason field (e.g., - * "RATE_LIMIT_EXCEEDED", "SERVICE_DISABLED") will be used. This offers the most precise - * error cause. - *
    2. Specific Server Error Code: If no {@code ErrorInfo.reason} is available, but a - * server error code was received: - *
        - *
      • For HTTP: The HTTP status code (e.g., "403", "503"). - *
      • For gRPC: The gRPC status code name (e.g., "PERMISSION_DENIED", "UNAVAILABLE"). - *
      - *
    3. Client-Side Network/Operational Errors: For errors occurring within the client - * library or network stack, mapping to specific enum representations from {@link - * ErrorTypeUtil.ErrorType}: - *
        - *
      • {@code CLIENT_TIMEOUT}: A client-configured timeout was reached. - *
      • {@code CLIENT_CONNECTION_ERROR}: Failure to establish the network connection (DNS, - * TCP, TLS). - *
      • {@code CLIENT_REQUEST_ERROR}: Client-side issue forming or sending the request. - *
      • {@code CLIENT_REQUEST_BODY_ERROR}: Error streaming the request body. - *
      • {@code CLIENT_RESPONSE_DECODE_ERROR}: Client-side error decoding the response body. - *
      • {@code CLIENT_REDIRECT_ERROR}: Problem handling HTTP redirects. - *
      • {@code CLIENT_AUTHENTICATION_ERROR}: Error during credential acquisition or - * application. - *
      • {@code CLIENT_UNKNOWN_ERROR}: Other unclassified client-side network or protocol - * errors. - *
      - *
    4. Language-specific error type: The class or struct name of the exception or error - * if available. This must be low-cardinality, meaning it returns the short name of the - * exception class (e.g. {@code "IllegalStateException"}) rather than its message. - *
    5. Internal Fallback: If the error doesn't fit any of the above categories, {@code - * "INTERNAL"} will be used, indicating an unexpected issue within the client library's own - * logic. - *
    - * - * @param error the Throwable from which to extract the error type string. - * @return a low-cardinality string representing the specific error type, or {@code null} if the - * provided error is {@code null}. + * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. See {@link + * ErrorTypeUtil#extractErrorType} for extended documentation. */ static String extractErrorType(@Nullable Throwable error) { return ErrorTypeUtil.extractErrorType(error); diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java index 122e2a66f3..f82a461cb0 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java @@ -107,11 +107,8 @@ public void attemptPermanentFailure(Throwable error) { private void recordErrorAndEndAttempt(Throwable error) { if (attemptHandle != null) { - if (error != null) { - attemptHandle.addAttribute( - ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ObservabilityUtils.extractErrorType(error)); - } + attemptHandle.addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, ObservabilityUtils.extractErrorType(error)); endAttempt(); } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index 69014be353..8f9e6dd08d 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -330,6 +330,23 @@ void testAttemptFailed_internalFallback() { verify(attemptHandle).end(); } + @Test + void testAttemptFailed_internalFallback_nullError() { + when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); + + tracer.attemptStarted(new Object(), 1); + + tracer.attemptFailedRetriesExhausted(null); + + // For an anonymous inner class Throwable, getSimpleName() is empty string, which triggers the + // fallback + verify(attemptHandle) + .addAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ErrorTypeUtil.ErrorType.INTERNAL.toString()); + verify(attemptHandle).end(); + } + private static class TestGoogleAuthException extends RuntimeException {} private static class RedirectException extends RuntimeException { From efbe7bad4106da052f5fa3534c2166267fc3be03 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 17 Mar 2026 15:06:09 -0400 Subject: [PATCH 08/10] docs: add javadoc for private methods --- .../google/api/gax/tracing/ErrorTypeUtil.java | 68 ++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java index 6e3691ef7c..16e3fdc7dd 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -132,6 +132,13 @@ public static String extractErrorType(@Nullable Throwable error) { return ErrorType.INTERNAL.toString(); } + /** + * Extracts the error type from an ApiException. This method prioritizes the ErrorInfo reason, + * then the transport-specific status code (HTTP or gRPC). + * + * @param apiException The ApiException to extract the error type from. + * @return A string representing the error type, or null if no specific type can be determined. + */ @Nullable private static String extractFromApiException(ApiException apiException) { // 1. Check for ErrorInfo.reason @@ -154,6 +161,13 @@ private static String extractFromApiException(ApiException apiException) { return null; } + /** + * Determines the client-side error type based on the provided Throwable. This method checks for + * various network and client-specific exceptions. + * + * @param error The Throwable to analyze. + * @return A string representing the client-side error type, or null if not matched. + */ @Nullable private static String getClientSideError(Throwable error) { if (isClientTimeout(error)) { @@ -171,7 +185,8 @@ private static String getClientSideError(Throwable error) { if (isClientRedirectError(error)) { return ErrorType.CLIENT_REDIRECT_ERROR.toString(); } - if (error instanceof IllegalArgumentException) { // This covers CLIENT_REQUEST_ERROR + // This covers CLIENT_REQUEST_ERROR for general illegal arguments in client requests. + if (error instanceof IllegalArgumentException) { return ErrorType.CLIENT_REQUEST_ERROR.toString(); } if (isRequestBodyError(error)) { @@ -183,10 +198,24 @@ private static String getClientSideError(Throwable error) { return null; } + /** + * Checks if the given Throwable represents a client-side timeout error. This includes socket + * timeouts and GAX-specific watchdog timeouts. + * + * @param e The Throwable to check. + * @return true if the error is a client timeout, false otherwise. + */ private static boolean isClientTimeout(Throwable e) { return e instanceof SocketTimeoutException || e instanceof WatchdogTimeoutException; } + /** + * Checks if the given Throwable represents a client-side connection error. This includes issues + * with establishing connections, unknown hosts, SSL handshakes, and unresolved addresses. + * + * @param e The Throwable to check. + * @return true if the error is a client connection error, false otherwise. + */ private static boolean isClientConnectionError(Throwable e) { return e instanceof ConnectException || e instanceof UnknownHostException @@ -194,24 +223,61 @@ private static boolean isClientConnectionError(Throwable e) { || e instanceof UnresolvedAddressException; } + /** + * Checks if the given Throwable represents a client-side response decoding error. This is + * identified by exceptions related to JSON or Gson parsing, either directly or as a cause. + * + * @param e The Throwable to check. + * @return true if the error is a client response decode error, false otherwise. + */ private static boolean isClientResponseDecodeError(Throwable e) { return e.getClass().getName().contains("Json") || e.getClass().getName().contains("Gson") || (e.getCause() != null && e.getCause().getClass().getName().contains("Gson")); } + /** + * Checks if the given Throwable represents a client-side redirect error. This is identified by + * the presence of "redirect" in the exception message. + * + * @param e The Throwable to check. + * @return true if the error is a client redirect error, false otherwise. + */ private static boolean isClientRedirectError(Throwable e) { return e.getMessage() != null && e.getMessage().contains("redirect"); } + /** + * Checks if the given Throwable represents a client-side authentication error. This is identified + * by exceptions related to the auth library. + * + * @param e The Throwable to check. + * @return true if the error is a client authentication error, false otherwise. + */ private static boolean isClientAuthenticationError(Throwable e) { return e.getClass().getName().contains("GoogleAuthException"); } + /** + * Checks if the given Throwable represents a client-side request body error. This is specifically + * mapped to RestSerializationException from httpjson, which indicates issues during the + * serialization of the request body for REST calls. + * + * @param e The Throwable to check. + * @return true if the error is a client request body error, false otherwise. + */ private static boolean isRequestBodyError(Throwable e) { return e.getClass().getName().contains("RestSerializationException"); } + /** + * Checks if the given Throwable represents an unknown client-side error. This is a general + * fallback for exceptions whose class name contains "unknown", indicating an unclassified + * client-side issue. + * + * @param e The Throwable to check. + * @return true if the error is an unknown client error, false otherwise. + */ private static boolean isClientUnknownError(Throwable e) { return e.getClass().getName().toLowerCase().contains("unknown"); } From 75fe9caf1cac95c1996af9e14f480f5068549f13 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 17 Mar 2026 15:32:09 -0400 Subject: [PATCH 09/10] fix: use recursive exception logic --- .../google/api/gax/tracing/ErrorTypeUtil.java | 61 ++++++++++++++----- .../api/gax/tracing/SpanTracerTest.java | 34 ----------- 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java index 16e3fdc7dd..cbe4e4a580 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -30,14 +30,10 @@ package com.google.api.gax.tracing; import com.google.api.gax.rpc.ApiException; -import com.google.api.gax.rpc.WatchdogTimeoutException; import com.google.common.base.Strings; -import java.net.ConnectException; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.nio.channels.UnresolvedAddressException; +import com.google.common.collect.ImmutableSet; +import java.util.Set; import javax.annotation.Nullable; -import javax.net.ssl.SSLHandshakeException; public class ErrorTypeUtil { @@ -58,6 +54,26 @@ public String toString() { } } + private static final Set JSON_DECODING_EXCEPTION_CLASS_NAMES = + ImmutableSet.of( + "com.google.gson.JsonSyntaxException", + "com.google.gson.JsonParseException", + "com.fasterxml.jackson.databind.JsonMappingException", + "com.fasterxml.jackson.core.JsonParseException"); + + private static final Set AUTHENTICATION_EXCEPTION_CLASS_NAMES = + ImmutableSet.of("com.google.auth.oauth2.GoogleAuthException"); + + private static final Set CLIENT_TIMEOUT_EXCEPTION_CLASS_NAMES = + ImmutableSet.of( + "java.net.SocketTimeoutException", "com.google.api.gax.rpc.WatchdogTimeoutException"); + private static final Set CLIENT_CONNECTION_EXCEPTIONS = + ImmutableSet.of( + "java.net.ConnectException", + "java.net.UnknownHostException", + "javax.net.ssl.SSLHandshakeException", + "java.nio.channels.UnresolvedAddressException"); + /** * Extracts a low-cardinality string representing the specific classification of the error to be * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. @@ -206,7 +222,7 @@ private static String getClientSideError(Throwable error) { * @return true if the error is a client timeout, false otherwise. */ private static boolean isClientTimeout(Throwable e) { - return e instanceof SocketTimeoutException || e instanceof WatchdogTimeoutException; + return hasErrorNameInCauseChain(e, CLIENT_TIMEOUT_EXCEPTION_CLASS_NAMES); } /** @@ -217,10 +233,7 @@ private static boolean isClientTimeout(Throwable e) { * @return true if the error is a client connection error, false otherwise. */ private static boolean isClientConnectionError(Throwable e) { - return e instanceof ConnectException - || e instanceof UnknownHostException - || e instanceof SSLHandshakeException - || e instanceof UnresolvedAddressException; + return hasErrorNameInCauseChain(e, CLIENT_CONNECTION_EXCEPTIONS); } /** @@ -231,9 +244,7 @@ private static boolean isClientConnectionError(Throwable e) { * @return true if the error is a client response decode error, false otherwise. */ private static boolean isClientResponseDecodeError(Throwable e) { - return e.getClass().getName().contains("Json") - || e.getClass().getName().contains("Gson") - || (e.getCause() != null && e.getCause().getClass().getName().contains("Gson")); + return hasErrorNameInCauseChain(e, JSON_DECODING_EXCEPTION_CLASS_NAMES); } /** @@ -255,7 +266,7 @@ private static boolean isClientRedirectError(Throwable e) { * @return true if the error is a client authentication error, false otherwise. */ private static boolean isClientAuthenticationError(Throwable e) { - return e.getClass().getName().contains("GoogleAuthException"); + return hasErrorNameInCauseChain(e, AUTHENTICATION_EXCEPTION_CLASS_NAMES); } /** @@ -267,7 +278,7 @@ private static boolean isClientAuthenticationError(Throwable e) { * @return true if the error is a client request body error, false otherwise. */ private static boolean isRequestBodyError(Throwable e) { - return e.getClass().getName().contains("RestSerializationException"); + return hasErrorNameInCauseChain(e, ImmutableSet.of("RestSerializationException")); } /** @@ -281,4 +292,22 @@ private static boolean isRequestBodyError(Throwable e) { private static boolean isClientUnknownError(Throwable e) { return e.getClass().getName().toLowerCase().contains("unknown"); } + + /** + * Recursively checks the throwable and its cause chain for any of the specified error name. + * + * @param t The Throwable to check. + * @param errorClassNames A set of fully qualified class names to check against. + * @return true if an error from the set is found in the cause chain, false otherwise. + */ + private static boolean hasErrorNameInCauseChain(Throwable t, Set errorClassNames) { + Throwable current = t; + while (current != null) { + if (errorClassNames.contains(current.getClass().getName())) { + return true; + } + current = current.getCause(); + } + return false; + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java index 8f9e6dd08d..2c2bf3c5c5 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java @@ -210,21 +210,6 @@ void testAttemptFailed_clientConnectionError() { verify(attemptHandle).end(); } - @Test - void testAttemptFailed_clientAuthenticationError() { - when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); - - tracer.attemptStarted(new Object(), 1); - - tracer.attemptFailedRetriesExhausted(new TestGoogleAuthException()); - - verify(attemptHandle) - .addAttribute( - ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ErrorTypeUtil.ErrorType.CLIENT_AUTHENTICATION_ERROR.toString()); - verify(attemptHandle).end(); - } - @Test void testAttemptFailed_clientResponseDecodeError() { when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); @@ -255,21 +240,6 @@ void testAttemptFailed_clientRedirectError() { verify(attemptHandle).end(); } - @Test - void testAttemptFailed_clientRequestBodyError() { - when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); - - tracer.attemptStarted(new Object(), 1); - - tracer.attemptFailedRetriesExhausted(new TestRestSerializationException()); - - verify(attemptHandle) - .addAttribute( - ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, - ErrorTypeUtil.ErrorType.CLIENT_REQUEST_BODY_ERROR.toString()); - verify(attemptHandle).end(); - } - @Test void testAttemptFailed_clientRequestError() { when(recorder.createSpan(eq(ATTEMPT_SPAN_NAME), anyMap())).thenReturn(attemptHandle); @@ -347,15 +317,11 @@ void testAttemptFailed_internalFallback_nullError() { verify(attemptHandle).end(); } - private static class TestGoogleAuthException extends RuntimeException {} - private static class RedirectException extends RuntimeException { public RedirectException(String message) { super(message); } } - private static class TestRestSerializationException extends RuntimeException {} - private static class UnknownClientException extends RuntimeException {} } From f746253cffc342ad94c1116fad9d3f96417ffa42 Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Wed, 18 Mar 2026 16:18:12 -0400 Subject: [PATCH 10/10] fix: remove unused exceptions --- .../main/java/com/google/api/gax/tracing/ErrorTypeUtil.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java index cbe4e4a580..9858e820fb 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -55,11 +55,7 @@ public String toString() { } private static final Set JSON_DECODING_EXCEPTION_CLASS_NAMES = - ImmutableSet.of( - "com.google.gson.JsonSyntaxException", - "com.google.gson.JsonParseException", - "com.fasterxml.jackson.databind.JsonMappingException", - "com.fasterxml.jackson.core.JsonParseException"); + ImmutableSet.of("com.google.gson.JsonSyntaxException", "com.google.gson.JsonParseException"); private static final Set AUTHENTICATION_EXCEPTION_CLASS_NAMES = ImmutableSet.of("com.google.auth.oauth2.GoogleAuthException");