Skip to content

Commit 4baf47e

Browse files
committed
feat(bigquery): added error attributes to span tracing
1 parent c4cabde commit 4baf47e

5 files changed

Lines changed: 465 additions & 14 deletions

File tree

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpc.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static java.net.HttpURLConnection.HTTP_OK;
2222
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
2323

24+
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
2425
import com.google.api.client.http.ByteArrayContent;
2526
import com.google.api.client.http.GenericUrl;
2627
import com.google.api.client.http.HttpRequest;
@@ -65,6 +66,7 @@
6566
import com.google.cloud.Tuple;
6667
import com.google.cloud.bigquery.BigQueryException;
6768
import com.google.cloud.bigquery.BigQueryOptions;
69+
import com.google.cloud.bigquery.telemetry.BigQueryTelemetryTracer;
6870
import com.google.cloud.bigquery.telemetry.HttpTracingRequestInitializer;
6971
import com.google.cloud.http.HttpTransportOptions;
7072
import com.google.common.base.Function;
@@ -1769,6 +1771,15 @@ private <T> T executeWithSpan(Span span, SpanOperation<T> operation) throws IOEx
17691771
}
17701772
try (Scope scope = span.makeCurrent()) {
17711773
return operation.execute(span);
1774+
} catch (Exception e) {
1775+
if (isHttpTracingEnabled()) {
1776+
if (e instanceof GoogleJsonResponseException) {
1777+
BigQueryTelemetryTracer.addErrorResponseToSpan(((GoogleJsonResponseException) e), span);
1778+
} else {
1779+
BigQueryTelemetryTracer.addExceptionToSpan(e, span);
1780+
}
1781+
}
1782+
throw e;
17721783
} finally {
17731784
span.end();
17741785
}

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/telemetry/BigQueryTelemetryTracer.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616

1717
package com.google.cloud.bigquery.telemetry;
1818

19+
import com.google.api.client.googleapis.json.GoogleJsonError;
20+
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
1921
import com.google.api.core.BetaApi;
2022
import com.google.api.core.InternalApi;
2123
import io.opentelemetry.api.common.AttributeKey;
2224
import io.opentelemetry.api.trace.Span;
25+
import io.opentelemetry.api.trace.StatusCode;
2326

