Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,146 @@

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

/**
* 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, 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}:
* <ul>
* <li>{@code CLIENT_TIMEOUT}: A client-configured timeout was reached.
* <li>{@code CLIENT_CONNECTION_ERROR}: Failure to establish the network connection (DNS,
* TCP, TLS).
* <li>{@code CLIENT_REQUEST_ERROR}: Client-side issue forming or sending the request.
* <li>{@code CLIENT_REQUEST_BODY_ERROR}: Error streaming the request body.
* <li>{@code CLIENT_RESPONSE_DECODE_ERROR}: Client-side error decoding the response body.
* <li>{@code CLIENT_REDIRECT_ERROR}: Problem handling HTTP redirects.
* <li>{@code CLIENT_AUTHENTICATION_ERROR}: Error during credential acquisition or
* application.
* <li>{@code CLIENT_UNKNOWN_ERROR}: Other unclassified client-side network or protocol
* errors.
* </ul>
* <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 null} if the
* provided error is {@code null}.
*/
static String extractErrorType(@Nullable Throwable error) {

Check failure on line 104 in gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 25 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java&issues=AZz4dLIdM3PDanF2KNWG&open=AZz4dLIdM3PDanF2KNWG&pullRequest=4148

Check failure on line 104 in gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java

View check run for this annotation

SonarQubeCloud / [java_showcase_integration_tests] SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 25 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java_integration_tests&issues=AZz4eKtv_9QiEEJB32XJ&open=AZz4eKtv_9QiEEJB32XJ&pullRequest=4148
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()) {

Check warning on line 174 in gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java

View check run for this annotation

SonarQubeCloud / [gapic-generator-java-root] SonarCloud Code Analysis

Remove this expression which always evaluates to "true"

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java&issues=AZz4dLIdM3PDanF2KNWF&open=AZz4dLIdM3PDanF2KNWF&pullRequest=4148

Check warning on line 174 in gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java

View check run for this annotation

SonarQubeCloud / [java_showcase_integration_tests] SonarCloud Code Analysis

Remove this expression which always evaluates to "true"

See more on https://sonarcloud.io/project/issues?id=googleapis_gapic-generator-java_integration_tests&issues=AZz4eKtv_9QiEEJB32XI&open=AZz4eKtv_9QiEEJB32XI&pullRequest=4148
return exceptionName;
}
Comment on lines +174 to +176
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check exceptionName != null is redundant. Class.getSimpleName() never returns null; it returns an empty string for anonymous classes, which is already handled by !exceptionName.isEmpty(). You can simplify this condition.

Suggested change
if (exceptionName != null && !exceptionName.isEmpty()) {
return exceptionName;
}
if (!exceptionName.isEmpty()) {
return exceptionName;
}
References
  1. This comment aligns with the principle of avoiding redundant null checks, similar to how validation within ApiTracerContext should be relied upon to prevent unnecessary checks.


// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public interface TraceManager {
Span createSpan(String name, Map<String, Object> attributes);

interface Span {
void addAttribute(String key, String value);

void end();
}
}
Loading
Loading