Skip to content

Commit 1adc224

Browse files
feat(bqjdbc): Implement Correlated OpenTelemetry Logging Bridge (#13002)
b/496678357 This PR implements the **Correlated GCP Logging Bridge** for the OpenTelemetry integration in the BigQuery JDBC driver. It enables bridging standard Java logs (`java.util.logging`) to the OpenTelemetry Logs API, allowing users to correlate logs with distributed traces and isolate them by connection session. ### Changes - `BigQueryDriver.java`: Implemented Cloud-Only Mode matrix logic to suppress local file creation when `enableGcpLogExporter=true` and `LogPath` is omitted. - `BigQueryJdbcRootLogger.java`: Updated `setLevel` to handle `Level.OFF` properly and skip file handler creation if path is null. - `BigQueryConnection.java`: Attached `OpenTelemetryJulHandler` to the `"com.google.cloud.bigquery"` namespace during initialization. - `OpenTelemetryJulHandler.java`: Created a new handler that bridges JUL logs to OTel Logs API with context harvesting and connection ID filtering. - `pom.xml`: Added `google-cloud-logging` dependency with version `3.33.0-SNAPSHOT` and auto-update marker. - `OpenTelemetryJulHandlerTest.java`: Created unit tests using `OpenTelemetryExtension` to verify log emission and filtering.
1 parent 919183a commit 1adc224

8 files changed

Lines changed: 401 additions & 3 deletions

File tree

java-bigquery/google-cloud-bigquery-jdbc/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@
182182
<artifactId>google-cloud-bigquerystorage</artifactId>
183183
<version>3.28.0-SNAPSHOT</version><!-- {x-version-update:google-cloud-bigquerystorage:current} -->
184184
</dependency>
185+
<dependency>
186+
<groupId>com.google.cloud</groupId>
187+
<artifactId>google-cloud-logging</artifactId>
188+
<version>3.33.0-SNAPSHOT</version><!-- {x-version-update:google-cloud-logging:current} -->
189+
</dependency>
185190
<dependency>
186191
<groupId>com.google.http-client</groupId>
187192
<artifactId>google-http-client-apache-v5</artifactId>

java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,7 @@ private void closeImpl() throws SQLException {
954954
} finally {
955955
BigQueryJdbcMdc.removeInstance(this);
956956
BigQueryJdbcRootLogger.closeConnectionHandler(this.connectionId);
957+
BigQueryJdbcOpenTelemetry.unregisterConnection(this.connectionId);
957958
}
958959
this.isClosed = true;
959960
}
@@ -1056,6 +1057,12 @@ private BigQuery getBigQueryConnection() {
10561057
OpenTelemetry openTelemetry =
10571058
BigQueryJdbcOpenTelemetry.getOpenTelemetry(
10581059
this.enableGcpTraceExporter, this.enableGcpLogExporter, this.customOpenTelemetry);
1060+
1061+
if (this.enableGcpLogExporter || this.customOpenTelemetry != null) {
1062+
BigQueryJdbcOpenTelemetry.registerConnection(
1063+
this.connectionId, openTelemetry, null, this.enableGcpLogExporter);
1064+
}
1065+
10591066
if (this.enableGcpTraceExporter || this.customOpenTelemetry != null) {
10601067
this.tracer = BigQueryJdbcOpenTelemetry.getTracer(openTelemetry);
10611068
bigQueryOptions.setOpenTelemetryTracer(this.tracer);

java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ public Connection connect(String url, Properties info) throws SQLException {
157157
if (logPath == null) {
158158
logPath = System.getenv(BigQueryJdbcUrlUtility.LOG_PATH_ENV_VAR);
159159
}
160-
if (logPath == null) {
160+
161+
// Fallback to default path only if not specified and not in Cloud-Only mode
162+
if (logPath == null && !ds.getEnableGcpLogExporter()) {
161163
logPath = BigQueryJdbcUrlUtility.DEFAULT_LOG_PATH;
162164
}
163165

java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,77 @@
1616

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

19+
import com.google.cloud.logging.Logging;
1920
import io.opentelemetry.api.OpenTelemetry;
2021
import io.opentelemetry.api.trace.Tracer;
22+
import java.util.Collection;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.logging.Handler;
25+
import java.util.logging.Logger;
2126

2227
public class BigQueryJdbcOpenTelemetry {
2328

2429
static final String INSTRUMENTATION_SCOPE_NAME = "com.google.cloud.bigquery.jdbc";
30+
static final String BIGQUERY_NAMESPACE = "com.google.cloud.bigquery";
31+
public static final String CONNECTION_ID_BAGGAGE_KEY = "jdbc.connection_id";
32+
33+
static class TelemetryConfig {
34+
final OpenTelemetry openTelemetry;
35+
final Logging loggingClient;
36+
final boolean useDirectGcpLogging;
37+
38+
TelemetryConfig(
39+
OpenTelemetry openTelemetry, Logging loggingClient, boolean useDirectGcpLogging) {
40+
this.openTelemetry = openTelemetry;
41+
this.loggingClient = loggingClient;
42+
this.useDirectGcpLogging = useDirectGcpLogging;
43+
}
44+
}
45+
46+
private static final ConcurrentHashMap<String, TelemetryConfig> connectionConfigs =
47+
new ConcurrentHashMap<>();
2548

2649
private BigQueryJdbcOpenTelemetry() {}
2750

51+
static {
52+
ensureGlobalHandlerAttached();
53+
}
54+
55+
public static void ensureGlobalHandlerAttached() {
56+
Logger logger = Logger.getLogger(BIGQUERY_NAMESPACE);
57+
boolean present = false;
58+
for (Handler h : logger.getHandlers()) {
59+
if (h instanceof OpenTelemetryJulHandler) {
60+
present = true;
61+
break;
62+
}
63+
}
64+
if (!present) {
65+
logger.addHandler(new OpenTelemetryJulHandler());
66+
}
67+
}
68+
69+
public static void registerConnection(
70+
String connectionId,
71+
OpenTelemetry openTelemetry,
72+
Logging loggingClient,
73+
boolean useDirectGcpLogging) {
74+
connectionConfigs.put(
75+
connectionId, new TelemetryConfig(openTelemetry, loggingClient, useDirectGcpLogging));
76+
}
77+
78+
public static void unregisterConnection(String connectionId) {
79+
connectionConfigs.remove(connectionId);
80+
}
81+
82+
public static TelemetryConfig getConnectionConfig(String connectionId) {
83+
return connectionConfigs.get(connectionId);
84+
}
85+
86+
public static Collection<TelemetryConfig> getRegisteredConfigs() {
87+
return connectionConfigs.values();
88+
}
89+
2890
/**
2991
* Initializes or returns the OpenTelemetry instance based on hybrid logic. Prefer
3092
* customOpenTelemetry if provided; fallback to an auto-configured GCP exporter if requested.

java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,17 @@ public static Logger getRootLogger() {
138138

139139
public static void setLevel(Level level, String logPath) throws IOException {
140140
if (level != Level.OFF) {
141-
setPath(logPath, level);
142-
logger.setLevel(level);
141+
if (logPath != null) {
142+
setPath(logPath, level);
143+
}
143144
} else {
144145
for (Handler h : logger.getHandlers()) {
145146
h.close();
146147
logger.removeHandler(h);
147148
}
148149
fileHandler = null;
149150
}
151+
logger.setLevel(level);
150152
}
151153

152154
static void setPath(String logPath, Level level) {
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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.jdbc;
18+
19+
import com.google.cloud.logging.LogEntry;
20+
import com.google.cloud.logging.Logging;
21+
import com.google.cloud.logging.Payload;
22+
import io.opentelemetry.api.OpenTelemetry;
23+
import io.opentelemetry.api.baggage.Baggage;
24+
import io.opentelemetry.api.common.AttributeKey;
25+
import io.opentelemetry.api.logs.LogRecordBuilder;
26+
import io.opentelemetry.api.logs.Logger;
27+
import io.opentelemetry.api.logs.Severity;
28+
import io.opentelemetry.api.trace.Span;
29+
import io.opentelemetry.api.trace.SpanContext;
30+
import io.opentelemetry.context.Context;
31+
import java.time.Instant;
32+
import java.util.Collections;
33+
import java.util.logging.Handler;
34+
import java.util.logging.Level;
35+
import java.util.logging.LogRecord;
36+
37+
/**
38+
* Custom logging handler that bridges java.util.logging records to OpenTelemetry or Google Cloud
39+
* Logging. Extracts TraceId, SpanId, and Connection UUID from context.
40+
*/
41+
public class OpenTelemetryJulHandler extends Handler {
42+
43+
public OpenTelemetryJulHandler() {}
44+
45+
@Override
46+
public void publish(LogRecord record) {
47+
if (!isLoggable(record)) {
48+
return;
49+
}
50+
51+
try {
52+
// Extract connection ID from baggage
53+
String connectionId =
54+
Baggage.fromContext(Context.current())
55+
.getEntryValue(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY);
56+
57+
// Fallback to MDC if not in baggage (if MDC is available and used)
58+
if (connectionId == null) {
59+
connectionId = BigQueryJdbcMdc.getConnectionId();
60+
}
61+
62+
if (connectionId == null) {
63+
return;
64+
}
65+
66+
BigQueryJdbcOpenTelemetry.TelemetryConfig config =
67+
BigQueryJdbcOpenTelemetry.getConnectionConfig(connectionId);
68+
if (config == null) {
69+
return;
70+
}
71+
72+
if (config.useDirectGcpLogging && config.loggingClient != null) {
73+
publishToGcp(record, connectionId, config.loggingClient);
74+
} else if (config.openTelemetry != null) {
75+
publishToOTel(record, connectionId, config.openTelemetry);
76+
}
77+
} catch (Throwable t) {
78+
// Ignore exceptions to prevent breaking application logging or other handlers
79+
}
80+
}
81+
82+
private void publishToGcp(LogRecord record, String connectionId, Logging loggingClient) {
83+
Context context = Context.current();
84+
SpanContext spanContext = Span.fromContext(context).getSpanContext();
85+
String traceId = spanContext.isValid() ? spanContext.getTraceId() : null;
86+
String spanId = spanContext.isValid() ? spanContext.getSpanId() : null;
87+
88+
// TODO(b/491238299): May require refinement for structured logging or error handling
89+
90+
LogEntry.Builder builder =
91+
LogEntry.newBuilder(Payload.StringPayload.of(formatMessage(record)))
92+
.setSeverity(mapGcpSeverity(record.getLevel()))
93+
.setTimestamp(record.getMillis());
94+
95+
if (traceId != null) {
96+
builder.setTrace(traceId);
97+
}
98+
if (spanId != null) {
99+
builder.setSpanId(spanId);
100+
}
101+
if (connectionId != null) {
102+
builder.addLabel(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY, connectionId);
103+
}
104+
105+
loggingClient.write(Collections.singleton(builder.build()));
106+
}
107+
108+
private com.google.cloud.logging.Severity mapGcpSeverity(Level level) {
109+
if (level == Level.SEVERE) return com.google.cloud.logging.Severity.ERROR;
110+
if (level == Level.WARNING) return com.google.cloud.logging.Severity.WARNING;
111+
if (level == Level.INFO) return com.google.cloud.logging.Severity.INFO;
112+
if (level == Level.CONFIG) return com.google.cloud.logging.Severity.INFO;
113+
if (level == Level.FINE) return com.google.cloud.logging.Severity.DEBUG;
114+
return com.google.cloud.logging.Severity.DEBUG;
115+
}
116+
117+
private void publishToOTel(LogRecord record, String connectionId, OpenTelemetry openTelemetry) {
118+
String loggerName = record.getLoggerName();
119+
Logger logger =
120+
openTelemetry
121+
.getLogsBridge()
122+
.get(
123+
loggerName != null
124+
? loggerName
125+
: BigQueryJdbcOpenTelemetry.INSTRUMENTATION_SCOPE_NAME);
126+
127+
LogRecordBuilder builder =
128+
logger
129+
.logRecordBuilder()
130+
.setBody(formatMessage(record))
131+
.setSeverity(mapSeverity(record.getLevel()))
132+
.setTimestamp(Instant.ofEpochMilli(record.getMillis()))
133+
.setContext(Context.current());
134+
135+
if (connectionId != null) {
136+
builder.setAttribute(
137+
AttributeKey.stringKey(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY),
138+
connectionId);
139+
}
140+
141+
builder.emit();
142+
}
143+
144+
private Severity mapSeverity(Level level) {
145+
if (level == Level.SEVERE) return Severity.ERROR;
146+
if (level == Level.WARNING) return Severity.WARN;
147+
if (level == Level.INFO) return Severity.INFO;
148+
if (level == Level.CONFIG) return Severity.INFO;
149+
if (level == Level.FINE) return Severity.DEBUG;
150+
if (level == Level.FINER) return Severity.TRACE;
151+
if (level == Level.FINEST) return Severity.TRACE;
152+
return Severity.TRACE;
153+
}
154+
155+
private String formatMessage(LogRecord record) {
156+
String message = record.getMessage();
157+
Object[] params = record.getParameters();
158+
if (params != null && params.length > 0) {
159+
try {
160+
return java.text.MessageFormat.format(message, params);
161+
} catch (IllegalArgumentException e) {
162+
return message;
163+
}
164+
}
165+
return message;
166+
}
167+
168+
@Override
169+
public void flush() {
170+
for (BigQueryJdbcOpenTelemetry.TelemetryConfig config :
171+
BigQueryJdbcOpenTelemetry.getRegisteredConfigs()) {
172+
if (config.useDirectGcpLogging && config.loggingClient != null) {
173+
try {
174+
config.loggingClient.flush();
175+
} catch (Exception e) {
176+
// Ignore failures during flush to protect other connections
177+
}
178+
}
179+
}
180+
}
181+
182+
@Override
183+
public void close() throws SecurityException {
184+
// TODO(b/491238299): Implement with gcp exporter logic
185+
}
186+
}

java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggingBaseTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public abstract class BigQueryJdbcLoggingBaseTest extends BigQueryJdbcBaseTest {
3434
@BeforeEach
3535
public void setUpLogValidator() {
3636
logger = BigQueryJdbcRootLogger.getRootLogger();
37+
logger.setLevel(java.util.logging.Level.ALL);
3738
capturedLogs.clear();
3839
threadId = Thread.currentThread().getId();
3940
handler =

0 commit comments

Comments
 (0)