2427
/** BigQuery Telemetry class that stores generic telemetry attributes and values */
2528
@BetaApi
@@ -70,4 +73,44 @@ public static void addCommonAttributeToSpan(Span span) {
7073
.setAttribute(GCP_CLIENT_LANGUAGE, BQ_GCP_CLIENT_LANGUAGE);
7174
// TODO: add version
7275
}
76+
77+
public static void addExceptionToSpan(Exception e, Span span) {
78+
span.recordException(e);
79+
String message = e.getMessage();
80+
String simpleName = e.getClass().getSimpleName();
81+
String statusMessage = simpleName + (message != null ? ": " + message : "");
82+
span.setAttribute(BigQueryTelemetryTracer.EXCEPTION_TYPE, e.getClass().getName());
83+
span.setAttribute(
84+
BigQueryTelemetryTracer.ERROR_TYPE, ErrorTypeUtil.getClientErrorType(e).toString());
85+
span.setAttribute(BigQueryTelemetryTracer.STATUS_MESSAGE, statusMessage);
86+
span.setStatus(StatusCode.ERROR, statusMessage);
87+
}
88+
89+
public static void addErrorResponseToSpan(GoogleJsonResponseException errorResponse, Span span) {
90+
span.setStatus(StatusCode.ERROR);
91+
// set default values in case details aren't available below
92+
if (errorResponse.getDetails() != null) {
93+
span.setAttribute(
94+
BigQueryTelemetryTracer.STATUS_MESSAGE, errorResponse.getDetails().getMessage());
95+
} else {
96+
span.setAttribute(BigQueryTelemetryTracer.STATUS_MESSAGE, errorResponse.getStatusMessage());
97+
}
98+
span.setAttribute(
99+
BigQueryTelemetryTracer.ERROR_TYPE, Integer.toString(errorResponse.getStatusCode()));
100+
101+
// reads error details from GoogleJsonResponseException and override any available error details
102+
if (errorResponse.getDetails() != null
103+
&& errorResponse.getDetails().getErrors() != null
104+
&& !errorResponse.getDetails().getErrors().isEmpty()) {
105+
GoogleJsonError.ErrorInfo errorInfo = errorResponse.getDetails().getErrors().get(0);
106+
String message = errorInfo.getMessage();
107+
if (message != null) {
108+
span.setAttribute(BigQueryTelemetryTracer.STATUS_MESSAGE, message);
109+
}
110+
String reason = errorInfo.getReason();
111+
if (reason != null) {
112+
span.setAttribute(BigQueryTelemetryTracer.ERROR_TYPE, reason);
113+
}
114+
}
115+
}
73116
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.google.cloud.bigquery.telemetry;
2+
3+
import com.google.api.core.BetaApi;
4+
5+
/** Utility class for identifying exception types for telemetry tracking. */
6+
// TODO: this class should get replaced with gax version when ready
7+
// work tracked in https://github.com/googleapis/google-cloud-java/issues/12105
8+
@BetaApi
9+
class ErrorTypeUtil {
10+
11+
enum ErrorType {
12+
CLIENT_TIMEOUT,
13+
CLIENT_CONNECTION_ERROR,
14+
CLIENT_REQUEST_ERROR,
15+
CLIENT_RESPONSE_DECODE_ERROR,
16+
CLIENT_UNKNOWN_ERROR;
17+
18+
@Override
19+
public String toString() {
20+
return name();
21+
}
22+
}
23+
24+
static boolean isClientTimeout(Exception e) {
25+
return e instanceof java.net.SocketTimeoutException;
26+
}
27+
28+
static boolean isClientConnectionError(Exception e) {
29+
return e instanceof java.net.ConnectException
30+
|| e instanceof java.net.UnknownHostException
31+
|| e instanceof javax.net.ssl.SSLHandshakeException
32+
|| e instanceof java.nio.channels.UnresolvedAddressException;
33+
}
34+
35+
static boolean isClientResponseDecodeError(Exception e) {
36+
return e instanceof com.google.gson.JsonParseException;
37+
}
38+
39+
static boolean isClientRequestError(Exception e) {
40+
return e instanceof java.lang.IllegalArgumentException;
41+
}
42+
43+
static ErrorType getClientErrorType(Exception e) {
44+
if (isClientTimeout(e)) {
45+
return ErrorType.CLIENT_TIMEOUT;
46+
} else if (ErrorTypeUtil.isClientConnectionError(e)) {
47+
return ErrorType.CLIENT_CONNECTION_ERROR;
48+
} else if (isClientResponseDecodeError(e)) {
49+
return ErrorType.CLIENT_RESPONSE_DECODE_ERROR;
50+
} else if (isClientRequestError(e)) {
51+
return ErrorType.CLIENT_REQUEST_ERROR;
52+
} else {
53+
return ErrorType.CLIENT_UNKNOWN_ERROR;
54+
}
55+
}
56+
}

java-bigquery/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/spi/v2/HttpBigQueryRpcTest.java

Lines changed: 139 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@
6262
import org.junit.jupiter.api.BeforeEach;
6363
import org.junit.jupiter.api.Nested;
6464
import org.junit.jupiter.api.Test;
65+
import org.junit.jupiter.api.parallel.Execution;
66+
import org.junit.jupiter.api.parallel.ExecutionMode;
6567

