Skip to content

Commit 167722d

Browse files
feat(bigquery): add resend attribute to span tracing + integration tests (#12313)
This adds the http.request.resend_count to span tracing in the case of a retry. We achieve this by adding a context to the parent span that tracks all retries. This remains at null for the initial retry and then gets incremented for all subsequent retries. This PR also introduces integration tests to fully validate the resent attribute. [example trace with resent attribute set ](https://screenshot.googleplex.com/BTjLU6v2stpYPWf.png) --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 4e18407 commit 167722d

File tree

6 files changed

+348
-4
lines changed

6 files changed

+348
-4
lines changed

java-bigquery/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/BigQueryRetryHelper.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,21 @@
2727
import com.google.cloud.RetryHelper;
2828
import io.opentelemetry.api.trace.Span;
2929
import io.opentelemetry.api.trace.Tracer;
30+
import io.opentelemetry.context.Context;
31+
import io.opentelemetry.context.ContextKey;
3032
import io.opentelemetry.context.Scope;
3133
import java.io.IOException;
3234
import java.util.concurrent.Callable;
3335
import java.util.concurrent.ExecutionException;
36+
import java.util.concurrent.atomic.AtomicInteger;
3437
import java.util.logging.Level;
3538
import java.util.logging.Logger;
3639

3740
public class BigQueryRetryHelper extends RetryHelper {
3841

42+
public static final ContextKey<AtomicInteger> RETRY_ATTEMPT_KEY =
43+
ContextKey.named("bq_retry_attempt");
44+
3945
private static final Logger LOG = Logger.getLogger(BigQueryRetryHelper.class.getName());
4046

4147
public static <V> V runWithRetries(
@@ -54,7 +60,11 @@ public static <V> V runWithRetries(
5460
.spanBuilder("com.google.cloud.bigquery.BigQueryRetryHelper.runWithRetries")
5561
.startSpan();
5662
}
57-
try (Scope runWithRetriesScope = runWithRetries != null ? runWithRetries.makeCurrent() : null) {
63+
Context retryContext = Context.current().with(RETRY_ATTEMPT_KEY, new AtomicInteger(0));
64+
if (runWithRetries != null) {
65+
retryContext = retryContext.with(runWithRetries);
66+
}
67+
try (Scope runWithRetriesScope = retryContext.makeCurrent()) {
5868
// Suppressing should be ok as a workaraund. Current and only ResultRetryAlgorithm
5969
// implementation does not use response at all, so ignoring its type is ok.
6070
@SuppressWarnings("unchecked")

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,19 @@
1616
package com.google.cloud.bigquery.telemetry;
1717

1818
import com.google.api.core.BetaApi;
19+
import com.google.common.annotations.VisibleForTesting;
1920

2021
/**
2122
* Utility class for identifying exception types for telemetry tracking. TODO: this class should get
2223
* replaced with gax version when ready work tracked in
2324
* https://github.com/googleapis/google-cloud-java/issues/12105
2425
*/
2526
@BetaApi
26-
class ErrorTypeUtil {
27+
@VisibleForTesting
28+
public class ErrorTypeUtil {
2729

28-
enum ErrorType {
30+
@VisibleForTesting
31+
public enum ErrorType {
2932
CLIENT_TIMEOUT,
3033
CLIENT_CONNECTION_ERROR,
3134
CLIENT_REQUEST_ERROR,

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
import com.google.api.client.http.*;
2020
import com.google.api.core.BetaApi;
2121
import com.google.api.core.InternalApi;
22+
import com.google.cloud.bigquery.BigQueryRetryHelper;
2223
import com.google.common.annotations.VisibleForTesting;
2324
import io.opentelemetry.api.common.AttributeKey;
2425
import io.opentelemetry.api.trace.Span;
2526
import io.opentelemetry.api.trace.Tracer;
2627
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
2728
import io.opentelemetry.context.Context;
2829
import java.io.IOException;
30+
import java.util.concurrent.atomic.AtomicInteger;
2931

3032
/**
3133
* HttpRequestInitializer that wraps a delegate initializer, intercepts all HTTP requests, adds
@@ -50,7 +52,7 @@ public class HttpTracingRequestInitializer implements HttpRequestInitializer {
5052
public static final AttributeKey<Long> HTTP_RESPONSE_BODY_SIZE =
5153
AttributeKey.longKey("http.response.body.size");
5254

53-
@VisibleForTesting static final String HTTP_RPC_SYSTEM_NAME = "http";
55+
@VisibleForTesting public static final String HTTP_RPC_SYSTEM_NAME = "http";
5456

5557
private static final java.util.Set<String> REDACTED_QUERY_PARAMETERS =
5658
com.google.common.collect.ImmutableSet.of(
@@ -84,6 +86,14 @@ public void initialize(HttpRequest request) throws IOException {
8486

8587
addInitialHttpAttributesToSpan(span, request);
8688

89+
AtomicInteger attemptTracker = Context.current().get(BigQueryRetryHelper.RETRY_ATTEMPT_KEY);
90+
if (attemptTracker != null) {
91+
int attempt = attemptTracker.getAndIncrement();
92+
if (attempt > 0) {
93+
span.setAttribute(HTTP_REQUEST_RESEND_COUNT, (long) attempt);
94+
}
95+
}
96+
8797
HttpResponseInterceptor originalInterceptor = request.getResponseInterceptor();
8898
request.setResponseInterceptor(
8999
response -> {
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.bigquery.it;
17+
18+
import static com.google.cloud.bigquery.telemetry.ErrorTypeUtil.ErrorType;
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertFalse;
21+
import static org.junit.jupiter.api.Assertions.assertNotNull;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
import static org.junit.jupiter.api.Assertions.fail;
24+
25+
import com.google.api.gax.retrying.RetrySettings;
26+
import com.google.cloud.bigquery.BigQuery;
27+
import com.google.cloud.bigquery.BigQueryException;
28+
import com.google.cloud.bigquery.telemetry.BigQueryTelemetryTracer;
29+
import com.google.cloud.bigquery.telemetry.HttpTracingRequestInitializer;
30+
import com.google.cloud.bigquery.testing.RemoteBigQueryHelper;
31+
import io.opentelemetry.api.OpenTelemetry;
32+
import io.opentelemetry.api.common.AttributeKey;
33+
import io.opentelemetry.api.trace.Tracer;
34+
import io.opentelemetry.sdk.OpenTelemetrySdk;
35+
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
36+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
37+
import io.opentelemetry.sdk.trace.data.SpanData;
38+
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
39+
import java.io.IOException;
40+
import java.util.List;
41+
import java.util.Map;
42+
import org.junit.jupiter.api.BeforeAll;
43+
import org.junit.jupiter.api.BeforeEach;
44+
import org.junit.jupiter.api.Test;
45+
46+
public class ITOpenTelemetryTest {
47+
48+
private static RemoteBigQueryHelper bigqueryHelper;
49+
private InMemorySpanExporter memoryExporter;
50+
private Tracer tracer;
51+
52+
@BeforeAll
53+
public static void setUpClass() throws IOException {
54+
System.setProperty("com.google.cloud.bigquery.http.tracing.dev.enabled", "true");
55+
bigqueryHelper = RemoteBigQueryHelper.create();
56+
}
57+
58+
@BeforeEach
59+
public void setUp() {
60+
memoryExporter = InMemorySpanExporter.create();
61+
SdkTracerProvider tracerProvider =
62+
SdkTracerProvider.builder()
63+
.addSpanProcessor(SimpleSpanProcessor.create(memoryExporter))
64+
.build();
65+
OpenTelemetry openTelemetry =
66+
OpenTelemetrySdk.builder().setTracerProvider(tracerProvider).build();
67+
tracer = openTelemetry.getTracer("it-otel-test");
68+
}
69+
70+
@Test
71+
public void testListDatasetsTraced() {
72+
BigQuery bq =
73+
bigqueryHelper.getOptions().toBuilder()
74+
.setEnableOpenTelemetryTracing(true)
75+
.setOpenTelemetryTracer(tracer)
76+
.build()
77+
.getService();
78+
79+
bq.listDatasets();
80+
81+
List<SpanData> spans = memoryExporter.getFinishedSpanItems();
82+
assertNotNull(spans);
83+
assertFalse(spans.isEmpty(), "Expected at least one span collected");
84+
85+
boolean foundRpcSpan = false;
86+
for (SpanData span : spans) {
87+
if (span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.listDatasets")) {
88+
foundRpcSpan = true;
89+
Map<AttributeKey<?>, Object> attrs = span.getAttributes().asMap();
90+
checkGeneralAttributes(attrs);
91+
assertEquals("GET", attrs.get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD));
92+
assertEquals("DatasetService", attrs.get(AttributeKey.stringKey("bq.rpc.service")));
93+
assertEquals("ListDatasets", attrs.get(AttributeKey.stringKey("bq.rpc.method")));
94+
assertEquals("bigquery.googleapis.com", attrs.get(BigQueryTelemetryTracer.SERVER_ADDRESS));
95+
assertEquals(200L, attrs.get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE));
96+
assertEquals("bigquery.googleapis.com", attrs.get(BigQueryTelemetryTracer.URL_DOMAIN));
97+
assertEquals(
98+
"https://bigquery.googleapis.com/bigquery/v2/projects/gcloud-devel/datasets?prettyPrint=false",
99+
attrs.get(HttpTracingRequestInitializer.URL_FULL));
100+
assertEquals(
101+
"//bigquery.googleapis.com/projects/gcloud-devel/datasets",
102+
attrs.get(BigQueryTelemetryTracer.GCP_RESOURCE_DESTINATION_ID));
103+
assertEquals(
104+
"projects/{+projectId}/datasets", attrs.get(BigQueryTelemetryTracer.URL_TEMPLATE));
105+
}
106+
}
107+
assertTrue(foundRpcSpan, "Expected to find BigQueryRpc.listDatasets span");
108+
}
109+
110+
@Test
111+
public void testGetDatasetNotFoundTraced() {
112+
BigQuery bq =
113+
bigqueryHelper.getOptions().toBuilder()
114+
.setEnableOpenTelemetryTracing(true)
115+
.setOpenTelemetryTracer(tracer)
116+
.build()
117+
.getService();
118+
119+
bq.getDataset("non_existent_dataset");
120+
121+
List<SpanData> spans = memoryExporter.getFinishedSpanItems();
122+
assertNotNull(spans);
123+
assertFalse(spans.isEmpty(), "Expected at least one span collected");
124+
125+
boolean foundRpcSpan = false;
126+
for (SpanData span : spans) {
127+
if (span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.getDataset")) {
128+
foundRpcSpan = true;
129+
Map<AttributeKey<?>, Object> attrs = span.getAttributes().asMap();
130+
checkGeneralAttributes(attrs);
131+
assertEquals("GET", attrs.get(HttpTracingRequestInitializer.HTTP_REQUEST_METHOD));
132+
assertEquals("DatasetService", attrs.get(AttributeKey.stringKey("bq.rpc.service")));
133+
assertEquals("GetDataset", attrs.get(AttributeKey.stringKey("bq.rpc.method")));
134+
assertEquals(404L, attrs.get(HttpTracingRequestInitializer.HTTP_RESPONSE_STATUS_CODE));
135+
assertEquals(
136+
"projects/{+projectId}/datasets/{+datasetId}",
137+
attrs.get(BigQueryTelemetryTracer.URL_TEMPLATE));
138+
assertEquals(
139+
"https://bigquery.googleapis.com/bigquery/v2/projects/gcloud-devel/datasets/non_existent_dataset?prettyPrint=false",
140+
attrs.get(HttpTracingRequestInitializer.URL_FULL));
141+
assertEquals("bigquery.googleapis.com", attrs.get(BigQueryTelemetryTracer.SERVER_ADDRESS));
142+
assertEquals("bigquery.googleapis.com", attrs.get(BigQueryTelemetryTracer.URL_DOMAIN));
143+
assertEquals(
144+
"//bigquery.googleapis.com/projects/gcloud-devel/datasets/non_existent_dataset",
145+
attrs.get(BigQueryTelemetryTracer.GCP_RESOURCE_DESTINATION_ID));
146+
147+
// Error attributes
148+
assertEquals("notFound", attrs.get(BigQueryTelemetryTracer.ERROR_TYPE));
149+
assertEquals(
150+
"Not found: Dataset gcloud-devel:non_existent_dataset",
151+
attrs.get(BigQueryTelemetryTracer.STATUS_MESSAGE));
152+
}
153+
}
154+
assertTrue(foundRpcSpan, "Expected to find BigQueryRpc.getDataset span");
155+
}
156+
157+
@Test
158+
public void testConnectionErrorRetriesTraced() {
159+
// Pass invalid host to force connection error and retries
160+
BigQuery bq =
161+
bigqueryHelper.getOptions().toBuilder()
162+
.setRetrySettings(RetrySettings.newBuilder().setMaxAttempts(5).build())
163+
.setEnableOpenTelemetryTracing(true)
164+
.setOpenTelemetryTracer(tracer)
165+
.setHost("https://invalid-host-name-12345.com:8080")
166+
.build()
167+
.getService();
168+
169+
try {
170+
bq.listDatasets();
171+
fail("Expected BigQueryException due to invalid host");
172+
} catch (BigQueryException e) {
173+
// Expected
174+
}
175+
176+
List<SpanData> spans = memoryExporter.getFinishedSpanItems();
177+
assertNotNull(spans);
178+
assertFalse(spans.isEmpty(), "Expected at least one span collected");
179+
180+
int rpcSpanCount = 0;
181+
for (SpanData span : spans) {
182+
if (span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.listDatasets")) {
183+
rpcSpanCount++;
184+
Map<AttributeKey<?>, Object> attrs = span.getAttributes().asMap();
185+
checkGeneralAttributes(attrs);
186+
assertEquals(
187+
"https://invalid-host-name-12345.com:8080/bigquery/v2/projects/gcloud-devel/datasets?prettyPrint=false",
188+
(String) attrs.get(HttpTracingRequestInitializer.URL_FULL));
189+
assertEquals(
190+
"invalid-host-name-12345.com", attrs.get(BigQueryTelemetryTracer.SERVER_ADDRESS));
191+
assertEquals(8080L, attrs.get(BigQueryTelemetryTracer.SERVER_PORT));
192+
assertEquals("invalid-host-name-12345.com", attrs.get(BigQueryTelemetryTracer.URL_DOMAIN));
193+
assertEquals(
194+
"projects/{+projectId}/datasets", attrs.get(BigQueryTelemetryTracer.URL_TEMPLATE));
195+
assertEquals(
196+
"//bigquery.googleapis.com/projects/gcloud-devel/datasets",
197+
attrs.get(BigQueryTelemetryTracer.GCP_RESOURCE_DESTINATION_ID));
198+
checkRetryAttribute(span, rpcSpanCount);
199+
200+
// Error attributes
201+
assertEquals(
202+
"java.net.UnknownHostException", attrs.get(BigQueryTelemetryTracer.EXCEPTION_TYPE));
203+
assertEquals(
204+
ErrorType.CLIENT_CONNECTION_ERROR.toString(),
205+
attrs.get(BigQueryTelemetryTracer.ERROR_TYPE));
206+
assertEquals(
207+
"UnknownHostException: invalid-host-name-12345.com",
208+
attrs.get(BigQueryTelemetryTracer.STATUS_MESSAGE));
209+
}
210+
}
211+
assertEquals(5, rpcSpanCount, "Expected 5 attempts total");
212+
}
213+
214+
@Test
215+
public void testSimultaneousCallsDoNotAffectResendCountForEachother() {
216+
BigQuery bq =
217+
bigqueryHelper.getOptions().toBuilder()
218+
.setRetrySettings(RetrySettings.newBuilder().setMaxAttempts(5).build())
219+
.setEnableOpenTelemetryTracing(true)
220+
.setOpenTelemetryTracer(tracer)
221+
.setHost("https://invalid-host-name-123456.com")
222+
.build()
223+
.getService();
224+
225+
try {
226+
bq.listDatasets();
227+
fail("Expected BigQueryException due to invalid host");
228+
} catch (BigQueryException e) {
229+
// Expected
230+
}
231+
try {
232+
bq.cancel("test-job-id");
233+
fail("Expected BigQueryException due to invalid host");
234+
} catch (BigQueryException e) {
235+
// Expected
236+
}
237+
238+
List<SpanData> spans = memoryExporter.getFinishedSpanItems();
239+
assertNotNull(spans);
240+
assertFalse(spans.isEmpty(), "Expected at least one span collected");
241+
242+
int listDataSpanCount = 0;
243+
int cancelJobSpanCount = 0;
244+
for (SpanData span : spans) {
245+
if (span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.listDatasets")) {
246+
listDataSpanCount++;
247+
checkRetryAttribute(span, listDataSpanCount);
248+
} else if (span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.cancelJob")) {
249+
cancelJobSpanCount++;
250+
checkRetryAttribute(span, cancelJobSpanCount);
251+
}
252+
}
253+
assertEquals(5, listDataSpanCount, "Expected 5 attempts total for listDatasets call");
254+
assertEquals(5, cancelJobSpanCount, "Expected 5 attempts total for cancelJob call");
255+
}
256+
257+
private static void checkRetryAttribute(SpanData span, int listDataSpanCount) {
258+
Map<AttributeKey<?>, Object> attrs = span.getAttributes().asMap();
259+
Long resendCount = (Long) attrs.get(HttpTracingRequestInitializer.HTTP_REQUEST_RESEND_COUNT);
260+
if (listDataSpanCount == 1) {
261+
assertTrue(resendCount == null || resendCount == 0);
262+
} else {
263+
assertNotNull(resendCount, "Expected resend count for retry attempt " + listDataSpanCount);
264+
assertEquals((long) (listDataSpanCount - 1), resendCount.longValue());
265+
}
266+
}
267+
268+
private void checkGeneralAttributes(Map<AttributeKey<?>, Object> attrs) {
269+
assertEquals(
270+
HttpTracingRequestInitializer.HTTP_RPC_SYSTEM_NAME,
271+
attrs.get(BigQueryTelemetryTracer.RPC_SYSTEM_NAME));
272+
assertEquals(
273+
BigQueryTelemetryTracer.BQ_GCP_CLIENT_SERVICE,
274+
attrs.get(BigQueryTelemetryTracer.GCP_CLIENT_SERVICE));
275+
assertEquals(
276+
BigQueryTelemetryTracer.BQ_GCP_CLIENT_REPO,
277+
attrs.get(BigQueryTelemetryTracer.GCP_CLIENT_REPO));
278+
assertEquals(
279+
BigQueryTelemetryTracer.BQ_GCP_CLIENT_LANGUAGE,
280+
attrs.get(BigQueryTelemetryTracer.GCP_CLIENT_LANGUAGE));
281+
assertEquals(
282+
BigQueryTelemetryTracer.BQ_GCP_CLIENT_ARTIFACT,
283+
attrs.get(BigQueryTelemetryTracer.GCP_CLIENT_ARTIFACT));
284+
assertNotNull(attrs.get(BigQueryTelemetryTracer.GCP_CLIENT_VERSION));
285+
}
286+
}

0 commit comments

Comments
 (0)