diff --git a/java-bigquery/google-cloud-bigquery-jdbc/pom.xml b/java-bigquery/google-cloud-bigquery-jdbc/pom.xml
index 74f3df2fff9f..b7e17dea02f8 100644
--- a/java-bigquery/google-cloud-bigquery-jdbc/pom.xml
+++ b/java-bigquery/google-cloud-bigquery-jdbc/pom.xml
@@ -182,6 +182,11 @@
google-cloud-bigquerystorage
3.28.0-SNAPSHOT
+
+ com.google.cloud
+ google-cloud-logging
+ 3.33.0-SNAPSHOT
+
com.google.http-client
google-http-client-apache-v5
diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java
index 6f17d3d6d195..97c01edc2e3e 100644
--- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java
+++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java
@@ -954,6 +954,7 @@ private void closeImpl() throws SQLException {
} finally {
BigQueryJdbcMdc.removeInstance(this);
BigQueryJdbcRootLogger.closeConnectionHandler(this.connectionId);
+ BigQueryJdbcOpenTelemetry.unregisterConnection(this.connectionId);
}
this.isClosed = true;
}
@@ -1056,6 +1057,12 @@ private BigQuery getBigQueryConnection() {
OpenTelemetry openTelemetry =
BigQueryJdbcOpenTelemetry.getOpenTelemetry(
this.enableGcpTraceExporter, this.enableGcpLogExporter, this.customOpenTelemetry);
+
+ if (this.enableGcpLogExporter || this.customOpenTelemetry != null) {
+ BigQueryJdbcOpenTelemetry.registerConnection(
+ this.connectionId, openTelemetry, null, this.enableGcpLogExporter);
+ }
+
if (this.enableGcpTraceExporter || this.customOpenTelemetry != null) {
this.tracer = BigQueryJdbcOpenTelemetry.getTracer(openTelemetry);
bigQueryOptions.setOpenTelemetryTracer(this.tracer);
diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java
index a83bdc5093e6..1e3146d16a8e 100644
--- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java
+++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDriver.java
@@ -157,7 +157,9 @@ public Connection connect(String url, Properties info) throws SQLException {
if (logPath == null) {
logPath = System.getenv(BigQueryJdbcUrlUtility.LOG_PATH_ENV_VAR);
}
- if (logPath == null) {
+
+ // Fallback to default path only if not specified and not in Cloud-Only mode
+ if (logPath == null && !ds.getEnableGcpLogExporter()) {
logPath = BigQueryJdbcUrlUtility.DEFAULT_LOG_PATH;
}
diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java
index 181e15629c5b..3216b25d1557 100644
--- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java
+++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java
@@ -16,15 +16,77 @@
package com.google.cloud.bigquery.jdbc;
+import com.google.cloud.logging.Logging;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Handler;
+import java.util.logging.Logger;
public class BigQueryJdbcOpenTelemetry {
static final String INSTRUMENTATION_SCOPE_NAME = "com.google.cloud.bigquery.jdbc";
+ static final String BIGQUERY_NAMESPACE = "com.google.cloud.bigquery";
+ public static final String CONNECTION_ID_BAGGAGE_KEY = "jdbc.connection_id";
+
+ static class TelemetryConfig {
+ final OpenTelemetry openTelemetry;
+ final Logging loggingClient;
+ final boolean useDirectGcpLogging;
+
+ TelemetryConfig(
+ OpenTelemetry openTelemetry, Logging loggingClient, boolean useDirectGcpLogging) {
+ this.openTelemetry = openTelemetry;
+ this.loggingClient = loggingClient;
+ this.useDirectGcpLogging = useDirectGcpLogging;
+ }
+ }
+
+ private static final ConcurrentHashMap connectionConfigs =
+ new ConcurrentHashMap<>();
private BigQueryJdbcOpenTelemetry() {}
+ static {
+ ensureGlobalHandlerAttached();
+ }
+
+ public static void ensureGlobalHandlerAttached() {
+ Logger logger = Logger.getLogger(BIGQUERY_NAMESPACE);
+ boolean present = false;
+ for (Handler h : logger.getHandlers()) {
+ if (h instanceof OpenTelemetryJulHandler) {
+ present = true;
+ break;
+ }
+ }
+ if (!present) {
+ logger.addHandler(new OpenTelemetryJulHandler());
+ }
+ }
+
+ public static void registerConnection(
+ String connectionId,
+ OpenTelemetry openTelemetry,
+ Logging loggingClient,
+ boolean useDirectGcpLogging) {
+ connectionConfigs.put(
+ connectionId, new TelemetryConfig(openTelemetry, loggingClient, useDirectGcpLogging));
+ }
+
+ public static void unregisterConnection(String connectionId) {
+ connectionConfigs.remove(connectionId);
+ }
+
+ public static TelemetryConfig getConnectionConfig(String connectionId) {
+ return connectionConfigs.get(connectionId);
+ }
+
+ public static Collection getRegisteredConfigs() {
+ return connectionConfigs.values();
+ }
+
/**
* Initializes or returns the OpenTelemetry instance based on hybrid logic. Prefer
* customOpenTelemetry if provided; fallback to an auto-configured GCP exporter if requested.
diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java
index 32772521e9c2..f13713fe0004 100644
--- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java
+++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java
@@ -138,8 +138,9 @@ public static Logger getRootLogger() {
public static void setLevel(Level level, String logPath) throws IOException {
if (level != Level.OFF) {
- setPath(logPath, level);
- logger.setLevel(level);
+ if (logPath != null) {
+ setPath(logPath, level);
+ }
} else {
for (Handler h : logger.getHandlers()) {
h.close();
@@ -147,6 +148,7 @@ public static void setLevel(Level level, String logPath) throws IOException {
}
fileHandler = null;
}
+ logger.setLevel(level);
}
static void setPath(String logPath, Level level) {
diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/OpenTelemetryJulHandler.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/OpenTelemetryJulHandler.java
new file mode 100644
index 000000000000..b6be12f972af
--- /dev/null
+++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/OpenTelemetryJulHandler.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigquery.jdbc;
+
+import com.google.cloud.logging.LogEntry;
+import com.google.cloud.logging.Logging;
+import com.google.cloud.logging.Payload;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.baggage.Baggage;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.logs.LogRecordBuilder;
+import io.opentelemetry.api.logs.Logger;
+import io.opentelemetry.api.logs.Severity;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.context.Context;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+/**
+ * Custom logging handler that bridges java.util.logging records to OpenTelemetry or Google Cloud
+ * Logging. Extracts TraceId, SpanId, and Connection UUID from context.
+ */
+public class OpenTelemetryJulHandler extends Handler {
+
+ public OpenTelemetryJulHandler() {}
+
+ @Override
+ public void publish(LogRecord record) {
+ if (!isLoggable(record)) {
+ return;
+ }
+
+ try {
+ // Extract connection ID from baggage
+ String connectionId =
+ Baggage.fromContext(Context.current())
+ .getEntryValue(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY);
+
+ // Fallback to MDC if not in baggage (if MDC is available and used)
+ if (connectionId == null) {
+ connectionId = BigQueryJdbcMdc.getConnectionId();
+ }
+
+ if (connectionId == null) {
+ return;
+ }
+
+ BigQueryJdbcOpenTelemetry.TelemetryConfig config =
+ BigQueryJdbcOpenTelemetry.getConnectionConfig(connectionId);
+ if (config == null) {
+ return;
+ }
+
+ if (config.useDirectGcpLogging && config.loggingClient != null) {
+ publishToGcp(record, connectionId, config.loggingClient);
+ } else if (config.openTelemetry != null) {
+ publishToOTel(record, connectionId, config.openTelemetry);
+ }
+ } catch (Throwable t) {
+ // Ignore exceptions to prevent breaking application logging or other handlers
+ }
+ }
+
+ private void publishToGcp(LogRecord record, String connectionId, Logging loggingClient) {
+ Context context = Context.current();
+ SpanContext spanContext = Span.fromContext(context).getSpanContext();
+ String traceId = spanContext.isValid() ? spanContext.getTraceId() : null;
+ String spanId = spanContext.isValid() ? spanContext.getSpanId() : null;
+
+ // TODO(b/491238299): May require refinement for structured logging or error handling
+
+ LogEntry.Builder builder =
+ LogEntry.newBuilder(Payload.StringPayload.of(formatMessage(record)))
+ .setSeverity(mapGcpSeverity(record.getLevel()))
+ .setTimestamp(record.getMillis());
+
+ if (traceId != null) {
+ builder.setTrace(traceId);
+ }
+ if (spanId != null) {
+ builder.setSpanId(spanId);
+ }
+ if (connectionId != null) {
+ builder.addLabel(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY, connectionId);
+ }
+
+ loggingClient.write(Collections.singleton(builder.build()));
+ }
+
+ private com.google.cloud.logging.Severity mapGcpSeverity(Level level) {
+ if (level == Level.SEVERE) return com.google.cloud.logging.Severity.ERROR;
+ if (level == Level.WARNING) return com.google.cloud.logging.Severity.WARNING;
+ if (level == Level.INFO) return com.google.cloud.logging.Severity.INFO;
+ if (level == Level.CONFIG) return com.google.cloud.logging.Severity.INFO;
+ if (level == Level.FINE) return com.google.cloud.logging.Severity.DEBUG;
+ return com.google.cloud.logging.Severity.DEBUG;
+ }
+
+ private void publishToOTel(LogRecord record, String connectionId, OpenTelemetry openTelemetry) {
+ String loggerName = record.getLoggerName();
+ Logger logger =
+ openTelemetry
+ .getLogsBridge()
+ .get(
+ loggerName != null
+ ? loggerName
+ : BigQueryJdbcOpenTelemetry.INSTRUMENTATION_SCOPE_NAME);
+
+ LogRecordBuilder builder =
+ logger
+ .logRecordBuilder()
+ .setBody(formatMessage(record))
+ .setSeverity(mapSeverity(record.getLevel()))
+ .setTimestamp(Instant.ofEpochMilli(record.getMillis()))
+ .setContext(Context.current());
+
+ if (connectionId != null) {
+ builder.setAttribute(
+ AttributeKey.stringKey(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY),
+ connectionId);
+ }
+
+ builder.emit();
+ }
+
+ private Severity mapSeverity(Level level) {
+ if (level == Level.SEVERE) return Severity.ERROR;
+ if (level == Level.WARNING) return Severity.WARN;
+ if (level == Level.INFO) return Severity.INFO;
+ if (level == Level.CONFIG) return Severity.INFO;
+ if (level == Level.FINE) return Severity.DEBUG;
+ if (level == Level.FINER) return Severity.TRACE;
+ if (level == Level.FINEST) return Severity.TRACE;
+ return Severity.TRACE;
+ }
+
+ private String formatMessage(LogRecord record) {
+ String message = record.getMessage();
+ Object[] params = record.getParameters();
+ if (params != null && params.length > 0) {
+ try {
+ return java.text.MessageFormat.format(message, params);
+ } catch (IllegalArgumentException e) {
+ return message;
+ }
+ }
+ return message;
+ }
+
+ @Override
+ public void flush() {
+ for (BigQueryJdbcOpenTelemetry.TelemetryConfig config :
+ BigQueryJdbcOpenTelemetry.getRegisteredConfigs()) {
+ if (config.useDirectGcpLogging && config.loggingClient != null) {
+ try {
+ config.loggingClient.flush();
+ } catch (Exception e) {
+ // Ignore failures during flush to protect other connections
+ }
+ }
+ }
+ }
+
+ @Override
+ public void close() throws SecurityException {
+ // TODO(b/491238299): Implement with gcp exporter logic
+ }
+}
diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggingBaseTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggingBaseTest.java
index b97a7bd1e0f4..2196363663fb 100644
--- a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggingBaseTest.java
+++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcLoggingBaseTest.java
@@ -34,6 +34,7 @@ public abstract class BigQueryJdbcLoggingBaseTest extends BigQueryJdbcBaseTest {
@BeforeEach
public void setUpLogValidator() {
logger = BigQueryJdbcRootLogger.getRootLogger();
+ logger.setLevel(java.util.logging.Level.ALL);
capturedLogs.clear();
threadId = Thread.currentThread().getId();
handler =
diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/OpenTelemetryJulHandlerTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/OpenTelemetryJulHandlerTest.java
new file mode 100644
index 000000000000..a404ffeb2c73
--- /dev/null
+++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/OpenTelemetryJulHandlerTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.bigquery.jdbc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.google.cloud.logging.LogEntry;
+import com.google.cloud.logging.Logging;
+import com.google.cloud.logging.Payload;
+import io.opentelemetry.api.baggage.Baggage;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.logs.Severity;
+import io.opentelemetry.context.Scope;
+import io.opentelemetry.sdk.logs.data.LogRecordData;
+import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension;
+import java.util.List;
+import java.util.logging.Logger;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.ArgumentCaptor;
+
+public class OpenTelemetryJulHandlerTest {
+
+ @RegisterExtension
+ static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create();
+
+ private static final Logger logger = Logger.getLogger("com.google.cloud.bigquery");
+
+ @BeforeEach
+ public void setUp() {
+ logger.setLevel(java.util.logging.Level.ALL);
+ BigQueryJdbcOpenTelemetry.ensureGlobalHandlerAttached();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ BigQueryJdbcOpenTelemetry.unregisterConnection("test-uuid");
+ BigQueryJdbcOpenTelemetry.unregisterConnection("wrong-uuid");
+ BigQueryJdbcOpenTelemetry.unregisterConnection("gcp-uuid");
+ }
+
+ @Test
+ public void testPublishToOTel() {
+ BigQueryJdbcOpenTelemetry.registerConnection(
+ "test-uuid", otelTesting.getOpenTelemetry(), null, false);
+
+ BigQueryConnection mockConnection = mock(BigQueryConnection.class);
+ Baggage baggage =
+ Baggage.builder()
+ .put(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY, "test-uuid")
+ .build();
+ try (Scope scope = baggage.makeCurrent();
+ BigQueryJdbcMdc.MdcCloseable mdcScope =
+ BigQueryJdbcMdc.registerInstance(mockConnection, "test-uuid")) {
+ logger.info("Test message");
+ }
+
+ List logs = otelTesting.getLogRecords();
+ assertEquals(1, logs.size());
+ LogRecordData log = logs.get(0);
+ assertEquals("Test message", log.getBody().asString());
+ assertEquals(Severity.INFO, log.getSeverity());
+ assertEquals(
+ "test-uuid",
+ log.getAttributes()
+ .get(AttributeKey.stringKey(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY)));
+ }
+
+ @Test
+ public void testPublishWithFiltering() {
+ // Register for "test-uuid"
+ BigQueryJdbcOpenTelemetry.registerConnection(
+ "test-uuid", otelTesting.getOpenTelemetry(), null, false);
+
+ // Log with WRONG connection ID
+ Baggage baggage =
+ Baggage.builder()
+ .put(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY, "wrong-uuid")
+ .build();
+ try (Scope scope = baggage.makeCurrent()) {
+ logger.info("Test message");
+ }
+
+ List logs = otelTesting.getLogRecords();
+ assertTrue(logs.isEmpty()); // Should be filtered out because "wrong-uuid" has no config
+ }
+
+ @Test
+ public void testPublishToGcp() {
+ Logging loggingClient = mock(Logging.class);
+ BigQueryJdbcOpenTelemetry.registerConnection(
+ "gcp-uuid", otelTesting.getOpenTelemetry(), loggingClient, true);
+
+ BigQueryConnection mockConnection = mock(BigQueryConnection.class);
+ Baggage baggage =
+ Baggage.builder()
+ .put(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY, "gcp-uuid")
+ .build();
+ try (Scope scope = baggage.makeCurrent();
+ BigQueryJdbcMdc.MdcCloseable mdcScope =
+ BigQueryJdbcMdc.registerInstance(mockConnection, "gcp-uuid")) {
+ logger.info("Test message");
+ }
+
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(Iterable.class);
+ verify(loggingClient).write(captor.capture());
+
+ Iterable entries = captor.getValue();
+ LogEntry entry = entries.iterator().next();
+
+ assertEquals("Test message", ((Payload.StringPayload) entry.getPayload()).getData());
+ assertEquals(com.google.cloud.logging.Severity.INFO, entry.getSeverity());
+ }
+}