68+
@Execution(ExecutionMode.SAME_THREAD)
6669
public class HttpBigQueryRpcTest {
6770

6871
private static final String PROJECT_ID = "test-project";
@@ -73,6 +76,8 @@ public class HttpBigQueryRpcTest {
7376
private static final String JOB_ID = "test-job";
7477
private static final String LOCATION = "test-location";
7578

79+
private static final Object TEST_LOCK = new Object();
80+
7681
private InMemorySpanExporter spanExporter;
7782
private MockLowLevelHttpResponse mockResponse;
7883
private String lastRequestMethod;
@@ -196,6 +201,8 @@ class TelemetryEnabled {
196201
@BeforeEach
197202
public void setUp() {
198203
setUpServer();
204+
spanExporter.reset(); // Clear spans from previous tests
205+
System.setProperty("com.google.cloud.bigquery.http.tracing.dev.enabled", "true");
199206
rpc = createRpc(true);
200207
}
201208

@@ -911,10 +918,7 @@ public void testOtelAttributesFromOptionsGetAddedtoSpan() throws Exception {
911918

912919
@Test
913920
public void testHttpTracingEnabledAddsAdditionalAttributes() throws Exception {
914-
try {
915-
System.setProperty("com.google.cloud.bigquery.http.tracing.dev.enabled", "true");
916-
HttpBigQueryRpc customRpc = createRpc(true);
917-
921+
synchronized (TEST_LOCK) {
918922
setMockResponse(
919923
"{\"kind\":\"bigquery#dataset\",\"id\":\""
920924
+ PROJECT_ID
@@ -926,7 +930,7 @@ public void testHttpTracingEnabledAddsAdditionalAttributes() throws Exception {
926930
+ DATASET_ID
927931
+ "\"}}");
928932

929-
customRpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
933+
rpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
930934

931935
verifyRequest("GET", "/projects/" + PROJECT_ID + "/datasets/" + DATASET_ID);
932936
verifySpan(
@@ -936,6 +940,7 @@ public void testHttpTracingEnabledAddsAdditionalAttributes() throws Exception {
936940
Collections.singletonMap("bq.rpc.response.dataset.id", PROJECT_ID + ":" + DATASET_ID));
937941

938942
List<SpanData> spans = spanExporter.getFinishedSpanItems();
943+
System.out.println("total number of spans " + spans.size());
939944
assertThat(spans).isNotEmpty();
940945
SpanData rpcSpan =
941946
spans.stream()
@@ -947,17 +952,89 @@ public void testHttpTracingEnabledAddsAdditionalAttributes() throws Exception {
947952
assertNotNull(rpcSpan);
948953
assertEquals("http", rpcSpan.getAttributes().get(BigQueryTelemetryTracer.RPC_SYSTEM_NAME));
949954
assertNotNull(rpcSpan.getAttributes().get(BigQueryTelemetryTracer.GCP_CLIENT_SERVICE));
950-
} finally {
951-
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
952955
}
953956
}
954957

955958
@Test
956-
public void testHttpTracingDisabledDoesNotAddAdditionalAttributes() throws Exception {
957-
try {
958-
System.setProperty("com.google.cloud.bigquery.http.tracing.dev.enabled", "false");
959-
HttpBigQueryRpc customRpc = createRpc(true);
959+
public void testHttpTracingEnabled_GenericException_SetsAttributes() throws Exception {
960+
synchronized (TEST_LOCK) {
961+
assertThrows(
962+
IOException.class,
963+
() -> {
964+
rpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
965+
});
966+
967+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
968+
assertThat(spans).isNotEmpty();
969+
SpanData rpcSpan =
970+
spans.stream()
971+
.filter(
972+
span ->
973+
span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.getDataset"))
974+
.findFirst()
975+
.orElse(null);
976+
assertNotNull(rpcSpan);
977+
assertEquals(
978+
"java.io.IOException",
979+
rpcSpan.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE));
980+
assertEquals(
981+
"CLIENT_UNKNOWN_ERROR",
982+
rpcSpan.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE));
983+
}
984+
}
985+
986+
@Test
987+
public void testHttpTracingEnabled_JsonResponseException_SetsAttributes() throws Exception {
988+
synchronized (TEST_LOCK) {
989+
mockResponse.setStatusCode(400);
990+
mockResponse.setContentType(Json.MEDIA_TYPE);
991+
mockResponse.setContent(
992+
"{\"error\":{\"code\":400,\"message\":\"Invalid request\",\"errors\":[{\"message\":\"Invalid request\",\"domain\":\"global\",\"reason\":\"invalid\"}]}}");
993+
994+
assertThrows(
995+
IOException.class,
996+
() -> {
997+
rpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
998+
});
999+
1000+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
1001+
assertThat(spans).isNotEmpty();
1002+
SpanData rpcSpan =
1003+
spans.stream()
1004+
.filter(
1005+
span ->
1006+
span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.getDataset"))
1007+
.findFirst()
1008+
.orElse(null);
1009+
assertNotNull(rpcSpan);
1010+
assertEquals("invalid", rpcSpan.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE));
1011+
assertEquals(
1012+
"Invalid request", rpcSpan.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE));
1013+
}
1014+
}
1015+
}
9601016

1017+
@Nested
1018+
class TelemetryEnabledDevDisabled {
1019+
private HttpBigQueryRpc rpc;
1020+
1021+
@BeforeEach
1022+
public void setUp() {
1023+
setUpServer();
1024+
spanExporter.reset(); // Clear spans from previous tests
1025+
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
1026+
rpc = createRpc(true);
1027+
}
1028+
1029+
@org.junit.jupiter.api.AfterEach
1030+
public void tearDown() {
1031+
// Ensure property is cleared for this test class
1032+
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
1033+
}
1034+
1035+
@Test
1036+
public void testHttpTracingDisabledDoesNotAddAdditionalAttributes() throws Exception {
1037+
synchronized (TEST_LOCK) {
9611038
setMockResponse(
9621039
"{\"kind\":\"bigquery#dataset\",\"id\":\""
9631040
+ PROJECT_ID
@@ -969,7 +1046,7 @@ public void testHttpTracingDisabledDoesNotAddAdditionalAttributes() throws Excep
9691046
+ DATASET_ID
9701047
+ "\"}}");
9711048

972-
customRpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
1049+
rpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
9731050

9741051
verifyRequest("GET", "/projects/" + PROJECT_ID + "/datasets/" + DATASET_ID);
9751052
verifySpan(
@@ -990,8 +1067,33 @@ public void testHttpTracingDisabledDoesNotAddAdditionalAttributes() throws Excep
9901067
assertNotNull(rpcSpan);
9911068
assertNull(rpcSpan.getAttributes().get(BigQueryTelemetryTracer.RPC_SYSTEM_NAME));
9921069
assertNull(rpcSpan.getAttributes().get(BigQueryTelemetryTracer.GCP_CLIENT_SERVICE));
993-
} finally {
994-
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
1070+
}
1071+
}
1072+
1073+
@Test
1074+
public void testHttpTracingDisabled_GenericException_DoesNotSetAttributes() throws Exception {
1075+
synchronized (TEST_LOCK) {
1076+
spanExporter.reset(); // Clear any accumulated spans
1077+
1078+
assertThrows(
1079+
IOException.class,
1080+
() -> {
1081+
rpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
1082+
});
1083+
1084+
List<io.opentelemetry.sdk.trace.data.SpanData> spans = spanExporter.getFinishedSpanItems();
1085+
assertThat(spans).isNotEmpty();
1086+
io.opentelemetry.sdk.trace.data.SpanData rpcSpan =
1087+
spans.stream()
1088+
.filter(
1089+
span ->
1090+
span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.getDataset"))
1091+
.findFirst()
1092+
.orElse(null);
1093+
assertNotNull(rpcSpan);
1094+
assertNull(rpcSpan.getAttributes().get(BigQueryTelemetryTracer.EXCEPTION_TYPE));
1095+
assertNull(rpcSpan.getAttributes().get(BigQueryTelemetryTracer.ERROR_TYPE));
1096+
assertNull(rpcSpan.getAttributes().get(BigQueryTelemetryTracer.STATUS_MESSAGE));
9951097
}
9961098
}
9971099
}
@@ -1003,9 +1105,17 @@ class TelemetryDisabled {
10031105
@BeforeEach
10041106
public void setUp() {
10051107
setUpServer();
1108+
spanExporter.reset(); // Clear spans from previous tests
1109+
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
10061110
rpc = createRpc(false);
10071111
}
10081112

1113+
@org.junit.jupiter.api.AfterEach
1114+
public void tearDown() {
1115+
// Ensure property is cleared for this test class
1116+
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
1117+
}
1118+
10091119
@Test
10101120
public void testGetDatasetNoTelemetry() throws Exception {
10111121
setMockResponse(
@@ -1544,5 +1654,20 @@ public void testTestIamPermissionsNoTelemetry() throws Exception {
15441654
+ ":testIamPermissions");
15451655
verifyNoSpans();
15461656
}
1657+
1658+
@Test
1659+
public void testExecuteWithSpan_IgnoresHttpResponseException() throws Exception {
1660+
setMockResponse("");
1661+
mockResponse.setStatusCode(404);
1662+
1663+
try {
1664+
rpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
1665+
org.junit.jupiter.api.Assertions.fail("Expected HttpResponseException was not thrown");
1666+
} catch (com.google.api.client.http.HttpResponseException e) {
1667+
assertEquals(404, e.getStatusCode());
1668+
}
1669+
1670+
verifyNoSpans();
1671+
}
15471672
}
15481673
}

0 commit comments

Comments
 (0)