Skip to content

Commit 5b46669

Browse files
authored
feat(bigquery-jdbc): implement clearcut transport layer (#13738)
1 parent 8a79ad8 commit 5b46669

3 files changed

Lines changed: 408 additions & 0 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
* https://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.jdbc.telemetry.v1;
18+
19+
import com.google.api.client.http.ByteArrayContent;
20+
import com.google.api.client.http.GenericUrl;
21+
import com.google.api.client.http.HttpContent;
22+
import com.google.api.client.http.HttpRequest;
23+
import com.google.api.client.http.HttpRequestFactory;
24+
import com.google.api.client.http.HttpResponse;
25+
import com.google.api.client.http.HttpTransport;
26+
import com.google.api.client.http.javanet.NetHttpTransport;
27+
import com.google.cloud.bigquery.jdbc.BigQueryJdbcCustomLogger;
28+
import java.io.IOException;
29+
import java.io.InputStream;
30+
import java.util.logging.Level;
31+
import java.util.logging.Logger;
32+
33+
final class ClearcutTransport {
34+
private static final Logger logger =
35+
new BigQueryJdbcCustomLogger(ClearcutTransport.class.getName());
36+
private static final String CONTENT_TYPE_PROTOBUF = "application/x-protobuf";
37+
private static final int DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
38+
private static final int DEFAULT_READ_TIMEOUT_MS = 10_000;
39+
40+
private final HttpTransport httpTransport;
41+
private final TelemetryConfiguration config;
42+
private final HttpRequestFactory requestFactory;
43+
44+
ClearcutTransport(TelemetryConfiguration config) {
45+
this(new NetHttpTransport(), config);
46+
}
47+
48+
// Package-private constructor for testing overrides
49+
ClearcutTransport(HttpTransport httpTransport, TelemetryConfiguration config) {
50+
this.httpTransport = httpTransport;
51+
this.config = config;
52+
this.requestFactory = this.httpTransport.createRequestFactory();
53+
}
54+
55+
TransportResult send(TelemetryPayload payload) {
56+
if (!config.isEnabled()) {
57+
return TransportResult.disabled();
58+
}
59+
if (payload == null) {
60+
logger.log(Level.WARNING, "Cannot send null telemetry payload to Clearcut");
61+
return TransportResult.disabled();
62+
}
63+
64+
long now = System.currentTimeMillis();
65+
LogRequest logRequest =
66+
LogRequest.newBuilder()
67+
.setLogSource(config.getLogSource())
68+
.setRequestTimeMs(now)
69+
.addLogEvents(
70+
LogEvent.newBuilder()
71+
.setEventTimeMs(now)
72+
.setSourceExtension(payload.toByteString())
73+
.build())
74+
.build();
75+
76+
byte[] requestBytes = logRequest.toByteArray();
77+
HttpContent content = new ByteArrayContent(CONTENT_TYPE_PROTOBUF, requestBytes);
78+
GenericUrl url = new GenericUrl(config.getEndpointUrl());
79+
80+
long nextRequestWaitMillis = -1;
81+
82+
try {
83+
HttpRequest request = requestFactory.buildPostRequest(url, content);
84+
request.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT_MS);
85+
request.setReadTimeout(DEFAULT_READ_TIMEOUT_MS);
86+
request.setThrowExceptionOnExecuteError(false);
87+
88+
HttpResponse response = null;
89+
try {
90+
response = request.execute();
91+
int statusCode = response.getStatusCode();
92+
93+
if (response.getContent() != null) {
94+
try (InputStream is = response.getContent()) {
95+
LogResponse logResponse = LogResponse.parseFrom(is);
96+
if (logResponse.getNextRequestWaitMillis() > 0) {
97+
nextRequestWaitMillis = logResponse.getNextRequestWaitMillis();
98+
}
99+
} catch (IOException ignored) {
100+
// Ignore non-protobuf content from error bodies
101+
}
102+
}
103+
104+
boolean success = statusCode >= 200 && statusCode < 300;
105+
if (success) {
106+
logger.log(Level.FINE, "Successfully uploaded telemetry payload to Clearcut");
107+
} else {
108+
logger.log(
109+
Level.WARNING,
110+
String.format("Clearcut upload failed with status code: %d", statusCode));
111+
}
112+
return new TransportResult(success, nextRequestWaitMillis);
113+
} finally {
114+
if (response != null) {
115+
response.disconnect();
116+
}
117+
}
118+
} catch (IOException e) {
119+
logger.log(Level.WARNING, "IOException sending telemetry payload to Clearcut", e);
120+
return new TransportResult(false, nextRequestWaitMillis);
121+
} catch (Throwable t) {
122+
logger.log(Level.WARNING, "Unexpected error sending telemetry payload to Clearcut", t);
123+
return new TransportResult(false, nextRequestWaitMillis);
124+
}
125+
}
126+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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+
* https://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.jdbc.telemetry.v1;
18+
19+
import java.util.Objects;
20+
21+
/** Represents the immutable result of a Clearcut telemetry payload transmission attempt. */
22+
final class TransportResult {
23+
private static final TransportResult DISABLED = new TransportResult(false, -1);
24+
25+
private final boolean success;
26+
private final long nextRequestWaitMillis;
27+
28+
TransportResult(boolean success, long nextRequestWaitMillis) {
29+
this.success = success;
30+
this.nextRequestWaitMillis = nextRequestWaitMillis;
31+
}
32+
33+
static TransportResult disabled() {
34+
return DISABLED;
35+
}
36+
37+
boolean isSuccess() {
38+
return success;
39+
}
40+
41+
long getNextRequestWaitMillis() {
42+
return nextRequestWaitMillis;
43+
}
44+
45+
@Override
46+
public boolean equals(Object o) {
47+
if (this == o) {
48+
return true;
49+
}
50+
if (o == null || getClass() != o.getClass()) {
51+
return false;
52+
}
53+
TransportResult that = (TransportResult) o;
54+
return success == that.success && nextRequestWaitMillis == that.nextRequestWaitMillis;
55+
}
56+
57+
@Override
58+
public int hashCode() {
59+
return Objects.hash(success, nextRequestWaitMillis);
60+
}
61+
62+
@Override
63+
public String toString() {
64+
return "TransportResult{"
65+
+ "success="
66+
+ success
67+
+ ", nextRequestWaitMillis="
68+
+ nextRequestWaitMillis
69+
+ '}';
70+
}
71+
}

0 commit comments

Comments
 (0)