Skip to content

Commit fe373da

Browse files
committed
feat: add initial span tracing to http calls
1 parent e0d9bd2 commit fe373da

5 files changed

Lines changed: 508 additions & 4 deletions

File tree

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import com.google.cloud.Tuple;
6666
import com.google.cloud.bigquery.BigQueryException;
6767
import com.google.cloud.bigquery.BigQueryOptions;
68+
import com.google.cloud.bigquery.telemetry.HttpTracingRequestInitializer;
6869
import com.google.cloud.http.HttpTransportOptions;
6970
import com.google.common.base.Function;
7071
import com.google.common.collect.ImmutableList;
@@ -85,6 +86,8 @@ public class HttpBigQueryRpc implements BigQueryRpc {
8586

8687
public static final String DEFAULT_PROJECTION = "full";
8788
private static final String BASE_RESUMABLE_URI = "upload/bigquery/v2/projects/";
89+
static final String HTTP_TRACING_DEV_GATE_PROPERTY =
90+
"com.google.cloud.bigquery.http.tracing.dev.enabled";
8891
// see:
8992
// https://cloud.google.com/bigquery/loading-data-post-request#resume-upload
9093
private static final int HTTP_RESUME_INCOMPLETE = 308;
@@ -111,11 +114,20 @@ public HttpBigQueryRpc(BigQueryOptions options) {
111114
HttpTransport transport = transportOptions.getHttpTransportFactory().create();
112115
HttpRequestInitializer initializer = transportOptions.getHttpRequestInitializer(options);
113116
this.options = options;
117+
118+
if (options.isOpenTelemetryTracingEnabled()
119+
&& options.getOpenTelemetryTracer() != null
120+
&& isHttpTracingEnabled()) {
121+
initializer =
122+
new HttpTracingRequestInitializer(
123+
initializer, options.getOpenTelemetryTracer());
124+
}
125+
114126
bigquery =
115-
new Bigquery.Builder(transport, new GsonFactory(), initializer)
116-
.setRootUrl(options.getResolvedApiaryHost("bigquery"))
117-
.setApplicationName(options.getApplicationName())
118-
.build();
127+
new Bigquery.Builder(transport, new GsonFactory(), initializer)
128+
.setRootUrl(options.getResolvedApiaryHost("bigquery"))
129+
.setApplicationName(options.getApplicationName())
130+
.build();
119131
}
120132

121133
private static BigQueryException translate(IOException exception) {
@@ -1776,4 +1788,13 @@ private static Attributes otelAttributesFromOptions(Map<Option, ?> options) {
17761788
}
17771789
return builder.build();
17781790
}
1791+
1792+
@InternalApi("Visible for testing")
1793+
/* Temporary development gate for HttpTracingRequestInitializer rollout:
1794+
must be explicitly enabled with the system property
1795+
*/
1796+
static boolean isHttpTracingEnabled() {
1797+
return
1798+
Boolean.parseBoolean(System.getProperty(HTTP_TRACING_DEV_GATE_PROPERTY));
1799+
}
17791800
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
17+
package com.google.cloud.bigquery.telemetry;
18+
19+
import com.google.api.core.BetaApi;
20+
import com.google.api.core.InternalApi;
21+
import io.opentelemetry.api.common.AttributeKey;
22+
import io.opentelemetry.api.trace.Span;
23+
import io.opentelemetry.api.trace.SpanBuilder;
24+
import io.opentelemetry.api.trace.SpanKind;
25+
import io.opentelemetry.api.trace.Tracer;
26+
27+
/**
28+
* General BigQuery Telemetry class that stores generic telemetry attributes and any associated
29+
* logic to calculate.
30+
*/
31+
@BetaApi
32+
@InternalApi
33+
public final class BigQueryTelemetryTracer {
34+
35+
private BigQueryTelemetryTracer() {}
36+
37+
public static final String BQ_GCP_CLIENT_SERVICE = "bigquery";
38+
public static final String BQ_GCP_CLIENT_REPO = "googleapis/java-bigquery";
39+
public static final String BQ_GCP_CLIENT_ARTIFACT = "google-cloud-bigquery";
40+
public static final String BQ_GCP_CLIENT_LANGUAGE = "java";
41+
42+
// Common GCP Attributes
43+
public static final AttributeKey<String> GCP_CLIENT_SERVICE =
44+
AttributeKey.stringKey("gcp.client.service");
45+
public static final AttributeKey<String> GCP_CLIENT_VERSION =
46+
AttributeKey.stringKey("gcp.client.version");
47+
public static final AttributeKey<String> GCP_CLIENT_REPO =
48+
AttributeKey.stringKey("gcp.client.repo");
49+
public static final AttributeKey<String> GCP_CLIENT_ARTIFACT =
50+
AttributeKey.stringKey("gcp.client.artifact");
51+
public static final AttributeKey<String> GCP_CLIENT_LANGUAGE =
52+
AttributeKey.stringKey("gcp.client.language");
53+
public static final AttributeKey<String> GCP_RESOURCE_ID =
54+
AttributeKey.stringKey("gcp.resource.destination.id");
55+
public static final AttributeKey<String> RPC_SYSTEM_NAME =
56+
AttributeKey.stringKey("rpc.system.name");
57+
58+
// Common Error Attributes
59+
public static final AttributeKey<String> ERROR_TYPE = AttributeKey.stringKey("error.type");
60+
public static final AttributeKey<String> EXCEPTION_TYPE =
61+
AttributeKey.stringKey("exception.type");
62+
public static final AttributeKey<String> STATUS_MESSAGE =
63+
AttributeKey.stringKey("status.message");
64+
65+
// Common Server Attributes
66+
public static final AttributeKey<String> SERVER_ADDRESS =
67+
AttributeKey.stringKey("server.address");
68+
public static final AttributeKey<Long> SERVER_PORT = AttributeKey.longKey("server.port");
69+
70+
public static void addCommonAttributeToSpan(Span span) {
71+
span
72+
.setAttribute(GCP_CLIENT_SERVICE, BQ_GCP_CLIENT_SERVICE)
73+
.setAttribute(GCP_CLIENT_REPO, BQ_GCP_CLIENT_REPO)
74+
.setAttribute(GCP_CLIENT_ARTIFACT, BQ_GCP_CLIENT_ARTIFACT)
75+
.setAttribute(GCP_CLIENT_LANGUAGE, BQ_GCP_CLIENT_LANGUAGE);
76+
// TODO: add version
77+
}
78+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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+
17+
package com.google.cloud.bigquery.telemetry;
18+
19+
import com.google.api.client.http.*;
20+
import com.google.api.core.BetaApi;
21+
import com.google.api.core.InternalApi;
22+
import com.google.common.annotations.VisibleForTesting;
23+
import io.opentelemetry.api.common.AttributeKey;
24+
import io.opentelemetry.api.trace.Span;
25+
import io.opentelemetry.api.trace.StatusCode;
26+
import io.opentelemetry.api.trace.Tracer;
27+
28+
import java.io.BufferedInputStream;
29+
import java.io.BufferedReader;
30+
import java.io.IOException;
31+
import java.io.InputStream;
32+
import java.io.InputStreamReader;
33+
import java.net.URI;
34+
import java.net.URISyntaxException;
35+
import java.nio.charset.StandardCharsets;
36+
import java.util.Arrays;
37+
import java.util.Collections;
38+
import java.util.HashSet;
39+
import java.util.Set;
40+
import java.util.concurrent.atomic.AtomicBoolean;
41+
import java.util.concurrent.atomic.AtomicLong;
42+
import java.util.stream.Collectors;
43+
44+
/**
45+
* HttpRequestInitializer that wraps a delegate initializer, intercepts all HTTP requests, adds
46+
* OpenTelemetry tracing and then invokes delegate interceptor.
47+
*/
48+
@BetaApi
49+
@InternalApi
50+
public class HttpTracingRequestInitializer implements HttpRequestInitializer {
51+
52+
// HTTP Specific Telemetry Attributes
53+
public static final AttributeKey<String> HTTP_REQUEST_METHOD =
54+
AttributeKey.stringKey("http.request.method");
55+
public static final AttributeKey<String> URL_FULL = AttributeKey.stringKey("url.full");
56+
public static final AttributeKey<String> URL_TEMPLATE = AttributeKey.stringKey("url.template");
57+
public static final AttributeKey<String> URL_DOMAIN = AttributeKey.stringKey("url.domain");
58+
public static final AttributeKey<Long> HTTP_RESPONSE_STATUS_CODE =
59+
AttributeKey.longKey("http.response.status_code");
60+
public static final AttributeKey<Long> HTTP_REQUEST_RESEND_COUNT =
61+
AttributeKey.longKey("http.request.resend_count");
62+
public static final AttributeKey<Long> HTTP_REQUEST_BODY_SIZE =
63+
AttributeKey.longKey("http.request.body.size");
64+
public static final AttributeKey<Long> HTTP_RESPONSE_BODY_SIZE =
65+
AttributeKey.longKey("http.response.body.size");
66+
67+
@VisibleForTesting static final String HTTP_RPC_SYSTEM_NAME = "http";
68+
69+
70+
private final HttpRequestInitializer delegate;
71+
private final Tracer tracer;
72+
73+
public HttpTracingRequestInitializer(
74+
HttpRequestInitializer delegate, Tracer tracer) {
75+
this.delegate = delegate;
76+
this.tracer = tracer;
77+
}
78+
79+
@Override
80+
public void initialize(HttpRequest request) throws IOException {
81+
if (delegate != null) {
82+
delegate.initialize(request);
83+
}
84+
85+
if (tracer == null) {
86+
return;
87+
}
88+
89+
// Get the current active span (created by HttpBigQueryRpc) and add HTTP attributes to it
90+
Span span = Span.current();
91+
if (!span.getSpanContext().isValid()) {
92+
// No active span to exists, skip instrumentation
93+
return;
94+
}
95+
96+
String host = request.getUrl().getHost();
97+
Integer port = request.getUrl().getPort();
98+
99+
// Add initial HTTP attributes to the existing span
100+
addInitialHttpAttributesToSpan(span, host, port);
101+
}
102+
103+
/** Add initial HTTP attributes to the existing active span */
104+
private void addInitialHttpAttributesToSpan(
105+
Span span, String host, Integer port) {
106+
BigQueryTelemetryTracer.addCommonAttributeToSpan(span);
107+
span.setAttribute(BigQueryTelemetryTracer.RPC_SYSTEM_NAME, HTTP_RPC_SYSTEM_NAME);
108+
span.setAttribute(BigQueryTelemetryTracer.SERVER_ADDRESS, host);
109+
if (port != null && port > 0) {
110+
span.setAttribute(BigQueryTelemetryTracer.SERVER_PORT, port.longValue());
111+
}
112+
// TODO add full sanitized url, url domain, request method
113+
}
114+
}

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,96 @@ public void testOtelAttributesFromOptionsGetAddedtoSpan() throws Exception {
906906
"GetDataset",
907907
expectedAttributes);
908908
}
909+
910+
@Test
911+
public void testHttpTracingEnabled() throws Exception {
912+
String originalProperty = System.getProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
913+
try {
914+
System.setProperty("com.google.cloud.bigquery.http.tracing.dev.enabled", "true");
915+
HttpBigQueryRpc customRpc = createRpc(true);
916+
917+
setMockResponse(
918+
"{\"kind\":\"bigquery#dataset\",\"id\":\""
919+
+ PROJECT_ID
920+
+ ":"
921+
+ DATASET_ID
922+
+ "\",\"datasetReference\":{\"projectId\":\""
923+
+ PROJECT_ID
924+
+ "\",\"datasetId\":\""
925+
+ DATASET_ID
926+
+ "\"}}");
927+
928+
customRpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
929+
930+
verifyRequest("GET", "/projects/" + PROJECT_ID + "/datasets/" + DATASET_ID);
931+
verifySpan(
932+
"com.google.cloud.bigquery.BigQueryRpc.getDataset",
933+
"DatasetService",
934+
"GetDataset",
935+
Collections.singletonMap("bq.rpc.response.dataset.id", PROJECT_ID + ":" + DATASET_ID));
936+
937+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
938+
assertThat(spans).isNotEmpty();
939+
SpanData rpcSpan = spans.stream()
940+
.filter(span -> span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.getDataset"))
941+
.findFirst()
942+
.orElse(null);
943+
assertNotNull(rpcSpan);
944+
assertEquals("http", rpcSpan.getAttributes().get(AttributeKey.stringKey("rpc.system.name")));
945+
assertNotNull(rpcSpan.getAttributes().get(AttributeKey.stringKey("server.address")));
946+
} finally {
947+
if (originalProperty != null) {
948+
System.setProperty("com.google.cloud.bigquery.http.tracing.dev.enabled", originalProperty);
949+
} else {
950+
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
951+
}
952+
}
953+
}
954+
955+
@Test
956+
public void testHttpTracingDisabled() throws Exception {
957+
String originalProperty = System.getProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
958+
try {
959+
System.setProperty("com.google.cloud.bigquery.http.tracing.dev.enabled", "false");
960+
HttpBigQueryRpc customRpc = createRpc(true);
961+
962+
setMockResponse(
963+
"{\"kind\":\"bigquery#dataset\",\"id\":\""
964+
+ PROJECT_ID
965+
+ ":"
966+
+ DATASET_ID
967+
+ "\",\"datasetReference\":{\"projectId\":\""
968+
+ PROJECT_ID
969+
+ "\",\"datasetId\":\""
970+
+ DATASET_ID
971+
+ "\"}}");
972+
973+
customRpc.getDatasetSkipExceptionTranslation(PROJECT_ID, DATASET_ID, new HashMap<>());
974+
975+
verifyRequest("GET", "/projects/" + PROJECT_ID + "/datasets/" + DATASET_ID);
976+
verifySpan(
977+
"com.google.cloud.bigquery.BigQueryRpc.getDataset",
978+
"DatasetService",
979+
"GetDataset",
980+
Collections.singletonMap("bq.rpc.response.dataset.id", PROJECT_ID + ":" + DATASET_ID));
981+
982+
List<SpanData> spans = spanExporter.getFinishedSpanItems();
983+
assertThat(spans).isNotEmpty();
984+
SpanData rpcSpan = spans.stream()
985+
.filter(span -> span.getName().equals("com.google.cloud.bigquery.BigQueryRpc.getDataset"))
986+
.findFirst()
987+
.orElse(null);
988+
assertNotNull(rpcSpan);
989+
assertThat(rpcSpan.getAttributes().get(AttributeKey.stringKey("rpc.system.name"))).isNull();
990+
assertThat(rpcSpan.getAttributes().get(AttributeKey.stringKey("server.address"))).isNull();
991+
} finally {
992+
if (originalProperty != null) {
993+
System.setProperty("com.google.cloud.bigquery.http.tracing.dev.enabled", originalProperty);
994+
} else {
995+
System.clearProperty("com.google.cloud.bigquery.http.tracing.dev.enabled");
996+
}
997+
}
998+
}
909999
}
9101000

9111001
@Nested

0 commit comments

Comments
 (0)