diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpc.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpc.java index 81f8bd95b04d..7e68a36b9201 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpc.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpc.java @@ -1769,6 +1769,12 @@ private T executeWithSpan(Span span, SpanOperation operation) throws IOEx } try (Scope scope = span.makeCurrent()) { return operation.execute(span); + } catch (Exception e) { + if (!(e instanceof com.google.api.client.http.HttpResponseException)) { + com.google.cloud.bigquery.telemetry.HttpTracingRequestInitializer.addExceptionToSpan( + e, span); + } + throw e; } finally { span.end(); } diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/ErrorResponse.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/ErrorResponse.java new file mode 100644 index 000000000000..cae50ed472c9 --- /dev/null +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/ErrorResponse.java @@ -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; + } +} diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/ExceptionTypeUtil.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/ExceptionTypeUtil.java new file mode 100644 index 000000000000..758f91261396 --- /dev/null +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/ExceptionTypeUtil.java @@ -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; + } +} diff --git a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializer.java b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializer.java index 575e62a28ac2..f85e6bde7d22 100644 --- a/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializer.java +++ b/java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializer.java @@ -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 @@ -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 @@ -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 */ @@ -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(); + } + } } } diff --git a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpcTest.java b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpcTest.java index 161d4f030a64..be608f735287 100644 --- a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpcTest.java +++ b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpcTest.java @@ -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 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 diff --git a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializerTest.java b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializerTest.java index ea29c20f210f..4a7bd4891fbe 100644 --- a/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializerTest.java +++ b/java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/telemetry/HttpTracingRequestInitializerTest.java @@ -28,14 +28,17 @@ import com.google.api.client.http.HttpRequestFactory; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.LowLevelHttpRequest; import com.google.api.client.http.LowLevelHttpResponse; import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.api.client.testing.http.MockLowLevelHttpResponse; +import com.google.gson.JsonParseException; 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 io.opentelemetry.context.Scope; import io.opentelemetry.sdk.OpenTelemetrySdk; @@ -122,7 +125,8 @@ public void testNoSpanIsCreatedIfNoActiveSpan() throws IOException { new HttpTracingRequestInitializer(delegateInitializer, tracer); HttpTransport transport = createTransport(); - // close span before building the request so there is no current span during initialization + // close span before building the request so there is no current span during + // initialization spanScope.close(); parentSpan.end(); @@ -152,6 +156,488 @@ public void testDelegateInitializerIsCalled() throws IOException { verify(delegateInitializer, times(1)).initialize(any(HttpRequest.class)); } + @Test + public void testHttpResponseInterceptor_Success() throws IOException { + HttpTransport transport = createTransportWithResponse(200, "OK", "OK"); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + HttpResponse response = request.execute(); + response.disconnect(); + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "GET", span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD)); + assertEquals( + 200L, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE)); + assertEquals(StatusCode.OK, span.getStatus().getStatusCode()); + } + + @Test + public void testHttpUnsuccessfulResponseHandler_ErrorWithDetails() throws IOException { + String errorJson = + "{\n" + + " \"error\": {\n" + + " \"errors\": [\n" + + " {\n" + + " \"message\": \"Detailed error message\",\n" + + " \"reason\": \"DetailedReason\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + HttpTransport transport = createTransportWithResponse(400, errorJson, "Bad Request"); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (HttpResponseException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "GET", span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD)); + assertEquals( + 400L, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE)); + assertEquals( + "Detailed error message", span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + assertEquals("DetailedReason", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + } + + @Test + public void testHttpUnsuccessfulResponseHandler_ErrorWithoutDetails() throws IOException { + HttpTransport transport = + createTransportWithResponse(500, "Internal Server Error", "Internal Server Error"); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (HttpResponseException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "GET", span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD)); + assertEquals( + 500L, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE)); + assertEquals( + "Internal Server Error", span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + assertEquals("500", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + } + + @Test + public void testHttpUnsuccessfulResponseHandler_Exception() throws IOException { + HttpTransport transport = createTransportWithResponse(500, "Error", "Error"); + + // Create a delegate that sets an unsuccessful response handler that throws + // IOException + HttpRequestInitializer delegate = + req -> + req.setUnsuccessfulResponseHandler( + (request1, response, supportsRetry) -> { + throw new IOException("Original handler exception"); + }); + + initializer = new HttpTracingRequestInitializer(delegate, tracer); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (IOException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "IOException: Original handler exception", + span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + assertEquals( + "CLIENT_UNKNOWN_ERROR", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + "java.io.IOException", span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals(StatusCode.ERROR, span.getStatus().getStatusCode()); + } + + @Test + public void testTracerIsNull() throws IOException { + HttpRequestInitializer delegateInitializer = mock(HttpRequestInitializer.class); + HttpTracingRequestInitializer tracingInitializer = + new HttpTracingRequestInitializer(delegateInitializer, null); + + HttpTransport transport = createTransport(); + HttpRequest request = buildGetRequest(transport, tracingInitializer, BASE_URL); + + HttpResponse response = request.execute(); + response.disconnect(); + + verify(delegateInitializer, times(1)).initialize(any(HttpRequest.class)); + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + assertEquals(1, spans.size()); + SpanData span = spans.get(0); + assertNull(span.getAttributes().get(BigQueryTelemetryTracer.RPC_SYSTEM_NAME)); + } + + @Test + public void testHttpSuccessResponseInterceptor_Exception() throws IOException { + HttpTransport transport = createTransportWithResponse(200, "OK", "OK"); + + HttpRequestInitializer delegate = + req -> + req.setResponseInterceptor( + response -> { + throw new IOException("Original interceptor exception"); + }); + + initializer = new HttpTracingRequestInitializer(delegate, tracer); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (IOException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "IOException: Original interceptor exception", + span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + assertEquals( + "CLIENT_UNKNOWN_ERROR", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + "java.io.IOException", span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals(StatusCode.ERROR, span.getStatus().getStatusCode()); + } + + @Test + public void testHttpUnsuccessfulResponseHandler_ErrorMissingReason() throws IOException { + String errorJson = + "{\n" + + " \"error\": {\n" + + " \"errors\": [\n" + + " {\n" + + " \"message\": \"Detailed error message\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + HttpTransport transport = createTransportWithResponse(400, errorJson, "Bad Request"); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (HttpResponseException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "GET", span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD)); + assertEquals( + 400L, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE)); + assertEquals( + "Detailed error message", span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + assertEquals("400", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + } + + @Test + public void testHttpUnsuccessfulResponseHandler_ErrorMissingMessage() throws IOException { + String errorJson = + "{\n" + + " \"error\": {\n" + + " \"errors\": [\n" + + " {\n" + + " \"reason\": \"ERROR\"\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + HttpTransport transport = createTransportWithResponse(400, errorJson, "Bad Request"); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (HttpResponseException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "GET", span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD)); + assertEquals( + 400L, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE)); + assertEquals("Bad Request", span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + assertEquals("ERROR", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + } + + @Test + public void testHttpUnsuccessfulResponseHandler_ErrorMessageNoDetails() throws IOException { + HttpTransport transport = createTransportWithResponse(400, null, "Bad Request"); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (HttpResponseException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "GET", span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD)); + assertEquals( + 400L, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE)); + assertEquals("Bad Request", span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + assertEquals("400", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + } + + @Test + public void testHttpUnsuccessfulResponseHandler_ErrorNoMessageNoDetails() throws IOException { + HttpTransport transport = createTransportWithResponse(400, null, null); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (HttpResponseException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "GET", span.getAttributes().get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD)); + assertEquals( + 400, span.getAttributes().get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE)); + assertEquals("400", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertNull(span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + } + + // All tests below use the delegate to fake these exceptions + @Test + public void testHttpUnsuccessfulResponseHandler_SocketTimeoutException() throws IOException { + HttpTransport transport = createTransportWithResponse(500, "Error", "Error"); + HttpRequestInitializer delegate = + req -> + req.setUnsuccessfulResponseHandler( + (request1, response, supportsRetry) -> { + throw new java.net.SocketTimeoutException("Read timed out"); + }); + + initializer = new HttpTracingRequestInitializer(delegate, tracer); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (IOException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals("CLIENT_TIMEOUT", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + "java.net.SocketTimeoutException", + span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals( + "SocketTimeoutException: Read timed out", + span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + } + + @Test + public void testHttpUnsuccessfulResponseHandler_ConnectException() throws IOException { + HttpTransport transport = createTransportWithResponse(500, "Error", "Error"); + + HttpRequestInitializer delegate = + req -> + req.setUnsuccessfulResponseHandler( + (request1, response, supportsRetry) -> { + throw new java.net.ConnectException("Connection refused"); + }); + + initializer = new HttpTracingRequestInitializer(delegate, tracer); + HttpRequest request = buildGetRequest(transport, initializer, BASE_URL); + + try { + request.execute(); + } catch (IOException e) { + // Expected + } + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + verifyGeneralSpanData(span); + + assertEquals( + "CLIENT_CONNECTION_ERROR", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + "java.net.ConnectException", + span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals( + "ConnectException: Connection refused", + span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + } + + @Test + public void testAddExceptionToSpan_DecodeException() { + Exception e = new JsonParseException("parse error"); + HttpTracingRequestInitializer.addExceptionToSpan(e, parentSpan); + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + assertEquals( + "CLIENT_RESPONSE_DECODE_ERROR", + span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + e.getClass().getName(), span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals( + "JsonParseException: parse error", + span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + } + + @Test + public void testAddExceptionToSpan_RedirectException() { + Exception e = new Exception("Too many redirects"); + HttpTracingRequestInitializer.addExceptionToSpan(e, parentSpan); + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + assertEquals( + "CLIENT_REDIRECT_ERROR", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + e.getClass().getName(), span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals( + "Exception: Too many redirects", + span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + } + + @Test + public void testAddExceptionToSpan_AuthenticationException() { + // note com.google.auth.oauth2.GoogleAuthException is package private so recreating here + class GoogleAuthException extends Exception {} + Exception e = new GoogleAuthException(); + HttpTracingRequestInitializer.addExceptionToSpan(e, parentSpan); + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + assertEquals( + "CLIENT_AUTHENTICATION_ERROR", + span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + e.getClass().getName(), span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals( + "GoogleAuthException", span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + } + + @Test + public void testAddExceptionToSpan_IllegalArgumentException() { + Exception e = new IllegalArgumentException("missing field"); + HttpTracingRequestInitializer.addExceptionToSpan(e, parentSpan); + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + assertEquals( + "CLIENT_REQUEST_ERROR", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + e.getClass().getName(), span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals( + "IllegalArgumentException: missing field", + span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + } + + @Test + public void testAddExceptionToSpan_UnknownException() { + Exception e = new Exception("unknown"); + HttpTracingRequestInitializer.addExceptionToSpan(e, parentSpan); + + spanScope.close(); + parentSpan.end(); + + List spans = spanExporter.getFinishedSpanItems(); + SpanData span = spans.get(0); + assertEquals( + "CLIENT_UNKNOWN_ERROR", span.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE)); + assertEquals( + e.getClass().getName(), span.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE)); + assertEquals( + "Exception: unknown", span.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE)); + } + private static HttpTransport createTransport() { return new MockHttpTransport() { @Override @@ -193,4 +679,27 @@ private void verifyGeneralSpanData(SpanData span) { HttpTracingRequestInitializer.HTTP_RPC_SYSTEM_NAME, span.getAttributes().get(BigQueryTelemetryTracer.RPC_SYSTEM_NAME)); } + + private static HttpTransport createTransportWithResponse( + int statusCode, String content, String reasonPhrase) { + return new MockHttpTransport() { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) { + return new MockLowLevelHttpRequest() { + @Override + public LowLevelHttpResponse execute() { + MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); + response.setStatusCode(statusCode); + if (content != null) { + response.setContent(content); + } + if (reasonPhrase != null) { + response.setReasonPhrase(reasonPhrase); + } + return response; + } + }; + } + }; + } }