|
| 1 | +/* |
| 2 | + * Copyright 2026 Google LLC |
| 3 | + * |
| 4 | + * Redistribution and use in source and binary forms, with or without |
| 5 | + * modification, are permitted provided that the following conditions are |
| 6 | + * met: |
| 7 | + * |
| 8 | + * * Redistributions of source code must retain the above copyright |
| 9 | + * notice, this list of conditions and the following disclaimer. |
| 10 | + * * Redistributions in binary form must reproduce the above |
| 11 | + * copyright notice, this list of conditions and the following disclaimer |
| 12 | + * in the documentation and/or other materials provided with the |
| 13 | + * distribution. |
| 14 | + * * Neither the name of Google LLC nor the names of its |
| 15 | + * contributors may be used to endorse or promote products derived from |
| 16 | + * this software without specific prior written permission. |
| 17 | + * |
| 18 | + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| 19 | + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| 20 | + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| 21 | + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| 22 | + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| 23 | + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| 24 | + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| 25 | + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| 26 | + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 27 | + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 28 | + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 29 | + */ |
| 30 | +package com.google.api.gax.tracing; |
| 31 | + |
| 32 | +import com.google.api.gax.rpc.ApiException; |
| 33 | +import com.google.api.gax.rpc.WatchdogTimeoutException; |
| 34 | +import com.google.common.base.Strings; |
| 35 | +import java.net.ConnectException; |
| 36 | +import java.net.SocketTimeoutException; |
| 37 | +import java.net.UnknownHostException; |
| 38 | +import java.nio.channels.UnresolvedAddressException; |
| 39 | +import javax.annotation.Nullable; |
| 40 | +import javax.net.ssl.SSLHandshakeException; |
| 41 | + |
| 42 | +public class ErrorTypeUtil { |
| 43 | + |
| 44 | + enum ErrorType { |
| 45 | + CLIENT_TIMEOUT, |
| 46 | + CLIENT_CONNECTION_ERROR, |
| 47 | + CLIENT_REQUEST_ERROR, |
| 48 | + CLIENT_REQUEST_BODY_ERROR, |
| 49 | + CLIENT_RESPONSE_DECODE_ERROR, |
| 50 | + CLIENT_REDIRECT_ERROR, |
| 51 | + CLIENT_AUTHENTICATION_ERROR, |
| 52 | + CLIENT_UNKNOWN_ERROR, |
| 53 | + INTERNAL; |
| 54 | + |
| 55 | + @Override |
| 56 | + public String toString() { |
| 57 | + return name(); |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + /** |
| 62 | + * Extracts a low-cardinality string representing the specific classification of the error to be |
| 63 | + * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. |
| 64 | + * |
| 65 | + * <p>This value is determined based on the following priority: |
| 66 | + * |
| 67 | + * <ol> |
| 68 | + * <li><b>{@code google.rpc.ErrorInfo.reason}:</b> If the error response from the service |
| 69 | + * includes {@code google.rpc.ErrorInfo} details, the reason field (e.g., |
| 70 | + * "RATE_LIMIT_EXCEEDED", "SERVICE_DISABLED") will be used. This offers the most precise |
| 71 | + * error cause. |
| 72 | + * <li><b>Specific Server Error Code:</b> If no {@code ErrorInfo.reason} is available, but a |
| 73 | + * server error code was received: |
| 74 | + * <ul> |
| 75 | + * <li>For HTTP: The HTTP status code (e.g., "403", "503"). |
| 76 | + * <li>For gRPC: The gRPC status code name (e.g., "PERMISSION_DENIED", "UNAVAILABLE"). |
| 77 | + * </ul> |
| 78 | + * <li><b>Client-Side Network/Operational Errors:</b> For errors occurring within the client |
| 79 | + * library or network stack, mapping to specific enum representations from {@link |
| 80 | + * ErrorType}: |
| 81 | + * <ul> |
| 82 | + * <li>{@code CLIENT_TIMEOUT}: A client-configured timeout was reached. |
| 83 | + * <li>{@code CLIENT_CONNECTION_ERROR}: Failure to establish the network connection (DNS, |
| 84 | + * TCP, TLS). |
| 85 | + * <li>{@code CLIENT_REQUEST_ERROR}: Client-side issue forming or sending the request. |
| 86 | + * <li>{@code CLIENT_REQUEST_BODY_ERROR}: Error streaming the request body. |
| 87 | + * <li>{@code CLIENT_RESPONSE_DECODE_ERROR}: Client-side error decoding the response body. |
| 88 | + * <li>{@code CLIENT_REDIRECT_ERROR}: Problem handling HTTP redirects. |
| 89 | + * <li>{@code CLIENT_AUTHENTICATION_ERROR}: Error during credential acquisition or |
| 90 | + * application. |
| 91 | + * <li>{@code CLIENT_UNKNOWN_ERROR}: Other unclassified client-side network or protocol |
| 92 | + * errors. |
| 93 | + * </ul> |
| 94 | + * <li><b>Language-specific error type:</b> The class or struct name of the exception or error |
| 95 | + * if available. This must be low-cardinality, meaning it returns the short name of the |
| 96 | + * exception class (e.g. {@code "IllegalStateException"}) rather than its message. |
| 97 | + * <li><b>Internal Fallback:</b> If the error doesn't fit any of the above categories, {@code |
| 98 | + * "INTERNAL"} will be used, indicating an unexpected issue within the client library's own |
| 99 | + * logic. |
| 100 | + * </ol> |
| 101 | + * |
| 102 | + * @param error the Throwable from which to extract the error type string. |
| 103 | + * @return a low-cardinality string representing the specific error type, or {@code null} if the |
| 104 | + * provided error is {@code null}. |
| 105 | + */ |
| 106 | + public static String extractErrorType(@Nullable Throwable error) { |
| 107 | + if (error == null) { |
| 108 | + return null; |
| 109 | + } |
| 110 | + |
| 111 | + // 1. & 2. Extract error info reason or server status code |
| 112 | + if (error instanceof ApiException) { |
| 113 | + String errorType = extractFromApiException((ApiException) error); |
| 114 | + if (errorType != null) { |
| 115 | + return errorType; |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + // 3. Attempt client side error |
| 120 | + String clientError = getClientSideError(error); |
| 121 | + if (clientError != null) { |
| 122 | + return clientError; |
| 123 | + } |
| 124 | + |
| 125 | + // 4. Language-specific error type fallback |
| 126 | + String exceptionName = error.getClass().getSimpleName(); |
| 127 | + if (!Strings.isNullOrEmpty(exceptionName)) { |
| 128 | + return exceptionName; |
| 129 | + } |
| 130 | + |
| 131 | + // 5. Internal Fallback |
| 132 | + return ErrorType.INTERNAL.toString(); |
| 133 | + } |
| 134 | + |
| 135 | + @Nullable |
| 136 | + private static String extractFromApiException(ApiException apiException) { |
| 137 | + // 1. Check for ErrorInfo.reason |
| 138 | + String reason = apiException.getReason(); |
| 139 | + if (!Strings.isNullOrEmpty(reason)) { |
| 140 | + return reason; |
| 141 | + } |
| 142 | + |
| 143 | + // 2. Specific Server Error Code |
| 144 | + if (apiException.getStatusCode() != null) { |
| 145 | + Object transportCode = apiException.getStatusCode().getTransportCode(); |
| 146 | + if (transportCode instanceof Integer) { |
| 147 | + // HTTP Status Code |
| 148 | + return String.valueOf(transportCode); |
| 149 | + } else if (apiException.getStatusCode().getCode() != null) { |
| 150 | + // gRPC Status Code name |
| 151 | + return apiException.getStatusCode().getCode().name(); |
| 152 | + } |
| 153 | + } |
| 154 | + return null; |
| 155 | + } |
| 156 | + |
| 157 | + @Nullable |
| 158 | + private static String getClientSideError(Throwable error) { |
| 159 | + if (isClientTimeout(error)) { |
| 160 | + return ErrorType.CLIENT_TIMEOUT.toString(); |
| 161 | + } |
| 162 | + if (isClientConnectionError(error)) { |
| 163 | + return ErrorType.CLIENT_CONNECTION_ERROR.toString(); |
| 164 | + } |
| 165 | + if (isClientAuthenticationError(error)) { |
| 166 | + return ErrorType.CLIENT_AUTHENTICATION_ERROR.toString(); |
| 167 | + } |
| 168 | + if (isClientResponseDecodeError(error)) { |
| 169 | + return ErrorType.CLIENT_RESPONSE_DECODE_ERROR.toString(); |
| 170 | + } |
| 171 | + if (isClientRedirectError(error)) { |
| 172 | + return ErrorType.CLIENT_REDIRECT_ERROR.toString(); |
| 173 | + } |
| 174 | + if (error instanceof IllegalArgumentException) { // This covers CLIENT_REQUEST_ERROR |
| 175 | + return ErrorType.CLIENT_REQUEST_ERROR.toString(); |
| 176 | + } |
| 177 | + if (error.getClass().getSimpleName().contains("RequestBodyException")) { |
| 178 | + return ErrorType.CLIENT_REQUEST_BODY_ERROR.toString(); |
| 179 | + } |
| 180 | + if (error.getClass().getSimpleName().contains("UnknownClientException")) { |
| 181 | + return ErrorType.CLIENT_UNKNOWN_ERROR.toString(); |
| 182 | + } |
| 183 | + |
| 184 | + return null; |
| 185 | + } |
| 186 | + |
| 187 | + private static boolean isClientTimeout(Throwable e) { |
| 188 | + return e instanceof SocketTimeoutException || e instanceof WatchdogTimeoutException; |
| 189 | + } |
| 190 | + |
| 191 | + private static boolean isClientConnectionError(Throwable e) { |
| 192 | + return e instanceof ConnectException |
| 193 | + || e instanceof UnknownHostException |
| 194 | + || e instanceof SSLHandshakeException |
| 195 | + || e instanceof UnresolvedAddressException; |
| 196 | + } |
| 197 | + |
| 198 | + private static boolean isClientResponseDecodeError(Throwable e) { |
| 199 | + return e.getClass().getName().contains("Json") |
| 200 | + || e.getClass().getName().contains("Gson") |
| 201 | + || (e.getCause() != null && e.getCause().getClass().getName().contains("Gson")); |
| 202 | + } |
| 203 | + |
| 204 | + private static boolean isClientRedirectError(Throwable e) { |
| 205 | + return e.getMessage() != null && e.getMessage().contains("redirect"); |
| 206 | + } |
| 207 | + |
| 208 | + private static boolean isClientAuthenticationError(Throwable e) { |
| 209 | + return e.getClass().getName().contains("GoogleAuthException"); |
| 210 | + } |
| 211 | +} |
0 commit comments