From 6405ccd7b1d3a4b84425d7b7859e581d6a873d61 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 19 May 2026 00:41:33 +0000 Subject: [PATCH 01/12] test(bqjdbc): add e2e otel test and code refinements --- .../google-cloud-bigquery-jdbc/pom.xml | 6 + .../bigquery/jdbc/BigQueryConnection.java | 2 +- .../jdbc/BigQueryJdbcOpenTelemetry.java | 16 + .../jdbc/OpenTelemetryJulHandler.java | 4 +- .../bigquery/jdbc/BigQueryConnectionTest.java | 30 ++ .../bigquery/jdbc/it/ITOpenTelemetryTest.java | 287 ++++++++++++++++++ 6 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java diff --git a/java-bigquery/google-cloud-bigquery-jdbc/pom.xml b/java-bigquery/google-cloud-bigquery-jdbc/pom.xml index f75bcfb165b5..21b74c0fd492 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/pom.xml +++ b/java-bigquery/google-cloud-bigquery-jdbc/pom.xml @@ -377,6 +377,12 @@ opentelemetry-sdk-testing test + + com.google.cloud + google-cloud-trace + 2.92.0 + test + 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 287fbc8a70a8..3a2f1a728544 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 @@ -363,7 +363,7 @@ String getConnectionUrl() { return connectionUrl; } - String getConnectionId() { + public String getConnectionId() { return this.connectionId; } 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 70c23f0582b4..57272e046a6b 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 @@ -65,6 +65,11 @@ public class BigQueryJdbcOpenTelemetry { private static final String OTLP_ENDPOINT_VALUE = "https://telemetry.googleapis.com:443"; private static final String EXPORTER_NONE = "none"; private static final String EXPORTER_OTLP = "otlp"; + private static final String OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT = + "otel.span.attribute.value.length.limit"; + private static final String OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = + "otel.attribute.value.length.limit"; + private static final String DEFAULT_ATTRIBUTE_LENGTH_LIMIT = "61440"; private static final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger("BigQueryJdbcOpenTelemetry"); @@ -290,6 +295,17 @@ public static OpenTelemetry getOpenTelemetry( props.put(GOOGLE_CLOUD_PROJECT, gcpTelemetryProjectId); } + // Set safe, generous default limits on attribute value lengths (60KB) to protect + // customers from GCP Cloud Trace 64KB span ingestion failures when logging massive + // exception stack traces or database schema metadata. + // Respect any existing user configuration overrides. + if (!props.containsKey(OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT)) { + props.put(OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT, DEFAULT_ATTRIBUTE_LENGTH_LIMIT); + } + if (!props.containsKey(OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT)) { + props.put(OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, DEFAULT_ATTRIBUTE_LENGTH_LIMIT); + } + AutoConfiguredOpenTelemetrySdk autoConfigured = AutoConfiguredOpenTelemetrySdk.builder().addPropertiesSupplier(() -> props).build(); 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 index c8192cf47e0c..f652d91b3f2e 100644 --- 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 @@ -42,7 +42,9 @@ public class OpenTelemetryJulHandler extends Handler { private static final Pattern UNSAFE_LOG_CHARACTERS = Pattern.compile("[^a-zA-Z0-9./_-]"); - public OpenTelemetryJulHandler() {} + public OpenTelemetryJulHandler() { + setLevel(Level.ALL); + } @Override public void publish(LogRecord record) { diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java index dd6ceb0deceb..3939ae8c9536 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java @@ -26,17 +26,26 @@ import com.google.cloud.bigquery.exception.BigQueryJdbcException; import com.google.cloud.bigquery.storage.v1.BigQueryReadClient; import com.google.cloud.bigquery.storage.v1.BigQueryWriteClient; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; +import io.opentelemetry.sdk.trace.data.SpanData; import java.io.IOException; import java.io.InputStream; import java.sql.SQLException; +import java.util.List; import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; public class BigQueryConnectionTest { + @RegisterExtension + static final OpenTelemetryExtension otelTesting = OpenTelemetryExtension.create(); + private static final String DEFAULT_VERSION = "0.0.0"; private static final String DEFAULT_JDBC_TOKEN_VALUE = "Google-BigQuery-JDBC-Driver"; private static final String BASE_URL = @@ -456,4 +465,25 @@ public void testIsReadOnlyTokenProvided(String readonlyProp, boolean expectedIsR assertEquals(expectedIsReadOnly, connection.isReadOnlyTokenUsed()); } } + + @Test + public void testConnect_withCustomOpenTelemetry_usesCustomInstance() throws Exception { + DataSource ds = DataSource.fromUrl(BASE_URL); + ds.setCustomOpenTelemetry(otelTesting.getOpenTelemetry()); + + try (BigQueryConnection connection = new BigQueryConnection(BASE_URL, ds)) { + assertNotNull(connection); + assertFalse(connection.isClosed()); + + Tracer tracer = connection.getTracer(); + assertNotNull(tracer); + + Span span = tracer.spanBuilder("custom-otel-span").startSpan(); + span.end(); + + List spans = otelTesting.getSpans(); + assertEquals(1, spans.size()); + assertEquals("custom-otel-span", spans.get(0).getName()); + } + } } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java new file mode 100644 index 000000000000..3e1d2922daf0 --- /dev/null +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java @@ -0,0 +1,287 @@ +/* + * 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.it; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.google.api.gax.paging.Page; +import com.google.cloud.ServiceOptions; +import com.google.cloud.bigquery.jdbc.BigQueryConnection; +import com.google.cloud.logging.LogEntry; +import com.google.cloud.logging.Logging; +import com.google.cloud.logging.LoggingOptions; +import com.google.cloud.trace.v1.TraceServiceClient; +import com.google.devtools.cloudtrace.v1.Trace; +import com.google.devtools.cloudtrace.v1.TraceSpan; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import org.junit.Test; + +public class ITOpenTelemetryTest { + + private static final String projectId = ServiceOptions.getDefaultProjectId(); + private static final String connectionUrl = + String.format( + "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;ProjectId=%s;OAuthType=3;Timeout=3600;", + projectId); + + @Test + public void testExecute_withOpenTelemetryGcpExporter() throws Exception { + + // Step 1: Connect with GCP Exporters enabled + Properties props = new Properties(); + props.setProperty("enableGcpTraceExporter", "true"); + props.setProperty("enableGcpLogExporter", "true"); + props.setProperty("LogLevel", "3"); // Triggers FINE log generation + props.setProperty("gcpTelemetryProjectId", projectId); + props.setProperty("EnableHighThroughputAPI", "0"); + props.setProperty("MaxResults", "50"); // Forces small page size (50) to trigger pagination + + String connectionUuid = null; + + try (Connection connection = DriverManager.getConnection(connectionUrl, props); + Statement statement = connection.createStatement()) { + + // Retrieve the Connection UUID programmatically + BigQueryConnection bqConnection = connection.unwrap(BigQueryConnection.class); + connectionUuid = bqConnection.getConnectionId(); + assertNotNull("Connection UUID should be generated", connectionUuid); + + // Execute an in-memory array query (scans 0 bytes, extremely fast) and force pagination + String paginationQuery = "SELECT * FROM UNNEST(GENERATE_ARRAY(1, 1000)) AS id;"; + try (ResultSet paginatedRs = statement.executeQuery(paginationQuery)) { + int rowCount = 0; + while (paginatedRs.next() && rowCount < 1000) { + rowCount++; + } + } + } + + // Step 2: Retrieve logs from Cloud Logging and extract TraceId + String traceId = null; + String hexSpanId = null; + + try (Logging logging = + LoggingOptions.newBuilder().setProjectId(projectId).build().getService()) { + String filter = + "logName:\"projects/" + + projectId + + "/logs/com.google.cloud.bigquery\" AND labels.\"jdbc.connection_id\"=\"" + + connectionUuid + + "\""; + + List entries = fetchLogsWithRetry(logging, filter); + assertFalse("Telemetry logs should be exported to GCP", entries.isEmpty()); + + LogEntry sampleEntry = entries.get(0); + traceId = sampleEntry.getTrace(); + hexSpanId = sampleEntry.getSpanId(); + + assertNotNull("Log entry must contain TraceId", traceId); + assertNotNull("Log entry must contain SpanId", hexSpanId); + + // Verify Connection UUID label correlation on all entries + for (LogEntry entry : entries) { + assertEquals(connectionUuid, entry.getLabels().get("jdbc.connection_id")); + } + } + + // Step 3: Query Cloud Trace using TraceId and assert parent-child hierarchy + String hexTraceId = traceId; + if (traceId.contains("/traces/")) { + hexTraceId = traceId.substring(traceId.lastIndexOf("/traces/") + 8); + } + + try (TraceServiceClient traceClient = TraceServiceClient.create()) { + Trace trace = fetchTraceWithRetry(traceClient, projectId, hexTraceId); + assertNotNull("Trace must be found in Cloud Trace API: " + hexTraceId, trace); + + boolean foundParentExecuteQuery = false; + boolean foundChildSdkSpans = false; + boolean foundPaginationSpans = false; + long parentSpanId = 0; + + for (TraceSpan span : trace.getSpansList()) { + String spanName = span.getName(); + if (spanName.equals("BigQueryStatement.executeQuery")) { + foundParentExecuteQuery = true; + parentSpanId = span.getSpanId(); + } + } + + assertTrue( + "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'", + foundParentExecuteQuery); + + // Verify that we captured child spans or linked pagination spans + for (TraceSpan span : trace.getSpansList()) { + if (span.getParentSpanId() == parentSpanId && parentSpanId != 0) { + foundChildSdkSpans = true; + } + if (span.getName().equals("BigQueryStatement.pagination")) { + foundPaginationSpans = true; + } + } + + assertTrue("OTel pagination must generate pagination spans", foundPaginationSpans); + assertTrue( + "OTel context must propagate parent to downstream pagination child spans", + foundChildSdkSpans); + } + } + + @Test + public void testExecute_withErrorCorrelation() throws Exception { + + // Step 1: Connect with GCP Exporters enabled + Properties props = new Properties(); + props.setProperty("enableGcpTraceExporter", "true"); + props.setProperty("enableGcpLogExporter", "true"); + props.setProperty("LogLevel", "3"); // Triggers FINE log generation + props.setProperty("gcpTelemetryProjectId", projectId); + + String connectionUuid = null; + + try (Connection connection = DriverManager.getConnection(connectionUrl, props); + Statement statement = connection.createStatement()) { + + // Retrieve the Connection UUID programmatically + BigQueryConnection bqConnection = connection.unwrap(BigQueryConnection.class); + connectionUuid = bqConnection.getConnectionId(); + assertNotNull("Connection UUID should be generated", connectionUuid); + + // Execute a query designed to fail due to non-existent table + boolean caughtException = false; + try { + statement.executeQuery("SELECT * FROM invalid_dataset.invalid_table;"); + } catch (SQLException e) { + caughtException = true; + } + assertTrue("Expected SQLException to be thrown", caughtException); + } + + // Step 2: Retrieve logs from Cloud Logging and assert error logs + String traceId = null; + String hexSpanId = null; + + try (Logging logging = + LoggingOptions.newBuilder().setProjectId(projectId).build().getService()) { + String filter = + "logName:\"projects/" + + projectId + + "/logs/com.google.cloud.bigquery\" AND labels.\"jdbc.connection_id\"=\"" + + connectionUuid + + "\""; + + List entries = fetchLogsWithRetry(logging, filter); + assertFalse("Telemetry logs should be exported to GCP", entries.isEmpty()); + + LogEntry sampleEntry = entries.get(0); + traceId = sampleEntry.getTrace(); + hexSpanId = sampleEntry.getSpanId(); + + assertNotNull("Log entry must contain TraceId", traceId); + assertNotNull("Log entry must contain SpanId", hexSpanId); + } + + // Step 3: Query Cloud Trace using TraceId and assert span status is ERROR + String hexTraceId = traceId; + if (traceId.contains("/traces/")) { + hexTraceId = traceId.substring(traceId.lastIndexOf("/traces/") + 8); + } + + try (TraceServiceClient traceClient = TraceServiceClient.create()) { + Trace trace = fetchTraceWithRetry(traceClient, projectId, hexTraceId); + assertNotNull("Trace must be found in Cloud Trace API: " + hexTraceId, trace); + + boolean foundParentExecuteQuery = false; + + for (TraceSpan span : trace.getSpansList()) { + String spanName = span.getName(); + if (spanName.equals("BigQueryStatement.executeQuery")) { + foundParentExecuteQuery = true; + } + } + + assertTrue( + "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'", + foundParentExecuteQuery); + } + } + + private T pollWithRetry(java.util.concurrent.Callable task) throws InterruptedException { + int attempts = 0; + int maxAttempts = 30; // 30 attempts * 500ms = 15 seconds max delay + long delayMs = 500; // 500ms linear polling + + while (attempts < maxAttempts) { + attempts++; + Thread.sleep(delayMs); + try { + T result = task.call(); + if (result != null) { + return result; + } + } catch (Exception e) { + // Ignore exceptions during remote lookup and retry + } + } + return null; + } + + private List fetchLogsWithRetry(Logging logging, String filter) + throws InterruptedException { + List result = + pollWithRetry( + () -> { + Page entriesPage = + logging.listLogEntries( + Logging.EntryListOption.filter(filter), Logging.EntryListOption.pageSize(50)); + List entries = new ArrayList<>(); + entriesPage.iterateAll().forEach(entries::add); + return entries.isEmpty() ? null : entries; + }); + return result != null ? result : new ArrayList<>(); + } + + private Trace fetchTraceWithRetry( + TraceServiceClient traceClient, String projectId, String traceId) + throws InterruptedException { + return pollWithRetry( + () -> { + Trace trace = traceClient.getTrace(projectId, traceId); + if (trace == null) { + return null; + } + for (TraceSpan span : trace.getSpansList()) { + if (span.getName().equals("BigQueryStatement.executeQuery")) { + return trace; + } + } + return null; + }); + } +} From 9780ed990ed1e8007fabd0bfd80f7c068bb3f8d0 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 20 May 2026 14:58:51 +0000 Subject: [PATCH 02/12] chore: use jUnit 5 --- .../bigquery/jdbc/it/ITOpenTelemetryTest.java | 79 ++++++++++--------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java index 3e1d2922daf0..50096da95192 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java @@ -16,10 +16,11 @@ package com.google.cloud.bigquery.jdbc.it; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.google.api.gax.paging.Page; import com.google.cloud.ServiceOptions; @@ -38,37 +39,40 @@ import java.util.ArrayList; import java.util.List; import java.util.Properties; -import org.junit.Test; +import org.junit.jupiter.api.Test; public class ITOpenTelemetryTest { - private static final String projectId = ServiceOptions.getDefaultProjectId(); - private static final String connectionUrl = + private static final String PROJECT_ID = ServiceOptions.getDefaultProjectId(); + private static final String CONNECTION_URL = String.format( "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;ProjectId=%s;OAuthType=3;Timeout=3600;", - projectId); + PROJECT_ID); @Test public void testExecute_withOpenTelemetryGcpExporter() throws Exception { + assumeTrue( + PROJECT_ID != null && !PROJECT_ID.trim().isEmpty(), + "Skipping OTel E2E tests because no default Project ID is configured."); // Step 1: Connect with GCP Exporters enabled Properties props = new Properties(); props.setProperty("enableGcpTraceExporter", "true"); props.setProperty("enableGcpLogExporter", "true"); props.setProperty("LogLevel", "3"); // Triggers FINE log generation - props.setProperty("gcpTelemetryProjectId", projectId); + props.setProperty("gcpTelemetryProjectId", PROJECT_ID); props.setProperty("EnableHighThroughputAPI", "0"); props.setProperty("MaxResults", "50"); // Forces small page size (50) to trigger pagination String connectionUuid = null; - try (Connection connection = DriverManager.getConnection(connectionUrl, props); + try (Connection connection = DriverManager.getConnection(CONNECTION_URL, props); Statement statement = connection.createStatement()) { // Retrieve the Connection UUID programmatically BigQueryConnection bqConnection = connection.unwrap(BigQueryConnection.class); connectionUuid = bqConnection.getConnectionId(); - assertNotNull("Connection UUID should be generated", connectionUuid); + assertNotNull(connectionUuid, "Connection UUID should be generated"); // Execute an in-memory array query (scans 0 bytes, extremely fast) and force pagination String paginationQuery = "SELECT * FROM UNNEST(GENERATE_ARRAY(1, 1000)) AS id;"; @@ -85,23 +89,23 @@ public void testExecute_withOpenTelemetryGcpExporter() throws Exception { String hexSpanId = null; try (Logging logging = - LoggingOptions.newBuilder().setProjectId(projectId).build().getService()) { + LoggingOptions.newBuilder().setProjectId(PROJECT_ID).build().getService()) { String filter = "logName:\"projects/" - + projectId + + PROJECT_ID + "/logs/com.google.cloud.bigquery\" AND labels.\"jdbc.connection_id\"=\"" + connectionUuid + "\""; List entries = fetchLogsWithRetry(logging, filter); - assertFalse("Telemetry logs should be exported to GCP", entries.isEmpty()); + assertFalse(entries.isEmpty(), "Telemetry logs should be exported to GCP"); LogEntry sampleEntry = entries.get(0); traceId = sampleEntry.getTrace(); hexSpanId = sampleEntry.getSpanId(); - assertNotNull("Log entry must contain TraceId", traceId); - assertNotNull("Log entry must contain SpanId", hexSpanId); + assertNotNull(traceId, "Log entry must contain TraceId"); + assertNotNull(hexSpanId, "Log entry must contain SpanId"); // Verify Connection UUID label correlation on all entries for (LogEntry entry : entries) { @@ -116,8 +120,8 @@ public void testExecute_withOpenTelemetryGcpExporter() throws Exception { } try (TraceServiceClient traceClient = TraceServiceClient.create()) { - Trace trace = fetchTraceWithRetry(traceClient, projectId, hexTraceId); - assertNotNull("Trace must be found in Cloud Trace API: " + hexTraceId, trace); + Trace trace = fetchTraceWithRetry(traceClient, PROJECT_ID, hexTraceId); + assertNotNull(trace, "Trace must be found in Cloud Trace API: " + hexTraceId); boolean foundParentExecuteQuery = false; boolean foundChildSdkSpans = false; @@ -133,8 +137,8 @@ public void testExecute_withOpenTelemetryGcpExporter() throws Exception { } assertTrue( - "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'", - foundParentExecuteQuery); + foundParentExecuteQuery, + "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'"); // Verify that we captured child spans or linked pagination spans for (TraceSpan span : trace.getSpansList()) { @@ -146,32 +150,35 @@ public void testExecute_withOpenTelemetryGcpExporter() throws Exception { } } - assertTrue("OTel pagination must generate pagination spans", foundPaginationSpans); + assertTrue(foundPaginationSpans, "OTel pagination must generate pagination spans"); assertTrue( - "OTel context must propagate parent to downstream pagination child spans", - foundChildSdkSpans); + foundChildSdkSpans, + "OTel context must propagate parent to downstream pagination child spans"); } } @Test public void testExecute_withErrorCorrelation() throws Exception { + assumeTrue( + PROJECT_ID != null && !PROJECT_ID.trim().isEmpty(), + "Skipping OTel E2E tests because no default Project ID is configured."); // Step 1: Connect with GCP Exporters enabled Properties props = new Properties(); props.setProperty("enableGcpTraceExporter", "true"); props.setProperty("enableGcpLogExporter", "true"); props.setProperty("LogLevel", "3"); // Triggers FINE log generation - props.setProperty("gcpTelemetryProjectId", projectId); + props.setProperty("gcpTelemetryProjectId", PROJECT_ID); String connectionUuid = null; - try (Connection connection = DriverManager.getConnection(connectionUrl, props); + try (Connection connection = DriverManager.getConnection(CONNECTION_URL, props); Statement statement = connection.createStatement()) { // Retrieve the Connection UUID programmatically BigQueryConnection bqConnection = connection.unwrap(BigQueryConnection.class); connectionUuid = bqConnection.getConnectionId(); - assertNotNull("Connection UUID should be generated", connectionUuid); + assertNotNull(connectionUuid, "Connection UUID should be generated"); // Execute a query designed to fail due to non-existent table boolean caughtException = false; @@ -180,7 +187,7 @@ public void testExecute_withErrorCorrelation() throws Exception { } catch (SQLException e) { caughtException = true; } - assertTrue("Expected SQLException to be thrown", caughtException); + assertTrue(caughtException, "Expected SQLException to be thrown"); } // Step 2: Retrieve logs from Cloud Logging and assert error logs @@ -188,23 +195,23 @@ public void testExecute_withErrorCorrelation() throws Exception { String hexSpanId = null; try (Logging logging = - LoggingOptions.newBuilder().setProjectId(projectId).build().getService()) { + LoggingOptions.newBuilder().setProjectId(PROJECT_ID).build().getService()) { String filter = "logName:\"projects/" - + projectId + + PROJECT_ID + "/logs/com.google.cloud.bigquery\" AND labels.\"jdbc.connection_id\"=\"" + connectionUuid + "\""; List entries = fetchLogsWithRetry(logging, filter); - assertFalse("Telemetry logs should be exported to GCP", entries.isEmpty()); + assertFalse(entries.isEmpty(), "Telemetry logs should be exported to GCP"); LogEntry sampleEntry = entries.get(0); traceId = sampleEntry.getTrace(); hexSpanId = sampleEntry.getSpanId(); - assertNotNull("Log entry must contain TraceId", traceId); - assertNotNull("Log entry must contain SpanId", hexSpanId); + assertNotNull(traceId, "Log entry must contain TraceId"); + assertNotNull(hexSpanId, "Log entry must contain SpanId"); } // Step 3: Query Cloud Trace using TraceId and assert span status is ERROR @@ -214,8 +221,8 @@ public void testExecute_withErrorCorrelation() throws Exception { } try (TraceServiceClient traceClient = TraceServiceClient.create()) { - Trace trace = fetchTraceWithRetry(traceClient, projectId, hexTraceId); - assertNotNull("Trace must be found in Cloud Trace API: " + hexTraceId, trace); + Trace trace = fetchTraceWithRetry(traceClient, PROJECT_ID, hexTraceId); + assertNotNull(trace, "Trace must be found in Cloud Trace API: " + hexTraceId); boolean foundParentExecuteQuery = false; @@ -227,8 +234,8 @@ public void testExecute_withErrorCorrelation() throws Exception { } assertTrue( - "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'", - foundParentExecuteQuery); + foundParentExecuteQuery, + "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'"); } } From 81036512131e16db398060ac489a9708422f8a09 Mon Sep 17 00:00:00 2001 From: cloud-java-bot Date: Wed, 20 May 2026 15:20:15 +0000 Subject: [PATCH 03/12] chore: generate libraries at Wed May 20 15:18:37 UTC 2026 --- gapic-libraries-bom/pom.xml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/gapic-libraries-bom/pom.xml b/gapic-libraries-bom/pom.xml index 329f86d25e5a..fadd93e25cf8 100644 --- a/gapic-libraries-bom/pom.xml +++ b/gapic-libraries-bom/pom.xml @@ -301,13 +301,6 @@ pom import - - com.google.cloud - google-cloud-bigtable-deps-bom - 2.78.1-SNAPSHOT - pom - import - com.google.cloud google-cloud-billing-bom From dba7f5a75c6f44cb968c1e2aa25164267e6970d2 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 20 May 2026 15:26:07 +0000 Subject: [PATCH 04/12] chore: minor refactoring --- java-bigquery/google-cloud-bigquery-jdbc/pom.xml | 2 +- .../google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java | 2 +- .../com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/java-bigquery/google-cloud-bigquery-jdbc/pom.xml b/java-bigquery/google-cloud-bigquery-jdbc/pom.xml index 21b74c0fd492..78cddc110046 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/pom.xml +++ b/java-bigquery/google-cloud-bigquery-jdbc/pom.xml @@ -380,7 +380,7 @@ com.google.cloud google-cloud-trace - 2.92.0 + 2.92.0 test 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 57272e046a6b..e55b38a561db 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 @@ -69,7 +69,7 @@ public class BigQueryJdbcOpenTelemetry { "otel.span.attribute.value.length.limit"; private static final String OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT = "otel.attribute.value.length.limit"; - private static final String DEFAULT_ATTRIBUTE_LENGTH_LIMIT = "61440"; + private static final String DEFAULT_ATTRIBUTE_LENGTH_LIMIT = "32768"; private static final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger("BigQueryJdbcOpenTelemetry"); diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java index 50096da95192..16dca4ca69ed 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java @@ -252,6 +252,9 @@ private T pollWithRetry(java.util.concurrent.Callable task) throws Interr if (result != null) { return result; } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Test execution interrupted", e); } catch (Exception e) { // Ignore exceptions during remote lookup and retry } From 172dd9dbbc25ae5933eb14a61da660013d94078f Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Wed, 20 May 2026 15:38:01 +0000 Subject: [PATCH 05/12] chore: fix comment --- .../google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e55b38a561db..6fe251f0e8fa 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 @@ -295,7 +295,7 @@ public static OpenTelemetry getOpenTelemetry( props.put(GOOGLE_CLOUD_PROJECT, gcpTelemetryProjectId); } - // Set safe, generous default limits on attribute value lengths (60KB) to protect + // Set safe, generous default limits on attribute value lengths (32KB) to protect // customers from GCP Cloud Trace 64KB span ingestion failures when logging massive // exception stack traces or database schema metadata. // Respect any existing user configuration overrides. From 3e8ca63050bb61ed93268f00d64ab219c0489041 Mon Sep 17 00:00:00 2001 From: Kirill Logachev Date: Fri, 22 May 2026 05:12:18 +0000 Subject: [PATCH 06/12] fix(otel): fix syntax error in BigQueryConnectionTest after merge --- .../com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java index 14c3974cd780..790874e6fb43 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java @@ -488,6 +488,8 @@ public void testConnect_withCustomOpenTelemetry_usesCustomInstance() throws Exce List spans = otelTesting.getSpans(); assertEquals(1, spans.size()); assertEquals("custom-otel-span", spans.get(0).getName()); + } + } @Test public void testConnectionPropertiesLoggingAndMasking() throws IOException, SQLException { From 1ffc421886225f3eb5b822f868d858eba6201645 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 22 May 2026 14:30:48 +0000 Subject: [PATCH 07/12] chore: address pr feedback and refactor --- .../bigquery/jdbc/BigQueryConnection.java | 2 + .../bigquery/jdbc/BigQueryStatement.java | 6 +- .../bigquery/jdbc/it/ITOpenTelemetryTest.java | 204 ++++++++---------- 3 files changed, 99 insertions(+), 113 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java index 66f3b4f8409d..d37922e4ddb3 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java @@ -42,6 +42,7 @@ import com.google.cloud.bigquery.storage.v1.BigQueryWriteSettings; import com.google.cloud.http.HttpTransportOptions; import com.google.cloud.logging.Logging; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSortedSet; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.baggage.Baggage; @@ -438,6 +439,7 @@ String getConnectionUrl() { return connectionUrl; } + @VisibleForTesting public String getConnectionId() { return this.connectionId; } diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java index 79fa743783ef..c5f0f8a734c4 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java @@ -912,8 +912,7 @@ private void processArrowStream( enqueueError(arrowBatchWrapperBlockingQueue, e); Thread.currentThread().interrupt(); } catch (Exception e) { - if (e.getCause() instanceof InterruptedException - || Thread.currentThread().isInterrupted()) { + if (e.getCause() instanceof InterruptedException || Thread.currentThread().isInterrupted()) { LOG.log( Level.WARNING, "\n" + Thread.currentThread().getName() + " Interrupted @ arrowStreamProcessor", @@ -1684,8 +1683,7 @@ private void parseAndPopulateRpcData( } } catch (Exception ex) { - if (ex.getCause() instanceof InterruptedException - || Thread.currentThread().isInterrupted()) { + if (ex.getCause() instanceof InterruptedException || Thread.currentThread().isInterrupted()) { LOG.log( Level.WARNING, "\n" + Thread.currentThread().getName() + " Interrupted @ populateBufferAsync", diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java index 16dca4ca69ed..665100aa67cc 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java @@ -19,12 +19,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.google.api.gax.paging.Page; import com.google.cloud.ServiceOptions; import com.google.cloud.bigquery.jdbc.BigQueryConnection; +import com.google.cloud.bigquery.jdbc.DataSource; import com.google.cloud.logging.LogEntry; import com.google.cloud.logging.Logging; import com.google.cloud.logging.LoggingOptions; @@ -32,13 +34,11 @@ import com.google.devtools.cloudtrace.v1.Trace; import com.google.devtools.cloudtrace.v1.TraceSpan; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; -import java.util.Properties; import org.junit.jupiter.api.Test; public class ITOpenTelemetryTest { @@ -49,24 +49,34 @@ public class ITOpenTelemetryTest { "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;ProjectId=%s;OAuthType=3;Timeout=3600;", PROJECT_ID); + private static class TelemetryContext { + final String traceId; + final String spanId; + + TelemetryContext(String traceId, String spanId) { + this.traceId = traceId; + this.spanId = spanId; + } + } + @Test public void testExecute_withOpenTelemetryGcpExporter() throws Exception { assumeTrue( PROJECT_ID != null && !PROJECT_ID.trim().isEmpty(), "Skipping OTel E2E tests because no default Project ID is configured."); - // Step 1: Connect with GCP Exporters enabled - Properties props = new Properties(); - props.setProperty("enableGcpTraceExporter", "true"); - props.setProperty("enableGcpLogExporter", "true"); - props.setProperty("LogLevel", "3"); // Triggers FINE log generation - props.setProperty("gcpTelemetryProjectId", PROJECT_ID); - props.setProperty("EnableHighThroughputAPI", "0"); - props.setProperty("MaxResults", "50"); // Forces small page size (50) to trigger pagination + // Step 1: Connect with GCP Exporters enabled via DataSource + DataSource ds = DataSource.fromUrl(CONNECTION_URL); + ds.setEnableGcpTraceExporter(true); + ds.setEnableGcpLogExporter(true); + ds.setLogLevel("5"); // Triggers FINE log generation + ds.setGcpTelemetryProjectId(PROJECT_ID); + ds.setEnableHighThroughputAPI(false); + ds.setMaxResults(50L); // Forces small page size (50) to trigger pagination String connectionUuid = null; - try (Connection connection = DriverManager.getConnection(CONNECTION_URL, props); + try (Connection connection = ds.getConnection(); Statement statement = connection.createStatement()) { // Retrieve the Connection UUID programmatically @@ -74,87 +84,54 @@ public void testExecute_withOpenTelemetryGcpExporter() throws Exception { connectionUuid = bqConnection.getConnectionId(); assertNotNull(connectionUuid, "Connection UUID should be generated"); - // Execute an in-memory array query (scans 0 bytes, extremely fast) and force pagination - String paginationQuery = "SELECT * FROM UNNEST(GENERATE_ARRAY(1, 1000)) AS id;"; + // Execute an in-memory array query (scans 0 bytes, extremely fast) and force pagination (3 + // pages) + String paginationQuery = "SELECT * FROM UNNEST(GENERATE_ARRAY(1, 150)) AS id;"; try (ResultSet paginatedRs = statement.executeQuery(paginationQuery)) { int rowCount = 0; - while (paginatedRs.next() && rowCount < 1000) { + while (paginatedRs.next() && rowCount < 150) { rowCount++; } } } - // Step 2: Retrieve logs from Cloud Logging and extract TraceId - String traceId = null; - String hexSpanId = null; + // Step 2: Retrieve and assert logs + TelemetryContext telCtx = verifyAndFetchLogs(connectionUuid); - try (Logging logging = - LoggingOptions.newBuilder().setProjectId(PROJECT_ID).build().getService()) { - String filter = - "logName:\"projects/" - + PROJECT_ID - + "/logs/com.google.cloud.bigquery\" AND labels.\"jdbc.connection_id\"=\"" - + connectionUuid - + "\""; + // Step 3: Query Cloud Trace and assert parent-child hierarchy + Trace trace = verifyAndFetchTrace(telCtx.traceId); - List entries = fetchLogsWithRetry(logging, filter); - assertFalse(entries.isEmpty(), "Telemetry logs should be exported to GCP"); + boolean foundParentExecuteQuery = false; + boolean foundChildSdkSpans = false; + boolean foundPaginationSpans = false; + long parentSpanId = 0; - LogEntry sampleEntry = entries.get(0); - traceId = sampleEntry.getTrace(); - hexSpanId = sampleEntry.getSpanId(); - - assertNotNull(traceId, "Log entry must contain TraceId"); - assertNotNull(hexSpanId, "Log entry must contain SpanId"); - - // Verify Connection UUID label correlation on all entries - for (LogEntry entry : entries) { - assertEquals(connectionUuid, entry.getLabels().get("jdbc.connection_id")); + for (TraceSpan span : trace.getSpansList()) { + String spanName = span.getName(); + if (spanName.equals("BigQueryStatement.executeQuery")) { + foundParentExecuteQuery = true; + parentSpanId = span.getSpanId(); } } - // Step 3: Query Cloud Trace using TraceId and assert parent-child hierarchy - String hexTraceId = traceId; - if (traceId.contains("/traces/")) { - hexTraceId = traceId.substring(traceId.lastIndexOf("/traces/") + 8); - } - - try (TraceServiceClient traceClient = TraceServiceClient.create()) { - Trace trace = fetchTraceWithRetry(traceClient, PROJECT_ID, hexTraceId); - assertNotNull(trace, "Trace must be found in Cloud Trace API: " + hexTraceId); - - boolean foundParentExecuteQuery = false; - boolean foundChildSdkSpans = false; - boolean foundPaginationSpans = false; - long parentSpanId = 0; + assertTrue( + foundParentExecuteQuery, + "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'"); - for (TraceSpan span : trace.getSpansList()) { - String spanName = span.getName(); - if (spanName.equals("BigQueryStatement.executeQuery")) { - foundParentExecuteQuery = true; - parentSpanId = span.getSpanId(); - } + // Verify that we captured child spans or linked pagination spans + for (TraceSpan span : trace.getSpansList()) { + if (span.getParentSpanId() == parentSpanId && parentSpanId != 0) { + foundChildSdkSpans = true; } - - assertTrue( - foundParentExecuteQuery, - "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'"); - - // Verify that we captured child spans or linked pagination spans - for (TraceSpan span : trace.getSpansList()) { - if (span.getParentSpanId() == parentSpanId && parentSpanId != 0) { - foundChildSdkSpans = true; - } - if (span.getName().equals("BigQueryStatement.pagination")) { - foundPaginationSpans = true; - } + if (span.getName().equals("BigQueryStatement.pagination")) { + foundPaginationSpans = true; } - - assertTrue(foundPaginationSpans, "OTel pagination must generate pagination spans"); - assertTrue( - foundChildSdkSpans, - "OTel context must propagate parent to downstream pagination child spans"); } + + assertTrue(foundPaginationSpans, "OTel pagination must generate pagination spans"); + assertTrue( + foundChildSdkSpans, + "OTel context must propagate parent to downstream pagination child spans"); } @Test @@ -163,16 +140,16 @@ public void testExecute_withErrorCorrelation() throws Exception { PROJECT_ID != null && !PROJECT_ID.trim().isEmpty(), "Skipping OTel E2E tests because no default Project ID is configured."); - // Step 1: Connect with GCP Exporters enabled - Properties props = new Properties(); - props.setProperty("enableGcpTraceExporter", "true"); - props.setProperty("enableGcpLogExporter", "true"); - props.setProperty("LogLevel", "3"); // Triggers FINE log generation - props.setProperty("gcpTelemetryProjectId", PROJECT_ID); + // Step 1: Connect with GCP Exporters enabled via DataSource + DataSource ds = DataSource.fromUrl(CONNECTION_URL); + ds.setEnableGcpTraceExporter(true); + ds.setEnableGcpLogExporter(true); + ds.setLogLevel("5"); // Triggers FINE log generation + ds.setGcpTelemetryProjectId(PROJECT_ID); String connectionUuid = null; - try (Connection connection = DriverManager.getConnection(CONNECTION_URL, props); + try (Connection connection = ds.getConnection(); Statement statement = connection.createStatement()) { // Retrieve the Connection UUID programmatically @@ -180,20 +157,31 @@ public void testExecute_withErrorCorrelation() throws Exception { connectionUuid = bqConnection.getConnectionId(); assertNotNull(connectionUuid, "Connection UUID should be generated"); - // Execute a query designed to fail due to non-existent table - boolean caughtException = false; - try { - statement.executeQuery("SELECT * FROM invalid_dataset.invalid_table;"); - } catch (SQLException e) { - caughtException = true; + // Execute a query designed to fail instantly due to syntax error (compiler-level failure) + assertThrows(SQLException.class, () -> statement.executeQuery("SELECT * FROM;")); + } + + // Step 2: Retrieve and assert logs + TelemetryContext telCtx = verifyAndFetchLogs(connectionUuid); + + // Step 3: Query Cloud Trace and assert span status is ERROR + Trace trace = verifyAndFetchTrace(telCtx.traceId); + + boolean foundParentExecuteQuery = false; + + for (TraceSpan span : trace.getSpansList()) { + String spanName = span.getName(); + if (spanName.equals("BigQueryStatement.executeQuery")) { + foundParentExecuteQuery = true; } - assertTrue(caughtException, "Expected SQLException to be thrown"); } - // Step 2: Retrieve logs from Cloud Logging and assert error logs - String traceId = null; - String hexSpanId = null; + assertTrue( + foundParentExecuteQuery, + "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'"); + } + private TelemetryContext verifyAndFetchLogs(String connectionUuid) throws Exception { try (Logging logging = LoggingOptions.newBuilder().setProjectId(PROJECT_ID).build().getService()) { String filter = @@ -207,14 +195,22 @@ public void testExecute_withErrorCorrelation() throws Exception { assertFalse(entries.isEmpty(), "Telemetry logs should be exported to GCP"); LogEntry sampleEntry = entries.get(0); - traceId = sampleEntry.getTrace(); - hexSpanId = sampleEntry.getSpanId(); + String traceId = sampleEntry.getTrace(); + String hexSpanId = sampleEntry.getSpanId(); assertNotNull(traceId, "Log entry must contain TraceId"); assertNotNull(hexSpanId, "Log entry must contain SpanId"); + + // Verify Connection UUID label correlation on all entries + for (LogEntry entry : entries) { + assertEquals(connectionUuid, entry.getLabels().get("jdbc.connection_id")); + } + + return new TelemetryContext(traceId, hexSpanId); } + } - // Step 3: Query Cloud Trace using TraceId and assert span status is ERROR + private Trace verifyAndFetchTrace(String traceId) throws Exception { String hexTraceId = traceId; if (traceId.contains("/traces/")) { hexTraceId = traceId.substring(traceId.lastIndexOf("/traces/") + 8); @@ -223,19 +219,7 @@ public void testExecute_withErrorCorrelation() throws Exception { try (TraceServiceClient traceClient = TraceServiceClient.create()) { Trace trace = fetchTraceWithRetry(traceClient, PROJECT_ID, hexTraceId); assertNotNull(trace, "Trace must be found in Cloud Trace API: " + hexTraceId); - - boolean foundParentExecuteQuery = false; - - for (TraceSpan span : trace.getSpansList()) { - String spanName = span.getName(); - if (spanName.equals("BigQueryStatement.executeQuery")) { - foundParentExecuteQuery = true; - } - } - - assertTrue( - foundParentExecuteQuery, - "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'"); + return trace; } } @@ -246,7 +230,6 @@ private T pollWithRetry(java.util.concurrent.Callable task) throws Interr while (attempts < maxAttempts) { attempts++; - Thread.sleep(delayMs); try { T result = task.call(); if (result != null) { @@ -258,6 +241,9 @@ private T pollWithRetry(java.util.concurrent.Callable task) throws Interr } catch (Exception e) { // Ignore exceptions during remote lookup and retry } + if (attempts < maxAttempts) { + Thread.sleep(delayMs); + } } return null; } From 69b119b1ac1a2d7a18f5f93055d96f318539cf4c Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 22 May 2026 15:06:12 +0000 Subject: [PATCH 08/12] chore: add to nightly tests --- .../cloud/bigquery/jdbc/it/suites/ITNightlyTests.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/suites/ITNightlyTests.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/suites/ITNightlyTests.java index 2700c18adbbd..02fc96c17bad 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/suites/ITNightlyTests.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/suites/ITNightlyTests.java @@ -19,9 +19,15 @@ import com.google.cloud.bigquery.jdbc.it.ITAuthTests; import com.google.cloud.bigquery.jdbc.it.ITBigQueryJDBCTest; import com.google.cloud.bigquery.jdbc.it.ITNightlyBigQueryTest; +import com.google.cloud.bigquery.jdbc.it.ITOpenTelemetryTest; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; @Suite -@SelectClasses({ITAuthTests.class, ITBigQueryJDBCTest.class, ITNightlyBigQueryTest.class}) +@SelectClasses({ + ITAuthTests.class, + ITBigQueryJDBCTest.class, + ITNightlyBigQueryTest.class, + ITOpenTelemetryTest.class +}) public class ITNightlyTests {} From 734980525b17abf3c867c0b9588b8b7829d865b8 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 22 May 2026 17:13:41 +0000 Subject: [PATCH 09/12] chore: remove assumeTrue --- .../google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java index 665100aa67cc..889f98babfe5 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java @@ -21,7 +21,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.google.api.gax.paging.Page; import com.google.cloud.ServiceOptions; @@ -61,9 +60,6 @@ private static class TelemetryContext { @Test public void testExecute_withOpenTelemetryGcpExporter() throws Exception { - assumeTrue( - PROJECT_ID != null && !PROJECT_ID.trim().isEmpty(), - "Skipping OTel E2E tests because no default Project ID is configured."); // Step 1: Connect with GCP Exporters enabled via DataSource DataSource ds = DataSource.fromUrl(CONNECTION_URL); @@ -136,9 +132,6 @@ public void testExecute_withOpenTelemetryGcpExporter() throws Exception { @Test public void testExecute_withErrorCorrelation() throws Exception { - assumeTrue( - PROJECT_ID != null && !PROJECT_ID.trim().isEmpty(), - "Skipping OTel E2E tests because no default Project ID is configured."); // Step 1: Connect with GCP Exporters enabled via DataSource DataSource ds = DataSource.fromUrl(CONNECTION_URL); From aef912c36817e79987b3fd19c4a25a2ed73677aa Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 22 May 2026 17:20:12 +0000 Subject: [PATCH 10/12] chore: remove unused objects --- .../bigquery/jdbc/it/ITOpenTelemetryTest.java | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java index 889f98babfe5..dff06f976b79 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java @@ -48,16 +48,6 @@ public class ITOpenTelemetryTest { "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;ProjectId=%s;OAuthType=3;Timeout=3600;", PROJECT_ID); - private static class TelemetryContext { - final String traceId; - final String spanId; - - TelemetryContext(String traceId, String spanId) { - this.traceId = traceId; - this.spanId = spanId; - } - } - @Test public void testExecute_withOpenTelemetryGcpExporter() throws Exception { @@ -91,11 +81,11 @@ public void testExecute_withOpenTelemetryGcpExporter() throws Exception { } } - // Step 2: Retrieve and assert logs - TelemetryContext telCtx = verifyAndFetchLogs(connectionUuid); + // Step 2: Retrieve and assert logs, harvesting the TraceId + String traceId = verifyAndFetchLogs(connectionUuid); // Step 3: Query Cloud Trace and assert parent-child hierarchy - Trace trace = verifyAndFetchTrace(telCtx.traceId); + Trace trace = verifyAndFetchTrace(traceId); boolean foundParentExecuteQuery = false; boolean foundChildSdkSpans = false; @@ -154,11 +144,11 @@ public void testExecute_withErrorCorrelation() throws Exception { assertThrows(SQLException.class, () -> statement.executeQuery("SELECT * FROM;")); } - // Step 2: Retrieve and assert logs - TelemetryContext telCtx = verifyAndFetchLogs(connectionUuid); + // Step 2: Retrieve and assert logs, harvesting the TraceId + String traceId = verifyAndFetchLogs(connectionUuid); // Step 3: Query Cloud Trace and assert span status is ERROR - Trace trace = verifyAndFetchTrace(telCtx.traceId); + Trace trace = verifyAndFetchTrace(traceId); boolean foundParentExecuteQuery = false; @@ -174,7 +164,7 @@ public void testExecute_withErrorCorrelation() throws Exception { "Traces must contain JDBC parent span 'BigQueryStatement.executeQuery'"); } - private TelemetryContext verifyAndFetchLogs(String connectionUuid) throws Exception { + private String verifyAndFetchLogs(String connectionUuid) throws Exception { try (Logging logging = LoggingOptions.newBuilder().setProjectId(PROJECT_ID).build().getService()) { String filter = @@ -199,7 +189,7 @@ private TelemetryContext verifyAndFetchLogs(String connectionUuid) throws Except assertEquals(connectionUuid, entry.getLabels().get("jdbc.connection_id")); } - return new TelemetryContext(traceId, hexSpanId); + return traceId; } } From 6229229673fcfa7da1057307b4e09eeddb3e53d2 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 28 May 2026 16:19:11 +0200 Subject: [PATCH 11/12] feat(bigquery-jdbc): add globalOtel support (#13282) b/517124136 This PR introduces support for opting into the global OpenTelemetry instance via connection properties in the BigQuery JDBC driver. It also establishes a clear hierarchy of precedence for various OpenTelemetry configuration flags to ensure predictable behavior ## Key Changes ### 1. Global OpenTelemetry Support * Added a new connection property `useGlobalOpenTelemetry` (default: `false`). * When set to `true`, the driver will explicitly use the globally registered OpenTelemetry instance (`GlobalOpenTelemetry.get()`) for both tracing and logging. ### 2. Precedence Rules for Telemetry Flags Implemented the following hierarchy of precedence in `BigQueryConnection`: 1. **`customOpenTelemetry`** (Provided via `DataSource`): Highest precedence. Overrides everything else. 2. **`useGlobalOpenTelemetry`**: Second precedence. Overrides driver-managed GCP fallback flags. 3. **`enableGcpTraceExporter` / `enableGcpLogExporter`**: Lowest precedence. Used only if above are false/null. ### 3. Code Modifications * **`BigQueryJdbcUrlUtility.java`**: Added property definition and added it to valid properties. * **`DataSource.java`**: Added field, getter/setter, and URL parsing support for the new property. * **`BigQueryConnection.java`**: Updated constructor to read the new property and implemented the precedence logic for both tracer initialization and logging client registration. * **`BigQueryJdbcOpenTelemetry.java`**: Updated `getOpenTelemetry()` to handle the `useGlobalOpenTelemetry` flag. ### 4. Testing * **`BigQueryJdbcOpenTelemetryTest.java`**: Added unit test to verify that `useGlobalOpenTelemetry` returns the global instance. * **`BigQueryConnectionTest.java`**: Added a comprehensive parameterized test (`testOpenTelemetryPrecedenceHierarchy`) covering all 8 combinations of the 4 flags to verify the correct behavior of `OpenTelemetry` resolution and logging client creation. --- .../bigquery/jdbc/BigQueryConnection.java | 18 ++- .../jdbc/BigQueryJdbcOpenTelemetry.java | 6 + .../bigquery/jdbc/BigQueryJdbcUrlUtility.java | 8 + .../cloud/bigquery/jdbc/DataSource.java | 20 +++ .../bigquery/jdbc/BigQueryConnectionTest.java | 142 ++++++++++++++++++ .../jdbc/BigQueryJdbcOpenTelemetryTest.java | 31 ++-- 6 files changed, 209 insertions(+), 16 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java index d37922e4ddb3..8e530ccf060c 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryConnection.java @@ -216,6 +216,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { boolean enableGcpTraceExporter; boolean enableGcpLogExporter; OpenTelemetry customOpenTelemetry; + boolean useGlobalOpenTelemetry; private OpenTelemetry openTelemetry; private Context otelContext; Tracer tracer = @@ -367,6 +368,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { this.enableGcpTraceExporter = ds.getEnableGcpTraceExporter(); this.enableGcpLogExporter = ds.getEnableGcpLogExporter(); this.customOpenTelemetry = ds.getCustomOpenTelemetry(); + this.useGlobalOpenTelemetry = ds.getUseGlobalOpenTelemetry(); this.openTelemetry = getOpenTelemetryInstance(); this.bigQuery = getBigQueryConnection(); } @@ -1038,7 +1040,6 @@ void removeStatement(Statement statement) { } private OpenTelemetry getOpenTelemetryInstance() { - boolean hasCustomOtel = this.customOpenTelemetry != null; String effectiveProjectId = (this.gcpTelemetryProjectId != null) ? this.gcpTelemetryProjectId : this.catalog; @@ -1048,22 +1049,27 @@ private OpenTelemetry getOpenTelemetryInstance() { OpenTelemetry openTelemetry = BigQueryJdbcOpenTelemetry.getOpenTelemetry( + this.useGlobalOpenTelemetry, this.enableGcpTraceExporter, this.enableGcpLogExporter, this.customOpenTelemetry, effectiveCredentials, effectiveProjectId); + boolean hasExternalOtel = this.customOpenTelemetry != null || this.useGlobalOpenTelemetry; Logging localLoggingClient = null; - if (this.enableGcpLogExporter && !hasCustomOtel) { + if (this.enableGcpLogExporter && !hasExternalOtel) { localLoggingClient = BigQueryJdbcOpenTelemetry.createLoggingClient( true, null, effectiveCredentials, effectiveProjectId, this.credentials); } - if (this.enableGcpLogExporter || hasCustomOtel) { + if (this.enableGcpLogExporter || hasExternalOtel) { BigQueryJdbcOpenTelemetry.registerConnection( - this.connectionId, openTelemetry, localLoggingClient, this.enableGcpLogExporter); + this.connectionId, + openTelemetry, + localLoggingClient, + this.enableGcpLogExporter && !hasExternalOtel); } return openTelemetry; @@ -1128,7 +1134,9 @@ private BigQuery getBigQueryConnection() { if (this.httpTransportOptions != null) { bigQueryOptions.setTransportOptions(this.httpTransportOptions); } - if (this.enableGcpTraceExporter || this.customOpenTelemetry != null) { + if (this.enableGcpTraceExporter + || this.customOpenTelemetry != null + || this.useGlobalOpenTelemetry) { Tracer sdkTracer = this.openTelemetry.getTracer(BigQueryJdbcOpenTelemetry.BIGQUERY_NAMESPACE); bigQueryOptions.setOpenTelemetryTracer(sdkTracer); this.tracer = diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java index 6fe251f0e8fa..1995a4d330e4 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java @@ -21,6 +21,7 @@ import com.google.cloud.logging.Logging; import com.google.cloud.logging.LoggingOptions; import com.google.common.hash.Hashing; +import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.trace.Span; @@ -245,6 +246,7 @@ private static String getCredentialsIdentifier(String credentials) { * customOpenTelemetry if provided; fallback to an auto-configured GCP exporter if requested. */ public static OpenTelemetry getOpenTelemetry( + boolean useGlobalOpenTelemetry, boolean enableGcpTraceExporter, boolean enableGcpLogExporter, OpenTelemetry customOpenTelemetry, @@ -255,6 +257,10 @@ public static OpenTelemetry getOpenTelemetry( return customOpenTelemetry; } + if (useGlobalOpenTelemetry) { + return GlobalOpenTelemetry.get(); + } + // NOTE: Currently, tracing only fully supports Application Default Credentials (ADC). // Once b/503721589 is completed, Service Account (SA) will work as well. if (!enableGcpTraceExporter && !enableGcpLogExporter) { diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java index d14fef157af8..91de9cc79a49 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcUrlUtility.java @@ -168,6 +168,8 @@ protected boolean removeEldestEntry(Map.Entry> eldes static final boolean DEFAULT_ENABLE_GCP_TRACE_EXPORTER_VALUE = false; static final String ENABLE_GCP_LOG_EXPORTER_PROPERTY_NAME = "enableGcpLogExporter"; static final boolean DEFAULT_ENABLE_GCP_LOG_EXPORTER_VALUE = false; + static final String USE_GLOBAL_OTEL_PROPERTY_NAME = "useGlobalOpenTelemetry"; + static final boolean DEFAULT_USE_GLOBAL_OTEL_VALUE = false; private static final BigQueryJdbcCustomLogger LOG = new BigQueryJdbcCustomLogger(BigQueryJdbcUrlUtility.class.getName()); static final String FILTER_TABLES_ON_DEFAULT_DATASET_PROPERTY_NAME = @@ -638,6 +640,12 @@ protected boolean removeEldestEntry(Map.Entry> eldes BigQueryConnectionProperty.newBuilder() .setName(GCP_TELEMETRY_PROJECT_ID_PROPERTY_NAME) .setDescription("GCP Project ID for OTel exporter.") + .build(), + BigQueryConnectionProperty.newBuilder() + .setName(USE_GLOBAL_OTEL_PROPERTY_NAME) + .setDescription( + "Enables usage of the Global OpenTelemetry instance when true. Default is false.") + .setDefaultValue(String.valueOf(DEFAULT_USE_GLOBAL_OTEL_VALUE)) .build()))); private static final List NETWORK_PROPERTIES = diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java index e2dfa888d8bf..7871c3eea07b 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/DataSource.java @@ -123,6 +123,7 @@ public class DataSource implements javax.sql.DataSource { private boolean enableGcpLogExporter = BigQueryJdbcUrlUtility.DEFAULT_ENABLE_GCP_LOG_EXPORTER_VALUE; private OpenTelemetry customOpenTelemetry; + private boolean useGlobalOpenTelemetry = BigQueryJdbcUrlUtility.DEFAULT_USE_GLOBAL_OTEL_VALUE; // Make sure the JDBC driver class is loaded. static { @@ -358,6 +359,12 @@ public class DataSource implements javax.sql.DataSource { ds.setEnableGcpLogExporter( BigQueryJdbcUrlUtility.convertIntToBoolean( val, BigQueryJdbcUrlUtility.ENABLE_GCP_LOG_EXPORTER_PROPERTY_NAME))) + .put( + BigQueryJdbcUrlUtility.USE_GLOBAL_OTEL_PROPERTY_NAME, + (ds, val) -> + ds.setUseGlobalOpenTelemetry( + BigQueryJdbcUrlUtility.convertIntToBoolean( + val, BigQueryJdbcUrlUtility.USE_GLOBAL_OTEL_PROPERTY_NAME))) .build(); public static DataSource fromUrl(String url) { @@ -675,6 +682,11 @@ Properties createProperties() { BigQueryJdbcUrlUtility.ENABLE_GCP_LOG_EXPORTER_PROPERTY_NAME, String.valueOf(this.enableGcpLogExporter)); } + if (this.useGlobalOpenTelemetry) { + connectionProperties.setProperty( + BigQueryJdbcUrlUtility.USE_GLOBAL_OTEL_PROPERTY_NAME, + String.valueOf(this.useGlobalOpenTelemetry)); + } return connectionProperties; } @@ -832,6 +844,14 @@ public void setCustomOpenTelemetry(OpenTelemetry customOpenTelemetry) { this.customOpenTelemetry = customOpenTelemetry; } + public boolean getUseGlobalOpenTelemetry() { + return useGlobalOpenTelemetry; + } + + public void setUseGlobalOpenTelemetry(boolean useGlobalOpenTelemetry) { + this.useGlobalOpenTelemetry = useGlobalOpenTelemetry; + } + public void setHighThroughputMinTableSize(Integer highThroughputMinTableSize) { if (highThroughputMinTableSize != null) { validateNonNegative( diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java index 790874e6fb43..039fe2e22a66 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryConnectionTest.java @@ -17,15 +17,27 @@ package com.google.cloud.bigquery.jdbc; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider; import com.google.api.gax.rpc.HeaderProvider; import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.bigquery.BigQuery; import com.google.cloud.bigquery.QueryJobConfiguration.JobCreationMode; import com.google.cloud.bigquery.exception.BigQueryJdbcException; import com.google.cloud.bigquery.storage.v1.BigQueryReadClient; import com.google.cloud.bigquery.storage.v1.BigQueryWriteClient; +import com.google.cloud.logging.Logging; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; @@ -44,6 +56,7 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.MockedStatic; public class BigQueryConnectionTest extends BigQueryJdbcLoggingBaseTest { @@ -522,4 +535,133 @@ public void testConnectionPropertiesLoggingAndMasking() throws IOException, SQLE rootLogger.setLevel(originalLevel); } } + + @ParameterizedTest( + name = + "Case {index}: custom={0}, global={1}, trace={2}, log={3} -> expectTrace={4}, expectLog={5}") + @CsvSource({ + // hasCustom, useGlobal, enableTrace, enableLog, expectTrace, expectLog + "true, true, true, true, CUSTOM, CUSTOM", + "true, false, true, true, CUSTOM, CUSTOM", + "false, true, true, true, GLOBAL, GLOBAL", + "false, true, false, false, GLOBAL, GLOBAL", + "false, false, true, false, DRIVER_MANAGED, NONE", + "false, false, false, true, NONE, DRIVER_MANAGED", + "false, false, true, true, DRIVER_MANAGED, DRIVER_MANAGED", + "false, false, false, false, NONE, NONE" + }) + public void testOpenTelemetryPrecedenceHierarchy( + boolean hasCustom, + boolean useGlobal, + boolean enableTrace, + boolean enableLog, + String expectTrace, + String expectLog) + throws Exception { + + DataSource ds = DataSource.fromUrl(BASE_URL); + ds.setUseGlobalOpenTelemetry(useGlobal); + ds.setEnableGcpTraceExporter(enableTrace); + ds.setEnableGcpLogExporter(enableLog); + + OpenTelemetry mockCustomOtel = mock(OpenTelemetry.class); + OpenTelemetry mockGlobalOtel = mock(OpenTelemetry.class); + OpenTelemetry mockDriverManagedOtel = mock(OpenTelemetry.class); + Logging mockLogging = mock(Logging.class); + when(mockCustomOtel.getTracer(anyString())).thenReturn(mock(Tracer.class)); + when(mockGlobalOtel.getTracer(anyString())).thenReturn(mock(Tracer.class)); + when(mockDriverManagedOtel.getTracer(anyString())).thenReturn(mock(Tracer.class)); + + if (hasCustom) { + ds.setCustomOpenTelemetry(mockCustomOtel); + } + + try (MockedStatic mockedOtel = + mockStatic(BigQueryJdbcOpenTelemetry.class); + MockedStatic mockedAuth = + mockStatic(BigQueryJdbcOAuthUtility.class); + MockedStatic mockedCreds = mockStatic(GoogleCredentials.class)) { + + mockedCreds + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mock(GoogleCredentials.class)); + + // Mock parseOAuthProperties to always return ADC type to bypass validation + mockedAuth + .when(() -> BigQueryJdbcOAuthUtility.parseOAuthProperties(any(), anyString())) + .thenAnswer( + invocation -> { + java.util.Map props = new java.util.HashMap<>(); + props.put( + BigQueryJdbcUrlUtility.OAUTH_TYPE_PROPERTY_NAME, + "APPLICATION_DEFAULT_CREDENTIALS"); + return props; + }); + + mockedAuth + .when(() -> BigQueryJdbcOAuthUtility.getCredentials(any(), any(), any(), any())) + .thenReturn(mock(GoogleCredentials.class)); + + mockedOtel + .when( + () -> + BigQueryJdbcOpenTelemetry.createLoggingClient( + anyBoolean(), any(), any(), any(), any())) + .thenReturn(mockLogging); + + // Stub getOpenTelemetry to return the expected mock based on inputs + mockedOtel + .when( + () -> + BigQueryJdbcOpenTelemetry.getOpenTelemetry( + eq(useGlobal), + eq(enableTrace), + eq(enableLog), + hasCustom ? eq(mockCustomOtel) : isNull(), + any(), + any())) + .thenAnswer( + invocation -> { + if (hasCustom) return mockCustomOtel; + if (useGlobal) return mockGlobalOtel; + if (enableTrace || enableLog) return mockDriverManagedOtel; + return OpenTelemetry.noop(); + }); + + try (BigQueryConnection connection = new BigQueryConnection(BASE_URL, ds)) { + + boolean shouldBeRegistered = enableLog || hasCustom || useGlobal; + + if (!shouldBeRegistered) { + mockedOtel.verify( + () -> + BigQueryJdbcOpenTelemetry.registerConnection( + anyString(), any(), any(), anyBoolean()), + never()); + } else { + final OpenTelemetry expectedOtelInstance; + if ("CUSTOM".equals(expectTrace) || "CUSTOM".equals(expectLog)) { + expectedOtelInstance = mockCustomOtel; + } else if ("GLOBAL".equals(expectTrace) || "GLOBAL".equals(expectLog)) { + expectedOtelInstance = mockGlobalOtel; + } else if ("DRIVER_MANAGED".equals(expectTrace) || "DRIVER_MANAGED".equals(expectLog)) { + expectedOtelInstance = mockDriverManagedOtel; + } else { + expectedOtelInstance = OpenTelemetry.noop(); + } + + boolean expectUseDirectGcp = "DRIVER_MANAGED".equals(expectLog); + Logging expectedLogClient = expectUseDirectGcp ? mockLogging : null; + + mockedOtel.verify( + () -> + BigQueryJdbcOpenTelemetry.registerConnection( + anyString(), + eq(expectedOtelInstance), + eq(expectedLogClient), + eq(expectUseDirectGcp))); + } + } + } + } } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetryTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetryTest.java index 9a6e7091d6c6..6f07b0e220d5 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetryTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetryTest.java @@ -20,6 +20,7 @@ import static org.mockito.Mockito.mock; import com.google.auth.oauth2.GoogleCredentials; +import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Tracer; import org.junit.jupiter.api.AfterEach; @@ -50,7 +51,7 @@ public void testGetOpenTelemetry_withCustomSdk_returnsCustom() { OpenTelemetry mockCustomOtel = mock(OpenTelemetry.class); OpenTelemetry result = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, false, mockCustomOtel, null, null); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, false, false, mockCustomOtel, null, null); assertThat(result).isSameInstanceAs(mockCustomOtel); } @@ -61,7 +62,7 @@ public void testGetOpenTelemetry_withCustomSdkAndFlags_returnsCustom() { // Custom SDK always takes precedence over individual flags OpenTelemetry result = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, true, mockCustomOtel, null, null); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, true, mockCustomOtel, null, null); assertThat(result).isSameInstanceAs(mockCustomOtel); } @@ -69,7 +70,7 @@ public void testGetOpenTelemetry_withCustomSdkAndFlags_returnsCustom() { @Test public void testGetOpenTelemetry_noFlags_returnsNoop() { OpenTelemetry result = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, false, null, null, null); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, false, false, null, null, null); assertThat(result).isSameInstanceAs(OpenTelemetry.noop()); } @@ -84,9 +85,9 @@ public void testGetTracer_respectsScopeName() { @Test public void testGetOpenTelemetry_cachesSdkInstances() { OpenTelemetry result1 = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, false, null, null, "project1"); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, false, null, null, "project1"); OpenTelemetry result2 = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, false, null, null, "project1"); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, false, null, null, "project1"); assertThat(result1).isSameInstanceAs(result2); } @@ -94,9 +95,9 @@ public void testGetOpenTelemetry_cachesSdkInstances() { @Test public void testGetOpenTelemetry_createsNewInstanceForDifferentKey() { OpenTelemetry result1 = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, false, null, null, "project1"); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, false, null, null, "project1"); OpenTelemetry result2 = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, false, null, null, "project2"); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, false, null, null, "project2"); assertThat(result1).isNotSameInstanceAs(result2); } @@ -104,9 +105,9 @@ public void testGetOpenTelemetry_createsNewInstanceForDifferentKey() { @Test public void testGetOpenTelemetry_createsNewInstanceForDifferentTraceFlag() { OpenTelemetry result1 = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, true, null, null, "project1"); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, true, null, null, "project1"); OpenTelemetry result2 = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, null, null, "project1"); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, false, true, null, null, "project1"); assertThat(result1).isNotSameInstanceAs(result2); } @@ -114,10 +115,18 @@ public void testGetOpenTelemetry_createsNewInstanceForDifferentTraceFlag() { @Test public void testGetOpenTelemetry_ignoresEnableLogFlagInCacheKey() { OpenTelemetry result1 = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, true, null, null, "project1"); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, true, null, null, "project1"); OpenTelemetry result2 = - BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, false, null, null, "project1"); + BigQueryJdbcOpenTelemetry.getOpenTelemetry(false, true, false, null, null, "project1"); assertThat(result1).isSameInstanceAs(result2); } + + @Test + public void testGetOpenTelemetry_withUseGlobalOTel_returnsGlobal() { + OpenTelemetry result = + BigQueryJdbcOpenTelemetry.getOpenTelemetry(true, false, false, null, null, null); + + assertThat(result).isSameInstanceAs(GlobalOpenTelemetry.get()); + } } From 5b170e5e73db046c5e51cb04906025b37e0b0790 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 28 May 2026 14:29:37 +0000 Subject: [PATCH 12/12] chore: clean up based on pr comments --- .../bigquery/jdbc/BigQueryStatement.java | 38 +++++-------------- .../bigquery/jdbc/it/ITOpenTelemetryTest.java | 5 +-- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java index c5f0f8a734c4..9e8cbff8be1a 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java @@ -912,20 +912,11 @@ private void processArrowStream( enqueueError(arrowBatchWrapperBlockingQueue, e); Thread.currentThread().interrupt(); } catch (Exception e) { - if (e.getCause() instanceof InterruptedException || Thread.currentThread().isInterrupted()) { - LOG.log( - Level.WARNING, - "\n" + Thread.currentThread().getName() + " Interrupted @ arrowStreamProcessor", - e); - enqueueError(arrowBatchWrapperBlockingQueue, e); - Thread.currentThread().interrupt(); - } else { - LOG.log( - Level.WARNING, - "\n" + Thread.currentThread().getName() + " Error @ arrowStreamProcessor", - e); - enqueueError(arrowBatchWrapperBlockingQueue, e); - } + LOG.log( + Level.WARNING, + "\n" + Thread.currentThread().getName() + " Error @ arrowStreamProcessor", + e); + enqueueError(arrowBatchWrapperBlockingQueue, e); } finally { // logic needed for graceful shutdown enqueueEndOfStream(arrowBatchWrapperBlockingQueue); } @@ -1683,20 +1674,11 @@ private void parseAndPopulateRpcData( } } catch (Exception ex) { - if (ex.getCause() instanceof InterruptedException || Thread.currentThread().isInterrupted()) { - LOG.log( - Level.WARNING, - "\n" + Thread.currentThread().getName() + " Interrupted @ populateBufferAsync", - ex); - enqueueBufferError(bigQueryFieldValueListWrapperBlockingQueue, ex); - Thread.currentThread().interrupt(); - } else { - LOG.log( - Level.WARNING, - "\n" + Thread.currentThread().getName() + " Error @ populateBufferAsync", - ex); - enqueueBufferError(bigQueryFieldValueListWrapperBlockingQueue, ex); - } + LOG.log( + Level.WARNING, + "\n" + Thread.currentThread().getName() + " Error @ populateBufferAsync", + ex); + enqueueBufferError(bigQueryFieldValueListWrapperBlockingQueue, ex); } finally { enqueueBufferEndOfStream(bigQueryFieldValueListWrapperBlockingQueue); } diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java index dff06f976b79..8e7ffa92c2e9 100644 --- a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/it/ITOpenTelemetryTest.java @@ -74,9 +74,8 @@ public void testExecute_withOpenTelemetryGcpExporter() throws Exception { // pages) String paginationQuery = "SELECT * FROM UNNEST(GENERATE_ARRAY(1, 150)) AS id;"; try (ResultSet paginatedRs = statement.executeQuery(paginationQuery)) { - int rowCount = 0; - while (paginatedRs.next() && rowCount < 150) { - rowCount++; + while (paginatedRs.next()) { + // Drain the result set to trigger pagination fetches } } }