Skip to content

Commit bfcad8b

Browse files
committed
refactor: put error type logic in new class
1 parent b215900 commit bfcad8b

3 files changed

Lines changed: 235 additions & 180 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
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

Comments
 (0)