Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e9382ee
feat: Refine tracing telemetry for client-side attributes
diegomarquezp Mar 24, 2026
678f80d
docs: indicate return INTERNAL
diegomarquezp Mar 24, 2026
3353528
test: fix ITOtelErrorType compilation by replacing OpenTelemetryTrace…
diegomarquezp Mar 24, 2026
0ce71dd
build: add missing license headers to showcase tests
diegomarquezp Mar 24, 2026
86d6cab
Revert "build: add missing license headers to showcase tests"
diegomarquezp Mar 24, 2026
fe5ee04
docs(o11y): fix Javadoc for extractErrorType
diegomarquezp Mar 26, 2026
fc1ebb2
chore: format
diegomarquezp Mar 26, 2026
318d0ab
Merge branch 'main' into observability/tracing-attr-error-type-transfer
diegomarquezp Mar 26, 2026
38ac1e6
fix: remove unrelated change
diegomarquezp Mar 26, 2026
446bb56
test: fix unavailable exception
diegomarquezp Mar 26, 2026
a6e6cad
Merge remote-tracking branch 'origin/main' into observability/tracing…
diegomarquezp Mar 26, 2026
fe20c3d
chore: Resolve fully qualified class names in ITOtelErrorType.java
diegomarquezp Mar 26, 2026
f45a32e
test: add timeouts to ITOtelErrorType.java
diegomarquezp Mar 26, 2026
11b0541
chore: Add class-level timeout to ITOtelErrorType.java
diegomarquezp Mar 26, 2026
2b92891
fix(showcase): use NoCredentialsProvider in ITOtelErrorType tests to …
diegomarquezp Mar 26, 2026
9c16df5
fix(showcase): use NoCredentialsProvider in remaining ITOtelErrorType…
diegomarquezp Mar 26, 2026
b149819
docs: correct error type logic comment
diegomarquezp Mar 27, 2026
4f6ff6e
test: revert unnecessary change
diegomarquezp Mar 27, 2026
d6b562b
Revert "test: revert unnecessary change"
diegomarquezp Mar 27, 2026
2ba70e2
test: do not use mocks
diegomarquezp Mar 27, 2026
d1470dd
Merge branch 'main' into observability/tracing-attr-error-type-transfer
diegomarquezp Mar 27, 2026
d7a8fef
Simplify error classification and merge observability integration tests
diegomarquezp Mar 27, 2026
1610b38
fix: remove recursive error logic in SpanTracer
diegomarquezp Mar 28, 2026
09bcc2d
fix: make ErrorType enum package private
diegomarquezp Mar 28, 2026
b80757b
fix: simplify logic
diegomarquezp Mar 28, 2026
2bf5470
test: move ErroTypeUtilTest to correct package
diegomarquezp Mar 28, 2026
c117355
docs: remove recursive approach from comments
diegomarquezp Mar 30, 2026
f2884dc
Merge branch 'main' into observability/tracing-attr-error-type-transfer
diegomarquezp Mar 30, 2026
57859d3
Merge remote-tracking branch 'origin/main' into observability/tracing…
diegomarquezp Mar 30, 2026
793b4cb
fix: remove duplicate error type attribute
diegomarquezp Mar 30, 2026
1fa2a55
Merge branch 'observability/tracing-attr-error-type-transfer' of http…
diegomarquezp Mar 30, 2026
fa84faa
chore: reformat
diegomarquezp Mar 30, 2026
94703eb
docs: fix order of occurence of error parsing
diegomarquezp Mar 30, 2026
e665b6e
fix: return null when error type is null
diegomarquezp Mar 30, 2026
537cd7e
refactor: use better name for known error extraction
diegomarquezp Mar 30, 2026
7b27f30
Revert "fix: return null when error type is null"
diegomarquezp Mar 30, 2026
51bd277
fix: remove unwanted file
diegomarquezp Mar 30, 2026
172f94a
fix: remove unused function
diegomarquezp Mar 31, 2026
c370890
docs: improve documentation of getRealCause()
diegomarquezp Mar 31, 2026
0687be0
chore: format
diegomarquezp Mar 31, 2026
3485f31
test: remove unnecessary error setting
diegomarquezp Mar 31, 2026
eceb3ef
test: update unused tests
diegomarquezp Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/*
* 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 com.google.common.util.concurrent.UncheckedExecutionException;
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 {

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;
}

private static final Set<Class<? extends Throwable>> AUTHENTICATION_EXCEPTION_CLASSES =
ImmutableSet.of(GeneralSecurityException.class);

private static final Set<Class<? extends Throwable>> CLIENT_TIMEOUT_EXCEPTION_CLASSES =
ImmutableSet.of(
SocketTimeoutException.class,
WatchdogTimeoutException.class,
DeadlineExceededException.class);

private static final Set<Class<? extends Throwable>> 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.
*
* <p>This value is determined based on the following priority:
*
* <ol>
* <li><b>{@code google.rpc.ErrorInfo.reason}:</b> 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.
* <li><b>Specific Server Error Code:</b> If no {@code ErrorInfo.reason} is available and it is
* not a client-side failure, but a server error code was received:
* <ul>
* <li>For HTTP: The HTTP status code (e.g., "403", "503").
* <li>For gRPC: The gRPC status code name (e.g., "PERMISSION_DENIED", "UNAVAILABLE").
* </ul>
* <li><b>Client-Side Network/Operational Errors:</b> For errors occurring within the client
* library or network stack, mapping to specific enum representations from {@link
* ErrorType}. This includes checking the exception for diagnostic markers (e.g., {@code
* ConnectException} or {@code SocketTimeoutException}).
* <li><b>Language-specific error type:</b> 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.
* <li><b>Internal Fallback:</b> 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.
* </ol>
*
* @param error the Throwable from which to extract the error type string.
* @return a low-cardinality string representing the specific error type, or {@code
* ErrorType.INTERNAL.toString()} if the provided error is {@code null} or non-determined.
*/
// Requirement source: go/clo:product-requirements-v1
public static String extractErrorType(@Nullable Throwable error) {
if (error == null) {
// No information about the error; we default to INTERNAL.
return ErrorType.INTERNAL.toString();
}

// 1. Unwrap standard wrapper exceptions if present
Throwable realError = getRealCause(error);

// 2. Attempt to extract specific error type from the main exception
String specificError = extractKnownErrorType(realError);
if (specificError != null) {
return specificError;
}

// 3. Language-specific error type fallback
String exceptionName = realError.getClass().getSimpleName();
if (!Strings.isNullOrEmpty(exceptionName)) {
return exceptionName;
}

// 4. Internal Fallback
return ErrorType.INTERNAL.toString();
}

