Skip to content
Closed
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 @@ -1769,6 +1769,12 @@ private <T> T executeWithSpan(Span span, SpanOperation<T> operation) throws IOEx
}
try (Scope scope = span.makeCurrent()) {
return operation.execute(span);
} catch (Exception e) {
Copy link
Copy Markdown
Contributor Author

@ldetmer ldetmer Mar 17, 2026

Choose a reason for hiding this comment

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

sorry Jin Seop I know you requested this earlier, I just forgot about client side exceptions, so in the end I did need it!

if (!(e instanceof com.google.api.client.http.HttpResponseException)) {
com.google.cloud.bigquery.telemetry.HttpTracingRequestInitializer.addExceptionToSpan(
e, span);
}
throw e;
} finally {
span.end();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.cloud.bigquery.telemetry;

import com.google.api.client.googleapis.json.GoogleJsonError;
import com.google.api.client.json.GenericJson;
import com.google.api.client.util.Key;

/** Error response model for telemetry parsing. */
public class ErrorResponse extends GenericJson {

@Key private GoogleJsonError error;

public GoogleJsonError getError() {
return error;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.google.cloud.bigquery.telemetry;

/** Utility class for identifying exception types for telemetry tracking. */
public final class ExceptionTypeUtil {

private ExceptionTypeUtil() {
// Utility class, no instantiation
}

public static boolean isClientTimeout(Exception e) {
return e instanceof java.net.SocketTimeoutException;
}

public static boolean isClientConnectionError(Exception e) {
return e instanceof java.net.ConnectException
|| e instanceof java.net.UnknownHostException
|| e instanceof javax.net.ssl.SSLHandshakeException;
}

public static boolean isHttpResponseException(Exception e) {
return e instanceof com.google.api.client.http.HttpResponseException;
}

public static boolean isClientResponseDecodeError(Exception e) {
return e.getClass().getName().contains("Json")
|| e.getClass().getName().contains("Gson")
|| (e.getCause() != null && e.getCause().getClass().getName().contains("Gson"));
}

public static boolean isClientRedirectError(Exception e) {
return e.getMessage() != null && e.getMessage().contains("redirect");
}

public static boolean isClientAuthenticationError(Exception e) {
return e.getClass().getName().contains("GoogleAuthException");
}

public static boolean isClientRequestError(Exception e) {
return e instanceof java.lang.IllegalArgumentException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@

package com.google.cloud.bigquery.telemetry;

import com.google.api.client.googleapis.json.GoogleJsonError;
import com.google.api.client.http.*;
import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
import com.google.common.annotations.VisibleForTesting;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
* HttpRequestInitializer that wraps a delegate initializer, intercepts all HTTP requests, adds
Expand Down Expand Up @@ -62,13 +66,17 @@ public HttpTracingRequestInitializer(HttpRequestInitializer delegate, Tracer tra

@Override
public void initialize(HttpRequest request) throws IOException {

if (delegate != null) {

delegate.initialize(request);
}

if (tracer == null) {
return;
}
// Get the current active span (created by HttpBigQueryRpc) and add HTTP attributes to it
// Get the current active span (created by HttpBigQueryRpc) and add HTTP
// attributes to it
Span span = Span.current();
if (!span.getSpanContext().isValid()) {
// No active span to exists, skip instrumentation
Expand All @@ -77,6 +85,39 @@ public void initialize(HttpRequest request) throws IOException {
String host = request.getUrl().getHost();
int port = request.getUrl().getPort();
addInitialHttpAttributesToSpan(span, host, port);

HttpResponseInterceptor originalInterceptor = request.getResponseInterceptor();
request.setResponseInterceptor(
response -> {
addCommonResponseAttributesToSpan(
span, request.getRequestMethod(), response.getStatusCode());
try {
if (originalInterceptor != null) {
originalInterceptor.interceptResponse(response);
}
span.setStatus(StatusCode.OK);
} catch (IOException e) {
addExceptionToSpan(e, span);
throw e;
}
});

HttpUnsuccessfulResponseHandler originalHandler = request.getUnsuccessfulResponseHandler();
request.setUnsuccessfulResponseHandler(
(request1, response, supportsRetry) -> {
int statusCode = response.getStatusCode();
addCommonResponseAttributesToSpan(span, request1.getRequestMethod(), statusCode);
addErrorResponseToSpan(response, span, statusCode);
try {
if (originalHandler != null) {
return originalHandler.handleResponse(request1, response, supportsRetry);
}
return false;
} catch (IOException e) {
addExceptionToSpan(e, span);
throw e;
}
});
}

/** Add initial HTTP attributes to the existing active span */
Expand All @@ -87,6 +128,97 @@ private void addInitialHttpAttributesToSpan(Span span, String host, Integer port
if (port != null && port > 0) {
span.setAttribute(BigQueryTelemetryTracer.SERVER_PORT, port.longValue());
}
// TODO add full sanitized url, url domain, request method
}

private static void addCommonResponseAttributesToSpan(
Span span, String httpMethod, int statusCode) {
span.setAttribute(HTTP_REQUEST_METHOD, httpMethod);
span.setAttribute(HTTP_RESPONSE_STATUS_CODE, (long) statusCode);
}

public static void addExceptionToSpan(Exception e, Span span) {
span.recordException(e);
String message = e.getMessage();
String simpleName = e.getClass().getSimpleName();
if (simpleName.isEmpty()) {
simpleName = e.getClass().getName();
}
String statusMessage = simpleName + (message != null ? ": " + message : "");
span.setAttribute(BigQueryTelemetryTracer.EXCEPTION_TYPE, e.getClass().getName());
span.setAttribute(BigQueryTelemetryTracer.ERROR_TYPE, getErrorType(e));
span.setAttribute(BigQueryTelemetryTracer.STATUS_MESSAGE, statusMessage);
span.setStatus(StatusCode.ERROR, statusMessage);
}

// TODO: this logic should get migrated to gax when ready
private static String getErrorType(Exception e) {
if (ExceptionTypeUtil.isClientTimeout(e)) {
return "CLIENT_TIMEOUT";
} else if (ExceptionTypeUtil.isClientConnectionError(e)) {
return "CLIENT_CONNECTION_ERROR";
} else if (ExceptionTypeUtil.isHttpResponseException(e)) {
int statusCode = ((com.google.api.client.http.HttpResponseException) e).getStatusCode();
return Integer.toString(statusCode);
} else if (ExceptionTypeUtil.isClientResponseDecodeError(e)) {
return "CLIENT_RESPONSE_DECODE_ERROR";
} else if (ExceptionTypeUtil.isClientRedirectError(e)) {
return "CLIENT_REDIRECT_ERROR";
} else if (ExceptionTypeUtil.isClientAuthenticationError(e)) {
return "CLIENT_AUTHENTICATION_ERROR";
} else if (ExceptionTypeUtil.isClientRequestError(e)) {
return "CLIENT_REQUEST_ERROR";
}
return "CLIENT_UNKNOWN_ERROR";
}

// first set defaults from HttpResponse then try to parse additional error
// details from response
private static void addErrorResponseToSpan(HttpResponse response, Span span, int statusCode) {
try {
String statusMessage = response.getStatusMessage();
if (statusMessage != null && !statusMessage.isEmpty()) {
span.setAttribute(BigQueryTelemetryTracer.STATUS_MESSAGE, statusMessage);
}
} catch (Exception ex) {
// Ignore
}
span.setAttribute(BigQueryTelemetryTracer.ERROR_TYPE, Integer.toString(statusCode));
try {
retrieveErrorDetailsFromResponseObject(response, span, statusCode);
} catch (IOException e) {
// Ignore, will use defaults above
}
}

private static void retrieveErrorDetailsFromResponseObject(
HttpResponse response, Span span, int statusCode) throws IOException {
InputStream content = response.getContent();
if (content != null) {
if (!content.markSupported()) {
content = new BufferedInputStream(content);
}
content.mark(1024 * 1024); // Mark up to 1MB
try {
com.google.api.client.json.JsonParser parser =
com.google.api.client.json.gson.GsonFactory.getDefaultInstance()
.createJsonParser(content);
ErrorResponse errorResponse = parser.parse(ErrorResponse.class);
if (errorResponse != null
&& errorResponse.getError() != null
&& errorResponse.getError().getErrors() != null) {
GoogleJsonError.ErrorInfo errorInfo = errorResponse.getError().getErrors().get(0);
String message = errorInfo.getMessage();
if (message != null) {
span.setAttribute(BigQueryTelemetryTracer.STATUS_MESSAGE, message);
}
String reason = errorInfo.getReason();
if (reason != null) {
span.setAttribute(BigQueryTelemetryTracer.ERROR_TYPE, reason);
}
}
} finally {
content.reset();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,40 @@ public void testHttpTracingDisabledDoesNotAddAdditionalAttributes() throws Excep
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
}
}

@Test
public void testExecuteWithSpan_CatchesException() throws Exception {
mockResponse.setContent(
(String) null); // Triggers IOException("Simulated network error") inside execute()

try {
rpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
org.junit.jupiter.api.Assertions.fail("Expected IOException was not thrown");
} catch (IOException e) {
assertEquals("Simulated network error", e.getMessage());
}

List<SpanData> spans = spanExporter.getFinishedSpanItems();
assertThat(spans).isNotEmpty();
SpanData rpcSpan =
spans.stream()
.filter(
span -> span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.getDataset"))
.findFirst()
.orElse(null);
assertNotNull(rpcSpan);

assertEquals(
"CLIENT_UNKNOWN_ERROR",
rpcSpan
.getAttributes()
.get(com.google.cloud.bigquery.telemetry.BigQueryTelemetryTracer.ERROR_TYPE));
assertEquals(
"java.io.IOException",
rpcSpan
.getAttributes()
.get(com.google.cloud.bigquery.telemetry.BigQueryTelemetryTracer.EXCEPTION_TYPE));
}
}

@Nested
Expand Down
Loading
Loading