From e9382eeaca5498821d3c058aa3c6db0fcbc0a5ba Mon Sep 17 00:00:00 2001 From: Diego Marquez Date: Tue, 24 Mar 2026 15:54:31 -0400 Subject: [PATCH 1/5] feat: Refine tracing telemetry for client-side attributes --- .../com/google/api/gax/tracing/ApiTracer.java | 10 + .../google/api/gax/tracing/ErrorTypeUtil.java | 252 +++++++++ .../gax/tracing/ObservabilityAttributes.java | 12 + .../api/gax/tracing/ObservabilityUtils.java | 9 + .../google/api/gax/tracing/SpanTracer.java | 36 +- .../google/api/gax/rpc/ErrorTypeUtilTest.java | 196 +++++++ .../api/gax/tracing/SpanTracerTest.java | 234 ++++++++ .../showcase/v1beta1/it/ITOtelErrorType.java | 507 ++++++++++++++++++ 8 files changed, 1253 insertions(+), 3 deletions(-) create mode 100644 sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java create mode 100644 sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ErrorTypeUtilTest.java create mode 100644 sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelErrorType.java diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java index 97f8e017db28..003242f3b4a4 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java @@ -183,6 +183,16 @@ default void responseReceived() {} default void requestSent() {} ; + /** + * Adds an annotation that a streaming request has been sent. + * + * @param requestSize the size of the request in bytes. + */ + default void requestSent(long requestSize) { + requestSent(); + } + ; + /** * Adds an annotation that a batch of writes has been flushed. * diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java new file mode 100644 index 000000000000..37af9331599e --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -0,0 +1,252 @@ +/* + * 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.DeadlineExceededException; +import com.google.api.gax.rpc.WatchdogTimeoutException; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import java.net.BindException; +import java.net.ConnectException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.nio.channels.UnresolvedAddressException; +import java.security.GeneralSecurityException; +import java.util.Set; +import javax.annotation.Nullable; +import javax.net.ssl.SSLHandshakeException; + +public class ErrorTypeUtil { + + public enum ErrorType { + CLIENT_TIMEOUT, + CLIENT_CONNECTION_ERROR, + CLIENT_REQUEST_ERROR, + /** Placeholder for potential future request body errors. */ + CLIENT_REQUEST_BODY_ERROR, + /** Placeholder for potential future response decode errors. */ + CLIENT_RESPONSE_DECODE_ERROR, + /** Placeholder for potential future redirect errors. */ + CLIENT_REDIRECT_ERROR, + CLIENT_AUTHENTICATION_ERROR, + /** Placeholder for potential future unknown errors. */ + CLIENT_UNKNOWN_ERROR, + INTERNAL; + + @Override + public String toString() { + return name(); + } + } + + private static final Set> AUTHENTICATION_EXCEPTION_CLASSES = + ImmutableSet.of(GeneralSecurityException.class); + + private static final Set> CLIENT_TIMEOUT_EXCEPTION_CLASSES = + ImmutableSet.of( + SocketTimeoutException.class, + WatchdogTimeoutException.class, + DeadlineExceededException.class); + + private static final Set> CLIENT_CONNECTION_EXCEPTIONS = + ImmutableSet.of( + ConnectException.class, + UnknownHostException.class, + SSLHandshakeException.class, + UnresolvedAddressException.class, + NoRouteToHostException.class, + BindException.class); + + /** + * 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. Client-Side Network/Operational Errors: For errors occurring within the client + * library or network stack, mapping to specific enum representations from {@link + * ErrorType}. This includes checking the cause chain for diagnostic markers (e.g., {@code + * ConnectException} or {@code SocketTimeoutException}). + *
  3. Specific Server Error Code: If no {@code ErrorInfo.reason} is available and it is + * not a client-side failure, 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"). + *
    + *
  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) { + // No information about the error; we default to INTERNAL. + return ErrorType.INTERNAL.toString(); + } + + // 1. Extract error info reason (most specific server-side info) + if (error instanceof ApiException) { + String reason = ((ApiException) error).getReason(); + if (!Strings.isNullOrEmpty(reason)) { + return reason; + } + } + + // 2. Attempt client side error (includes checking cause chains) + String clientError = getClientSideError(error); + if (clientError != null) { + return clientError; + } + + // 3. Extract server status code if available + if (error instanceof ApiException) { + String errorCode = extractServerErrorCode((ApiException) error); + if (errorCode != null) { + return errorCode; + } + } + + // 4. Language-specific error type fallback + String exceptionName = error.getClass().getSimpleName(); + if (!Strings.isNullOrEmpty(exceptionName)) { + return exceptionName; + } + + // 5. Internal Fallback + return ErrorType.INTERNAL.toString(); + } + + /** + * Extracts the server error code from an ApiException. + * + * @param apiException The ApiException to extract the error code from. + * @return A string representing the error code, or null if no specific code can be determined. + */ + @Nullable + private static String extractServerErrorCode(ApiException apiException) { + 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; + } + + /** + * 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)) { + return ErrorType.CLIENT_TIMEOUT.toString(); + } + if (isClientConnectionError(error)) { + return ErrorType.CLIENT_CONNECTION_ERROR.toString(); + } + if (isClientAuthenticationError(error)) { + return ErrorType.CLIENT_AUTHENTICATION_ERROR.toString(); + } + // This covers CLIENT_REQUEST_ERROR for general illegal arguments in client requests. + if (error instanceof IllegalArgumentException) { + return ErrorType.CLIENT_REQUEST_ERROR.toString(); + } + 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 hasErrorClassInCauseChain(e, CLIENT_TIMEOUT_EXCEPTION_CLASSES); + } + + /** + * 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 hasErrorClassInCauseChain(e, CLIENT_CONNECTION_EXCEPTIONS); + } + + private static boolean isClientAuthenticationError(Throwable e) { + return hasErrorClassInCauseChain(e, AUTHENTICATION_EXCEPTION_CLASSES); + } + + /** + * Recursively checks the throwable and its cause chain for any of the specified error classes. + * + * @param t The Throwable to check. + * @param errorClasses A set of class objects to check against. + * @return true if an error from the set is found in the cause chain, false otherwise. + */ + private static boolean hasErrorClassInCauseChain( + Throwable t, Set> errorClasses) { + Throwable current = t; + while (current != null) { + for (Class errorClass : errorClasses) { + if (errorClass.isInstance(current)) { + return true; + } + } + current = current.getCause(); + } + return false; + } +} 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 64095fde0099..7c1294a7d536 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 @@ -85,6 +85,18 @@ 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"; + + /** A human-readable error message, which may include details from the exception or response. */ + public static final String STATUS_MESSAGE_ATTRIBUTE = "status.message"; + + /** If the error was caused by an exception, the exception class name. */ + public static final String EXCEPTION_TYPE_ATTRIBUTE = "exception.type"; + /** 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 2487964370ca..a914f4c2c15e 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 @@ -39,6 +39,15 @@ 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. See {@link + * ErrorTypeUtil#extractErrorType} for extended documentation. + */ + static String extractErrorType(@Nullable Throwable error) { + return ErrorTypeUtil.extractErrorType(error); + } + /** Function to extract the status of the error as a string */ static String extractStatus(@Nullable Throwable error) { final String statusString; 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 a2690359dd4c..ee00ee797593 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 @@ -141,17 +141,47 @@ public void attemptCancelled() { @Override public void attemptFailedDuration(Throwable error, java.time.Duration delay) { - endAttempt(); + recordErrorAndEndAttempt(error); } @Override public void attemptFailedRetriesExhausted(Throwable error) { - endAttempt(); + recordErrorAndEndAttempt(error); } @Override public void attemptPermanentFailure(Throwable error) { - endAttempt(); + recordErrorAndEndAttempt(error); + } + + private void recordErrorAndEndAttempt(Throwable error) { + if (attemptSpan != null) { + attemptSpan.setAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, ObservabilityUtils.extractErrorType(error)); + + if (error != null) { + attemptSpan.setAttribute( + ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE, error.getClass().getName()); + + String errorMessage = extractErrorMessage(error); + if (errorMessage != null) { + attemptSpan.setAttribute(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, errorMessage); + } + } + + endAttempt(); + } + } + + private String extractErrorMessage(Throwable error) { + Throwable cause = error; + while (cause != null) { + if (cause.getMessage() != null && !cause.getMessage().isEmpty()) { + return cause.getMessage(); + } + cause = cause.getCause(); + } + return null; } private void endAttempt() { diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ErrorTypeUtilTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ErrorTypeUtilTest.java new file mode 100644 index 000000000000..a18a79626136 --- /dev/null +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/rpc/ErrorTypeUtilTest.java @@ -0,0 +1,196 @@ +/* + * 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.rpc; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.rpc.testing.FakeStatusCode; +import com.google.api.gax.tracing.ErrorTypeUtil; +import java.io.IOException; +import java.net.BindException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.NoRouteToHostException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import javax.net.ssl.SSLHandshakeException; +import org.junit.jupiter.api.Test; + +class ErrorTypeUtilTest { + + @Test + void testExtractErrorType_null() { + assertThat(ErrorTypeUtil.extractErrorType(null)) + .isEqualTo(ErrorTypeUtil.ErrorType.INTERNAL.toString()); + } + + @Test + void testExtractErrorType_apiException_noReason() { + ApiException exception = + new ApiException( + "fake_error", null, new FakeStatusCode(StatusCode.Code.INVALID_ARGUMENT), false); + assertThat(ErrorTypeUtil.extractErrorType(exception)) + .isEqualTo(StatusCode.Code.INVALID_ARGUMENT.toString()); + } + + @Test + void testExtractErrorType_realSocketTimeoutException() throws Exception { + try (ServerSocket serverSocket = new ServerSocket(0)) { + int port = serverSocket.getLocalPort(); + try (Socket clientSocket = new Socket()) { + clientSocket.connect(new InetSocketAddress("localhost", port), 1000); + clientSocket.setSoTimeout(10); // 10ms read timeout + clientSocket.getInputStream().read(); + org.junit.jupiter.api.Assertions.fail("Expected SocketTimeoutException"); + } catch (Exception e) { + assertThat(e).isInstanceOf(SocketTimeoutException.class); + assertThat(ErrorTypeUtil.extractErrorType(e)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_TIMEOUT.toString()); + } + } + } + + @Test + void testExtractErrorType_realConnectException() { + try { + try (ServerSocket tempServer = new ServerSocket(0)) { + int freePort = tempServer.getLocalPort(); + tempServer.close(); + new Socket("localhost", freePort); + } + org.junit.jupiter.api.Assertions.fail("Expected ConnectException"); + } catch (Exception e) { + assertThat(e).isInstanceOf(ConnectException.class); + assertThat(ErrorTypeUtil.extractErrorType(e)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + } + + @Test + void testExtractErrorType_realUnknownHostException() { + try { + new Socket("this.host.does.not.exist.invalid", 80); + org.junit.jupiter.api.Assertions.fail("Expected UnknownHostException"); + } catch (Exception e) { + assertThat(e).isInstanceOf(UnknownHostException.class); + assertThat(ErrorTypeUtil.extractErrorType(e)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + } + + @Test + void testExtractErrorType_realSSLHandshakeException() throws Exception { + // Emulating a reliable SSLHandshakeException (vs a generic SSLException) requires + // complex keystore setups which are brittle. We instantiate it directly here. + assertThat( + ErrorTypeUtil.extractErrorType(new SSLHandshakeException("Cert path building failed"))) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + + @Test + void testExtractErrorType_realBindException() throws Exception { + try (ServerSocket serverSocket1 = new ServerSocket(0)) { + int port = serverSocket1.getLocalPort(); + try (ServerSocket serverSocket2 = new ServerSocket(port)) { + org.junit.jupiter.api.Assertions.fail("Expected BindException"); + } catch (Exception e) { + assertThat(e).isInstanceOf(BindException.class); + assertThat(ErrorTypeUtil.extractErrorType(e)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + } + } + + @Test + void testExtractErrorType_clientTimeout_others() { + assertThat(ErrorTypeUtil.extractErrorType(new WatchdogTimeoutException("timeout", false))) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_TIMEOUT.toString()); + assertThat( + ErrorTypeUtil.extractErrorType( + new DeadlineExceededException( + "timeout", null, new FakeStatusCode(StatusCode.Code.DEADLINE_EXCEEDED), false))) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_TIMEOUT.toString()); + } + + @Test + void testExtractErrorType_clientAuthenticationError() { + assertThat(ErrorTypeUtil.extractErrorType(new GeneralSecurityException("auth fail"))) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_AUTHENTICATION_ERROR.toString()); + } + + @Test + void testExtractErrorType_clientRequestError() { + assertThat(ErrorTypeUtil.extractErrorType(new IllegalArgumentException())) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_REQUEST_ERROR.toString()); + } + + @Test + void testExtractErrorType_fallbackToSimpleName() { + assertThat(ErrorTypeUtil.extractErrorType(new NullPointerException())) + .isEqualTo("NullPointerException"); + assertThat(ErrorTypeUtil.extractErrorType(new IllegalStateException())) + .isEqualTo("IllegalStateException"); + } + + @Test + void testExtractErrorType_otherNetworkErrors() { + assertThat(ErrorTypeUtil.extractErrorType(new NoRouteToHostException())) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + + @Test + void testExtractErrorType_causeChainTraversal() { + Exception root = new ConnectException("refused"); + Exception wrapped = new IOException("io fail", root); + assertThat(ErrorTypeUtil.extractErrorType(wrapped)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + + @Test + void testExtractErrorType_unknownException() { + assertThat(ErrorTypeUtil.extractErrorType(new Exception("Unknown stuff"))) + .isEqualTo("Exception"); + } + + @Test + void testExtractErrorType_redirectFallback() { + assertThat(ErrorTypeUtil.extractErrorType(new Exception("redirect"))).isEqualTo("Exception"); + } + + @Test + void testExtractErrorType_unknownClassNameFallback() { + class UnknownClientException extends Exception {} + assertThat(ErrorTypeUtil.extractErrorType(new UnknownClientException())) + .isEqualTo("UnknownClientException"); + } +} 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 3e9fc53ce536..df264e1e07a6 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 @@ -35,11 +35,19 @@ 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 io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; +import java.net.ConnectException; +import java.net.SocketTimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -186,4 +194,230 @@ void testAttemptStarted_retryAttributes_http() { ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE), 5L); } + + @Test + void testAttemptFailed_errorInfoReason() { + spanTracer.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); + + spanTracer.attemptFailedRetriesExhausted(apiException); + + verify(span).setAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "RATE_LIMIT_EXCEEDED"); + verify(span).end(); + } + + @Test + void testAttemptFailed_specificServerErrorCodeGrpc() { + spanTracer.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); + + spanTracer.attemptFailedRetriesExhausted(apiException); + + verify(span).setAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "PERMISSION_DENIED"); + verify(span).end(); + } + + @Test + void testAttemptFailed_specificServerErrorCodeHttp() { + spanTracer.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); + + spanTracer.attemptFailedRetriesExhausted(apiException); + + verify(span).setAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "403"); + verify(span).end(); + } + + @Test + void testAttemptFailed_clientTimeout() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(new SocketTimeoutException()); + + verify(span) + .setAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ErrorTypeUtil.ErrorType.CLIENT_TIMEOUT.toString()); + verify(span).end(); + } + + @Test + void testAttemptFailed_clientConnectionError() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(new ConnectException("connection failed")); + + verify(span) + .setAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + verify(span).end(); + } + + @Test + void testAttemptFailed_clientRedirectError() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(new RedirectException("redirect failed")); + + verify(span).setAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "RedirectException"); + verify(span).end(); + } + + @Test + void testAttemptFailed_clientRequestError() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(new IllegalArgumentException()); + + verify(span) + .setAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ErrorTypeUtil.ErrorType.CLIENT_REQUEST_ERROR.toString()); + verify(span).end(); + } + + @Test + void testAttemptFailed_clientUnknownError() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(new UnknownClientException()); + + verify(span) + .setAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "UnknownClientException"); + verify(span).end(); + } + + @Test + void testAttemptFailed_languageSpecificFallback() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(new IllegalStateException("illegal state")); + + verify(span) + .setAttribute(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "IllegalStateException"); + verify(span).end(); + } + + @Test + void testAttemptFailed_internalFallback() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(new Throwable() {}); + + // For an anonymous inner class Throwable, getSimpleName() is empty string, which triggers the + // fallback + verify(span) + .setAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ErrorTypeUtil.ErrorType.INTERNAL.toString()); + verify(span).end(); + } + + @Test + void testAttemptFailed_internalFallback_nullError() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(null); + + // For an anonymous inner class Throwable, getSimpleName() is empty string, which triggers the + // fallback + verify(span) + .setAttribute( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, + ErrorTypeUtil.ErrorType.INTERNAL.toString()); + verify(span).end(); + } + + @Test + void testAttemptFailed_populatesExceptionTypeAndMessage() { + spanTracer.attemptStarted(new Object(), 1); + + spanTracer.attemptFailedRetriesExhausted(new IllegalStateException("custom error message")); + + verify(span) + .setAttribute( + ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE, "java.lang.IllegalStateException"); + verify(span) + .setAttribute(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, "custom error message"); + verify(span).end(); + } + + @Test + void testAttemptFailed_recursiveMessageSearch() { + spanTracer.attemptStarted(new Object(), 1); + + Throwable cause = new IllegalArgumentException("root cause message"); + Throwable wrapper = new IllegalStateException("", cause); + + spanTracer.attemptFailedRetriesExhausted(wrapper); + + verify(span) + .setAttribute( + ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE, "java.lang.IllegalStateException"); + verify(span) + .setAttribute(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, "root cause message"); + verify(span).end(); + } + + private static class RedirectException extends RuntimeException { + public RedirectException(String message) { + super(message); + } + } + + private static class UnknownClientException extends RuntimeException {} } diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelErrorType.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelErrorType.java new file mode 100644 index 000000000000..8eaf54fb1b28 --- /dev/null +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelErrorType.java @@ -0,0 +1,507 @@ +/* + * 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.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.api.gax.core.FixedCredentialsProvider; +import com.google.api.gax.httpjson.RestSerializationException; +import com.google.api.gax.rpc.DeadlineExceededException; +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.SpanTracerFactory; +import com.google.auth.Credentials; +import com.google.common.collect.ImmutableList; +import com.google.rpc.Status; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.EchoSettings; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import com.google.showcase.v1beta1.stub.EchoStubSettings; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ManagedChannelBuilder; +import io.grpc.MethodDescriptor; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.BindException; +import java.net.NoRouteToHostException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.nio.channels.UnresolvedAddressException; +import java.security.GeneralSecurityException; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import javax.net.ssl.SSLHandshakeException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ITOtelErrorType { + private InMemorySpanExporter spanExporter; + private OpenTelemetrySdk openTelemetrySdk; + + @BeforeEach + void setup() { + spanExporter = InMemorySpanExporter.create(); + + SdkTracerProvider tracerProvider = + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build(); + + openTelemetrySdk = + OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).buildAndRegisterGlobal(); + } + + @AfterEach + void tearDown() { + if (openTelemetrySdk != null) { + openTelemetrySdk.close(); + } + GlobalOpenTelemetry.resetForTest(); + } + + private void verifyErrorTypeAttribute(String expectedErrorType) { + List spans = spanExporter.getFinishedSpanItems(); + assertThat(spans).isNotEmpty(); + + SpanData errorSpan = + spans.stream() + .filter( + span -> + span.getAttributes() + .get( + AttributeKey.stringKey( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)) + != null) + .findFirst() + .orElseThrow(() -> new AssertionError("Span with error.type not found")); + + assertThat( + errorSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE))) + .isEqualTo(expectedErrorType); + } + + private EchoClient createInterceptorClient(Throwable toThrow) throws IOException { + SpanTracerFactory tracingFactory = + new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + + ClientInterceptor interceptor = + new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + if (toThrow instanceof RuntimeException) { + throw (RuntimeException) toThrow; + } else { + throw new RuntimeException(toThrow); + } + } + }; + + EchoSettings grpcEchoSettings = + EchoSettings.newBuilder() + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .setInterceptorProvider(() -> ImmutableList.of(interceptor)) + .build()) + .setEndpoint(TestClientInitializer.DEFAULT_GRPC_ENDPOINT) + .build(); + + EchoStubSettings.Builder echoStubSettingsBuilder = + (EchoStubSettings.Builder) grpcEchoSettings.getStubSettings().toBuilder(); + echoStubSettingsBuilder.setTracerFactory(tracingFactory); + + return EchoClient.create(echoStubSettingsBuilder.build().createStub()); + } + + @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)); + verifyErrorTypeAttribute("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)); + verifyErrorTypeAttribute("503"); + } + } + + @Test + void testTracing_clientConnectionError_ConnectException_grpc() throws Exception { + int port; + try (ServerSocket socket = new ServerSocket(0)) { + port = socket.getLocalPort(); + } + + SpanTracerFactory tracingFactory = + new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + EchoSettings grpcEchoSettings = + EchoSettings.newBuilder() + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint("localhost:" + port) + .build(); + + EchoStubSettings.Builder echoStubSettingsBuilder = + (EchoStubSettings.Builder) grpcEchoSettings.getStubSettings().toBuilder(); + echoStubSettingsBuilder.setTracerFactory(tracingFactory); + echoStubSettingsBuilder + .echoSettings() + .setRetrySettings( + echoStubSettingsBuilder.echoSettings().getRetrySettings().toBuilder() + .setMaxAttempts(1) + .build()); + + try (EchoClient client = EchoClient.create(echoStubSettingsBuilder.build().createStub())) { + assertThrows( + UnavailableException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_CONNECTION_ERROR"); + } + } + + @Test + void testTracing_clientConnectionError_UnknownHost_grpc() throws Exception { + SpanTracerFactory tracingFactory = + new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + EchoSettings grpcEchoSettings = + EchoSettings.newBuilder() + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint("this.is.a.bogus.host.name:7469") + .build(); + + EchoStubSettings.Builder echoStubSettingsBuilder = + (EchoStubSettings.Builder) grpcEchoSettings.getStubSettings().toBuilder(); + echoStubSettingsBuilder.setTracerFactory(tracingFactory); + echoStubSettingsBuilder + .echoSettings() + .setRetrySettings( + echoStubSettingsBuilder.echoSettings().getRetrySettings().toBuilder() + .setMaxAttempts(1) + .build()); + + try (EchoClient client = EchoClient.create(echoStubSettingsBuilder.build().createStub())) { + assertThrows( + UnavailableException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_CONNECTION_ERROR"); + } + } + + @Test + void testTracing_clientConnectionError_SSLHandshakeException_grpc() throws Exception { + try (EchoClient client = + createInterceptorClient(new SSLHandshakeException("Mock SSL failure"))) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_CONNECTION_ERROR"); + } + } + + @Test + void testTracing_clientConnectionError_UnresolvedAddressException_grpc() throws Exception { + try (EchoClient client = createInterceptorClient(new UnresolvedAddressException())) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_CONNECTION_ERROR"); + } + } + + @Test + void testTracing_clientConnectionError_NoRouteToHostException_grpc() throws Exception { + try (EchoClient client = createInterceptorClient(new NoRouteToHostException())) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_CONNECTION_ERROR"); + } + } + + @Test + void testTracing_clientConnectionError_BindException_grpc() throws Exception { + try (EchoClient client = createInterceptorClient(new BindException())) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_CONNECTION_ERROR"); + } + } + + @Test + void testTracing_clientTimeout_SocketTimeoutException_grpc() throws Exception { + try (EchoClient client = createInterceptorClient(new SocketTimeoutException())) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_TIMEOUT"); + } + } + + @Test + void testTracing_clientTimeout_DeadlineExceededException_grpc() throws Exception { + SpanTracerFactory tracingFactory = + new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + + try (ServerSocket serverSocket = new ServerSocket(0)) { + int port = serverSocket.getLocalPort(); + Thread serverThread = + new Thread( + () -> { + try { + try (Socket ignored = serverSocket.accept()) { + Thread.sleep(1000); + } + } catch (Exception ignored) { + } + }); + serverThread.start(); + + EchoSettings grpcEchoSettings = + EchoSettings.newBuilder() + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint("localhost:" + port) + .build(); + + EchoStubSettings.Builder echoStubSettingsBuilder = + (EchoStubSettings.Builder) grpcEchoSettings.getStubSettings().toBuilder(); + echoStubSettingsBuilder.setTracerFactory(tracingFactory); + echoStubSettingsBuilder + .echoSettings() + .setRetrySettings( + echoStubSettingsBuilder.echoSettings().getRetrySettings().toBuilder() + .setTotalTimeoutDuration(Duration.ofMillis(100)) + .setInitialRpcTimeoutDuration(Duration.ofMillis(100)) + .setMaxRpcTimeoutDuration(Duration.ofMillis(100)) + .setMaxAttempts(1) + .build()); + + try (EchoClient client = EchoClient.create(echoStubSettingsBuilder.build().createStub())) { + assertThrows( + DeadlineExceededException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_TIMEOUT"); + } finally { + serverThread.join(); + } + } + } + + @Test + void testTracing_clientAuthenticationError_GeneralSecurityException_grpc() throws Exception { + Credentials credentials = + new Credentials() { + @Override + public String getAuthenticationType() { + return "mock"; + } + + @Override + public Map> getRequestMetadata(URI uri) throws IOException { + throw new IOException("Mock auth failure", new GeneralSecurityException("Root cause")); + } + + @Override + public boolean hasRequestMetadata() { + return true; + } + + @Override + public boolean hasRequestMetadataOnly() { + return true; + } + + @Override + public void refresh() throws IOException {} + }; + + SpanTracerFactory tracingFactory = + new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + EchoSettings grpcEchoSettings = + EchoSettings.newBuilder() + .setCredentialsProvider(FixedCredentialsProvider.create(credentials)) + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint(TestClientInitializer.DEFAULT_GRPC_ENDPOINT) + .build(); + + EchoStubSettings.Builder echoStubSettingsBuilder = + (EchoStubSettings.Builder) grpcEchoSettings.getStubSettings().toBuilder(); + echoStubSettingsBuilder.setTracerFactory(tracingFactory); + + try (EchoClient client = EchoClient.create(echoStubSettingsBuilder.build().createStub())) { + assertThrows( + Exception.class, () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_AUTHENTICATION_ERROR"); + } + } + + @Test + void testTracing_clientAuthenticationError_FileNotFoundException_grpc() throws Exception { + try (EchoClient client = createInterceptorClient(new FileNotFoundException("Key not found"))) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + // Wrapping non-RuntimeExceptions in RuntimeException during interceptCall() + // means the simple class name of the exception being recorded is "RuntimeException" + verifyErrorTypeAttribute("RuntimeException"); + } + } + + @Test + void testTracing_clientRequestError_IllegalArgumentException_grpc() throws Exception { + try (EchoClient client = + createInterceptorClient(new IllegalArgumentException("Mock request error"))) { + assertThrows( + IllegalArgumentException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + verifyErrorTypeAttribute("CLIENT_REQUEST_ERROR"); + + SpanData errorSpan = + spanExporter.getFinishedSpanItems().stream() + .filter( + span -> + span.getAttributes() + .get( + AttributeKey.stringKey( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)) + != null) + .findFirst() + .orElseThrow(() -> new AssertionError("Span with error.type not found")); + + assertThat( + errorSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE))) + .isEqualTo("java.lang.IllegalArgumentException"); + assertThat( + errorSpan + .getAttributes() + .get(AttributeKey.stringKey(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE))) + .isEqualTo("Mock request error"); + } + } + + @Test + void testTracing_clientRedirectError_grpc() throws Exception { + try (EchoClient client = createInterceptorClient(new RuntimeException("Too many redirects"))) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + // Heuristic mapping of "redirect" in message has been removed. + // Expected result is now the simple class name of the exception. + verifyErrorTypeAttribute("RuntimeException"); + } + } + + @Test + void testTracing_clientUnknownError_grpc() throws Exception { + // Creating a custom exception class whose name contains "Unknown" + class MyUnknownException extends RuntimeException {} + try (EchoClient client = createInterceptorClient(new MyUnknownException())) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + // Heuristic mapping of "Unknown" in class name has been removed. + // Expected result is now the simple class name of the exception. + verifyErrorTypeAttribute("MyUnknownException"); + } + } + + @Test + void testTracing_clientRequestError_RestSerializationException_httpjson() throws Exception { + try (EchoClient client = + createInterceptorClient(new RestSerializationException("failed to serialize", null))) { + assertThrows( + RuntimeException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("test").build())); + // RestSerializationException is not handled due to ambiguity (serialization vs + // deserialization). + // Expected result is now its simple class name. + verifyErrorTypeAttribute("RestSerializationException"); + } + } +} From 678f80d0ba0c53837ef829245b50c68462a16eda Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 24 Mar 2026 16:38:48 -0400 Subject: [PATCH 2/5] docs: indicate return INTERNAL --- .../main/java/com/google/api/gax/tracing/ErrorTypeUtil.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java index 37af9331599e..c7a3c64979aa 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -61,11 +61,6 @@ public enum ErrorType { /** Placeholder for potential future unknown errors. */ CLIENT_UNKNOWN_ERROR, INTERNAL; - - @Override - public String toString() { - return name(); - } } private static final Set> AUTHENTICATION_EXCEPTION_CLASSES = From 3353528acf027971a2a3160253d62fef4568a9b9 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 24 Mar 2026 17:03:17 -0400 Subject: [PATCH 3/5] test: fix ITOtelErrorType compilation by replacing OpenTelemetryTraceManager --- .../showcase/v1beta1/it/ITOtelErrorType.java | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelErrorType.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelErrorType.java index 8eaf54fb1b28..afec93980d04 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelErrorType.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelErrorType.java @@ -39,7 +39,6 @@ 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.SpanTracerFactory; import com.google.auth.Credentials; import com.google.common.collect.ImmutableList; @@ -129,8 +128,7 @@ private void verifyErrorTypeAttribute(String expectedErrorType) { } private EchoClient createInterceptorClient(Throwable toThrow) throws IOException { - SpanTracerFactory tracingFactory = - new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); ClientInterceptor interceptor = new ClientInterceptor() { @@ -164,8 +162,7 @@ public ClientCall interceptCall( @Test void testTracing_failedEcho_grpc_recordsErrorType() throws Exception { - SpanTracerFactory tracingFactory = - new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); try (EchoClient client = TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) { @@ -182,8 +179,7 @@ void testTracing_failedEcho_grpc_recordsErrorType() throws Exception { @Test void testTracing_failedEcho_httpjson_recordsErrorType() throws Exception { - SpanTracerFactory tracingFactory = - new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); try (EchoClient client = TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) { @@ -205,8 +201,7 @@ void testTracing_clientConnectionError_ConnectException_grpc() throws Exception port = socket.getLocalPort(); } - SpanTracerFactory tracingFactory = - new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); EchoSettings grpcEchoSettings = EchoSettings.newBuilder() .setTransportChannelProvider( @@ -236,8 +231,7 @@ void testTracing_clientConnectionError_ConnectException_grpc() throws Exception @Test void testTracing_clientConnectionError_UnknownHost_grpc() throws Exception { - SpanTracerFactory tracingFactory = - new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); EchoSettings grpcEchoSettings = EchoSettings.newBuilder() .setTransportChannelProvider( @@ -318,8 +312,7 @@ void testTracing_clientTimeout_SocketTimeoutException_grpc() throws Exception { @Test void testTracing_clientTimeout_DeadlineExceededException_grpc() throws Exception { - SpanTracerFactory tracingFactory = - new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); try (ServerSocket serverSocket = new ServerSocket(0)) { int port = serverSocket.getLocalPort(); @@ -396,8 +389,7 @@ public boolean hasRequestMetadataOnly() { public void refresh() throws IOException {} }; - SpanTracerFactory tracingFactory = - new SpanTracerFactory(new OpenTelemetryTraceManager(openTelemetrySdk)); + SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk); EchoSettings grpcEchoSettings = EchoSettings.newBuilder() .setCredentialsProvider(FixedCredentialsProvider.create(credentials)) From 0ce71ddf8602272755df5485b7075d9c14ac5180 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 24 Mar 2026 17:15:08 -0400 Subject: [PATCH 4/5] build: add missing license headers to showcase tests --- .../v1beta1/it/ITEndpointContext.java | 30 +++++++++++++++++++ .../v1beta1/it/ITProtobuf3Compatibility.java | 30 +++++++++++++++++++ .../it/ITTimeObjectsPropagationTest.java | 30 +++++++++++++++++++ 3 files changed, 90 insertions(+) diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITEndpointContext.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITEndpointContext.java index 0f50051b68c4..08d7af542c0a 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITEndpointContext.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITEndpointContext.java @@ -1,3 +1,33 @@ +/* + * 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.showcase.v1beta1.it; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITProtobuf3Compatibility.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITProtobuf3Compatibility.java index 04582fe7fe6b..cfe12a30d24e 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITProtobuf3Compatibility.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITProtobuf3Compatibility.java @@ -1,3 +1,33 @@ +/* + * 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.showcase.v1beta1.it; import static com.google.common.truth.Truth.assertThat; diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITTimeObjectsPropagationTest.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITTimeObjectsPropagationTest.java index f7f801f627e5..36997d8dc78f 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITTimeObjectsPropagationTest.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITTimeObjectsPropagationTest.java @@ -1,3 +1,33 @@ +/* + * 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.showcase.v1beta1.it; import static org.junit.Assert.assertEquals; From 86d6cab7e11dfa4f9733e67cf500dc26fd2c0c59 Mon Sep 17 00:00:00 2001 From: Diego Date: Tue, 24 Mar 2026 17:17:25 -0400 Subject: [PATCH 5/5] Revert "build: add missing license headers to showcase tests" This reverts commit 0ce71ddf8602272755df5485b7075d9c14ac5180. --- .../v1beta1/it/ITEndpointContext.java | 30 ------------------- .../v1beta1/it/ITProtobuf3Compatibility.java | 30 ------------------- .../it/ITTimeObjectsPropagationTest.java | 30 ------------------- 3 files changed, 90 deletions(-) diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITEndpointContext.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITEndpointContext.java index 08d7af542c0a..0f50051b68c4 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITEndpointContext.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITEndpointContext.java @@ -1,33 +1,3 @@ -/* - * 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.showcase.v1beta1.it; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITProtobuf3Compatibility.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITProtobuf3Compatibility.java index cfe12a30d24e..04582fe7fe6b 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITProtobuf3Compatibility.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITProtobuf3Compatibility.java @@ -1,33 +1,3 @@ -/* - * 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.showcase.v1beta1.it; import static com.google.common.truth.Truth.assertThat; diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITTimeObjectsPropagationTest.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITTimeObjectsPropagationTest.java index 36997d8dc78f..f7f801f627e5 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITTimeObjectsPropagationTest.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITTimeObjectsPropagationTest.java @@ -1,33 +1,3 @@ -/* - * 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.showcase.v1beta1.it; import static org.junit.Assert.assertEquals;