/**
* Unwraps standard execution wrappers to find the real cause of the failure.
*
* <p>This method specifically unwraps:
*
* <ul>
* <li>{@link com.google.common.util.concurrent.UncheckedExecutionException}: This is an
* unchecked exception often thrown by {@code ApiExceptions.callAndTranslateApiException} or
* {@code ServerStreamIterator} when a checked exception or error occurs.
* </ul>
*
* @param t the Throwable to unwrap.
* @return the cause of the exception if it is an instance of {@link UncheckedExecutionException}
* and has a cause; otherwise, the throwable itself.
*/
private static Throwable getRealCause(Throwable t) {
if (t.getCause() == null || !(t instanceof UncheckedExecutionException)) {
return t;
}
return t.getCause();
}

/**
* Attempts to extract a specific error type (reason, code, or client error) but returns null if
* it cannot be specifically classified.
*/
@Nullable
private static String extractKnownErrorType(Throwable error) {
// 1. Extract error info reason
if (error instanceof ApiException) {
String reason = ((ApiException) error).getReason();
if (!Strings.isNullOrEmpty(reason)) {
return reason;
}
}

// 2. Extract server status code (swapped order)
if (error instanceof ApiException) {
String errorCode = extractServerErrorCode((ApiException) error);
if (errorCode != null) {
return errorCode;
}
}

// 3. Attempt client side error
String clientError = getClientSideError(error);
if (clientError != null) {
return clientError;
}

return null;
}

/**
* 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 != null) {
return String.valueOf(transportCode);
}
}
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 hasErrorClass(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 hasErrorClass(e, CLIENT_CONNECTION_EXCEPTIONS);
}

private static boolean isClientAuthenticationError(Throwable e) {
return hasErrorClass(e, AUTHENTICATION_EXCEPTION_CLASSES);
}

/**
* Checks if the throwable is an instance of 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 the error is an instance of a class from the set, false otherwise.
*/
private static boolean hasErrorClass(Throwable t, Set<Class<? extends Throwable>> errorClasses) {
for (Class<? extends Throwable> errorClass : errorClasses) {
if (errorClass.isInstance(t)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ public class ObservabilityAttributes {
/** The url template of the request (e.g. /v1/{name}:access). */
public static final String URL_TEMPLATE_ATTRIBUTE = "url.template";

/** 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";

/** Size of the response body in bytes. */
public static final String HTTP_RESPONSE_BODY_SIZE = "http.response.body.size";

Expand All @@ -100,7 +106,10 @@ public class ObservabilityAttributes {
/** The full URL of the HTTP request, with sensitive query parameters redacted. */
public static final String HTTP_URL_FULL_ATTRIBUTE = "url.full";

/** The type of error that occurred (e.g., from google.rpc.ErrorInfo.reason). */
/**
* * 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";

/** The domain of the error (e.g., from google.rpc.ErrorInfo.domain). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@

final class ObservabilityUtils {

private 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);
}

/** Constant for redacted values. */
private static final String REDACTED_VALUE = "REDACTED";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

package com.google.api.gax.tracing;

import com.google.api.client.util.Strings;
import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
import io.opentelemetry.api.trace.Span;
Expand Down Expand Up @@ -185,16 +186,40 @@ 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) {
recordErrorAndEndAttempt(error);
}

private void recordErrorAndEndAttempt(Throwable error) {
if (attemptSpan == null) {
return;
}

attemptSpan.setAttribute(
ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, ObservabilityUtils.extractErrorType(error));

if (error == null) {
endAttempt();
return;
}

attemptSpan.setAttribute(
ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE, error.getClass().getName());

if (!Strings.isNullOrEmpty(error.getMessage())) {
attemptSpan.setAttribute(
ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, error.getMessage());
}

endAttempt();
}

Expand Down
Loading
Loading