Skip to content

Commit e9382ee

Browse files
committed
feat: Refine tracing telemetry for client-side attributes
1 parent 9d8fc97 commit e9382ee

File tree

8 files changed

+1253
-3
lines changed

8 files changed

+1253
-3
lines changed

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,16 @@ default void responseReceived() {}
183183
default void requestSent() {}
184184
;
185185

186+
/**
187+
* Adds an annotation that a streaming request has been sent.
188+
*
189+
* @param requestSize the size of the request in bytes.
190+
*/
191+
default void requestSent(long requestSize) {
192+
requestSent();
193+
}
194+
;
195+
186196
/**
187197
* Adds an annotation that a batch of writes has been flushed.
188198
*
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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.DeadlineExceededException;
34+
import com.google.api.gax.rpc.WatchdogTimeoutException;
35+
import com.google.common.base.Strings;
36+
import com.google.common.collect.ImmutableSet;
37+
import java.net.BindException;
38+
import java.net.ConnectException;
39+
import java.net.NoRouteToHostException;
40+
import java.net.SocketTimeoutException;
41+
import java.net.UnknownHostException;
42+
import java.nio.channels.UnresolvedAddressException;
43+
import java.security.GeneralSecurityException;
44+
import java.util.Set;
45+
import javax.annotation.Nullable;
46+
import javax.net.ssl.SSLHandshakeException;
47+
48+
public class ErrorTypeUtil {
49+
50+
public enum ErrorType {
51+
CLIENT_TIMEOUT,
52+
CLIENT_CONNECTION_ERROR,
53+
CLIENT_REQUEST_ERROR,
54+
/** Placeholder for potential future request body errors. */
55+
CLIENT_REQUEST_BODY_ERROR,
56+
/** Placeholder for potential future response decode errors. */
57+
CLIENT_RESPONSE_DECODE_ERROR,
58+
/** Placeholder for potential future redirect errors. */
59+
CLIENT_REDIRECT_ERROR,
60+
CLIENT_AUTHENTICATION_ERROR,
61+
/** Placeholder for potential future unknown errors. */
62+
CLIENT_UNKNOWN_ERROR,
63+
INTERNAL;
64+
65+
@Override
66+
public String toString() {
67+
return name();
68+
}
69+
}
70+
71+
private static final Set<Class<? extends Throwable>> AUTHENTICATION_EXCEPTION_CLASSES =
72+
ImmutableSet.of(GeneralSecurityException.class);
73+
74+
private static final Set<Class<? extends Throwable>> CLIENT_TIMEOUT_EXCEPTION_CLASSES =
75+
ImmutableSet.of(
76+
SocketTimeoutException.class,
77+
WatchdogTimeoutException.class,
78+
DeadlineExceededException.class);
79+
80+
private static final Set<Class<? extends Throwable>> CLIENT_CONNECTION_EXCEPTIONS =
81+
ImmutableSet.of(
82+
ConnectException.class,
83+
UnknownHostException.class,
84+
SSLHandshakeException.class,
85+
UnresolvedAddressException.class,
86+
NoRouteToHostException.class,
87+
BindException.class);
88+
89+
/**
90+
* Extracts a low-cardinality string representing the specific classification of the error to be
91+
* used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute.
92+
*
93+
* <p>This value is determined based on the following priority:
94+
*
95+
* <ol>
96+
* <li><b>{@code google.rpc.ErrorInfo.reason}:</b> If the error response from the service
97+
* includes {@code google.rpc.ErrorInfo} details, the reason field (e.g.,
98+
* "RATE_LIMIT_EXCEEDED", "SERVICE_DISABLED") will be used. This offers the most precise
99+
* error cause.
100+
* <li><b>Client-Side Network/Operational Errors:</b> For errors occurring within the client
101+
* library or network stack, mapping to specific enum representations from {@link
102+
* ErrorType}. This includes checking the cause chain for diagnostic markers (e.g., {@code
103+
* ConnectException} or {@code SocketTimeoutException}).
104+
* <li><b>Specific Server Error Code:</b> If no {@code ErrorInfo.reason} is available and it is
105+
* not a client-side failure, but a server error code was received:
106+
* <ul>
107+
* <li>For HTTP: The HTTP status code (e.g., "403", "503").
108+
* <li>For gRPC: The gRPC status code name (e.g., "PERMISSION_DENIED", "UNAVAILABLE").
109+
* </ul>
110+
* <li><b>Language-specific error type:</b> The class or struct name of the exception or error
111+
* if available. This must be low-cardinality, meaning it returns the short name of the
112+
* exception class (e.g. {@code "IllegalStateException"}) rather than its message.
113+
* <li><b>Internal Fallback:</b> If the error doesn't fit any of the above categories, {@code
114+
* "INTERNAL"} will be used, indicating an unexpected issue within the client library's own
115+
* logic.
116+
* </ol>
117+
*
118+
* @param error the Throwable from which to extract the error type string.
119+
* @return a low-cardinality string representing the specific error type, or {@code null} if the
120+
* provided error is {@code null}.
121+
*/
122+
public static String extractErrorType(@Nullable Throwable error) {
123+
if (error == null) {
124+
// No information about the error; we default to INTERNAL.
125+
return ErrorType.INTERNAL.toString();
126+
}
127+
128+
// 1. Extract error info reason (most specific server-side info)
129+
if (error instanceof ApiException) {
130+
String reason = ((ApiException) error).getReason();
131+
if (!Strings.isNullOrEmpty(reason)) {
132+
return reason;
133+
}
134+
}
135+
136+
// 2. Attempt client side error (includes checking cause chains)
137+
String clientError = getClientSideError(error);
138+
if (clientError != null) {
139+
return clientError;
140+
}
141+
142+
// 3. Extract server status code if available
143+
if (error instanceof ApiException) {
144+
String errorCode = extractServerErrorCode((ApiException) error);
145+
if (errorCode != null) {
146+
return errorCode;
147+
}
148+
}
149+
150+
// 4. Language-specific error type fallback
151+
String exceptionName = error.getClass().getSimpleName();
152+
if (!Strings.isNullOrEmpty(exceptionName)) {
153+
return exceptionName;
154+
}
155+
156+
// 5. Internal Fallback
157+
return ErrorType.INTERNAL.toString();
158+
}
159+
160+
/**
161+
* Extracts the server error code from an ApiException.
162+
*
163+
* @param apiException The ApiException to extract the error code from.
164+
* @return A string representing the error code, or null if no specific code can be determined.
165+
*/
166+
@Nullable
167+
private static String extractServerErrorCode(ApiException apiException) {
168+
if (apiException.getStatusCode() != null) {
169+
Object transportCode = apiException.getStatusCode().getTransportCode();
170+
if (transportCode instanceof Integer) {
171+
// HTTP Status Code
172+
return String.valueOf(transportCode);
173+
} else if (apiException.getStatusCode().getCode() != null) {
174+
// gRPC Status Code name
175+
return apiException.getStatusCode().getCode().name();
176+
}
177+
}
178+
return null;
179+
}
180+
181+
/**
182+
* Determines the client-side error type based on the provided Throwable. This method checks for
183+
* various network and client-specific exceptions.
184+
*
185+
* @param error The Throwable to analyze.
186+
* @return A string representing the client-side error type, or null if not matched.
187+
*/
188+
@Nullable
189+
private static String getClientSideError(Throwable error) {
190+
if (isClientTimeout(error)) {
191+
return ErrorType.CLIENT_TIMEOUT.toString();
192+
}
193+
if (isClientConnectionError(error)) {
194+
return ErrorType.CLIENT_CONNECTION_ERROR.toString();
195+
}
196+
if (isClientAuthenticationError(error)) {
197+
return ErrorType.CLIENT_AUTHENTICATION_ERROR.toString();
198+
}
199+
// This covers CLIENT_REQUEST_ERROR for general illegal arguments in client requests.
200+
if (error instanceof IllegalArgumentException) {
201+
return ErrorType.CLIENT_REQUEST_ERROR.toString();
202+
}
203+
return null;
204+
}
205+
206+
/**
207+
* Checks if the given Throwable represents a client-side timeout error. This includes socket
208+
* timeouts and GAX-specific watchdog timeouts.
209+
*
210+
* @param e The Throwable to check.
211+
* @return true if the error is a client timeout, false otherwise.
212+
*/
213+
private static boolean isClientTimeout(Throwable e) {
214+
return hasErrorClassInCauseChain(e, CLIENT_TIMEOUT_EXCEPTION_CLASSES);
215+
}
216+
217+
/**
218+
* Checks if the given Throwable represents a client-side connection error. This includes issues
219+
* with establishing connections, unknown hosts, SSL handshakes, and unresolved addresses.
220+
*
221+
* @param e The Throwable to check.
222+
* @return true if the error is a client connection error, false otherwise.
223+
*/
224+
private static boolean isClientConnectionError(Throwable e) {
225+
return hasErrorClassInCauseChain(e, CLIENT_CONNECTION_EXCEPTIONS);
226+
}
227+
228+
private static boolean isClientAuthenticationError(Throwable e) {
229+
return hasErrorClassInCauseChain(e, AUTHENTICATION_EXCEPTION_CLASSES);
230+
}
231+
232+
/**
233+
* Recursively checks the throwable and its cause chain for any of the specified error classes.
234+
*
235+
* @param t The Throwable to check.
236+
* @param errorClasses A set of class objects to check against.
237+
* @return true if an error from the set is found in the cause chain, false otherwise.
238+
*/
239+
private static boolean hasErrorClassInCauseChain(
240+
Throwable t, Set<Class<? extends Throwable>> errorClasses) {
241+
Throwable current = t;
242+
while (current != null) {
243+
for (Class<? extends Throwable> errorClass : errorClasses) {
244+
if (errorClass.isInstance(current)) {
245+
return true;
246+
}
247+
}
248+
current = current.getCause();
249+
}
250+
return false;
251+
}
252+
}

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ public class ObservabilityAttributes {
8585
/** The url template of the request (e.g. /v1/{name}:access). */
8686
public static final String URL_TEMPLATE_ATTRIBUTE = "url.template";
8787

88+
/**
89+
* The specific error type. Value will be google.rpc.ErrorInfo.reason, a specific Server Error
90+
* Code, Client-Side Network/Operational Error (e.g., CLIENT_TIMEOUT) or internal fallback.
91+
*/
92+
public static final String ERROR_TYPE_ATTRIBUTE = "error.type";
93+
94+
/** A human-readable error message, which may include details from the exception or response. */
95+
public static final String STATUS_MESSAGE_ATTRIBUTE = "status.message";
96+
97+
/** If the error was caused by an exception, the exception class name. */
98+
public static final String EXCEPTION_TYPE_ATTRIBUTE = "exception.type";
99+
88100
/** The resend count of the request. Only used in HTTP transport. */
89101
public static final String HTTP_RESEND_COUNT_ATTRIBUTE = "http.request.resend_count";
90102

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939

4040
class ObservabilityUtils {
4141

42+
/**
43+
* Extracts a low-cardinality string representing the specific classification of the error to be
44+
* used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. See {@link
45+
* ErrorTypeUtil#extractErrorType} for extended documentation.
46+
*/
47+
static String extractErrorType(@Nullable Throwable error) {
48+
return ErrorTypeUtil.extractErrorType(error);
49+
}
50+
4251
/** Function to extract the status of the error as a string */
4352
static String extractStatus(@Nullable Throwable error) {
4453
final String statusString;

sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,47 @@ public void attemptCancelled() {
141141

142142
@Override
143143
public void attemptFailedDuration(Throwable error, java.time.Duration delay) {
144-
endAttempt();
144+
recordErrorAndEndAttempt(error);
145145
}
146146

147147
@Override
148148
public void attemptFailedRetriesExhausted(Throwable error) {
149-
endAttempt();
149+
recordErrorAndEndAttempt(error);
150150
}
151151

152152
@Override
153153
public void attemptPermanentFailure(Throwable error) {
154-
endAttempt();
154+
recordErrorAndEndAttempt(error);
155+
}
156+
157+
private void recordErrorAndEndAttempt(Throwable error) {
158+
if (attemptSpan != null) {
159+
attemptSpan.setAttribute(
160+
ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, ObservabilityUtils.extractErrorType(error));
161+
162+
if (error != null) {
163+
attemptSpan.setAttribute(
164+
ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE, error.getClass().getName());
165+
166+
String errorMessage = extractErrorMessage(error);
167+
if (errorMessage != null) {
168+
attemptSpan.setAttribute(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, errorMessage);
169+
}
170+
}
171+
172+
endAttempt();
173+
}
174+
}
175+
176+
private String extractErrorMessage(Throwable error) {
177+
Throwable cause = error;
178+
while (cause != null) {
179+
if (cause.getMessage() != null && !cause.getMessage().isEmpty()) {
180+
return cause.getMessage();
181+
}
182+
cause = cause.getCause();
183+
}
184+
return null;
155185
}
156186

157187
private void endAttempt() {

0 commit comments

Comments
 (0)