From 41cce1a438c90161767647d9c2a0e614d7b9e0fe Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 14 May 2026 14:00:06 +0000 Subject: [PATCH 1/7] feat(bqjdbc): Complete OpenTelemetry instrumentation and context propagation --- .../bigquery/jdbc/BigQueryConnection.java | 26 +++++++-- .../jdbc/BigQueryDatabaseMetaData.java | 48 ++++++++-------- .../jdbc/BigQueryJdbcOpenTelemetry.java | 50 +++++++++++++++++ .../jdbc/BigQueryPreparedStatement.java | 27 +++++++++ .../bigquery/jdbc/BigQueryStatement.java | 56 ++++++------------- .../jdbc/OpenTelemetryJulHandler.java | 5 -- .../jdbc/BigQueryDatabaseMetaDataTest.java | 13 +++++ .../bigquery/jdbc/BigQueryStatementTest.java | 12 ++++ 8 files changed, 164 insertions(+), 73 deletions(-) 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 979babd710b8..287fbc8a70a8 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 @@ -43,7 +43,9 @@ import com.google.cloud.http.HttpTransportOptions; import com.google.cloud.logging.Logging; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; import java.io.IOException; import java.io.InputStream; import java.sql.CallableStatement; @@ -149,6 +151,7 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { boolean enableGcpLogExporter; OpenTelemetry customOpenTelemetry; private OpenTelemetry openTelemetry; + private Context otelContext; Tracer tracer = OpenTelemetry.noop().getTracer(BigQueryJdbcOpenTelemetry.INSTRUMENTATION_SCOPE_NAME); DatabaseMetaData databaseMetaData; @@ -161,6 +164,11 @@ public class BigQueryConnection extends BigQueryNoOpsConnection { BigQueryConnection(String url, DataSource ds) throws IOException { this.connectionId = UUID.randomUUID().toString(); + Baggage baggage = + Baggage.builder() + .put(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY, this.connectionId) + .build(); + this.otelContext = Context.current().with(baggage); try (BigQueryJdbcMdc.MdcCloseable mdc = BigQueryJdbcMdc.registerInstance(this.connectionId)) { LOG.finest("++enter++"); @@ -1059,9 +1067,11 @@ private BigQuery getBigQueryConnection() { if (this.httpTransportOptions != null) { bigQueryOptions.setTransportOptions(this.httpTransportOptions); } - if (Boolean.TRUE.equals(this.enableGcpTraceExporter) || this.customOpenTelemetry != null) { - this.tracer = BigQueryJdbcOpenTelemetry.getTracer(this.openTelemetry); - bigQueryOptions.setOpenTelemetryTracer(this.tracer); + if (this.enableGcpTraceExporter || this.customOpenTelemetry != null) { + Tracer sdkTracer = this.openTelemetry.getTracer(BigQueryJdbcOpenTelemetry.BIGQUERY_NAMESPACE); + bigQueryOptions.setOpenTelemetryTracer(sdkTracer); + this.tracer = + this.openTelemetry.getTracer(BigQueryJdbcOpenTelemetry.INSTRUMENTATION_SCOPE_NAME); } BigQueryOptions options = bigQueryOptions.setHeaderProvider(this.headerProvider).build(); @@ -1112,7 +1122,7 @@ private BigQueryReadClient getBigQueryReadClientConnection() throws IOException bigQueryReadSettings.setTransportChannelProvider(activeProvider); - if (Boolean.TRUE.equals(this.enableGcpTraceExporter) || this.customOpenTelemetry != null) { + if (this.enableGcpTraceExporter || this.customOpenTelemetry != null) { bigQueryReadSettings.setOpenTelemetryTracerProvider(this.openTelemetry.getTracerProvider()); } @@ -1221,6 +1231,14 @@ public Tracer getTracer() { return this.tracer; } + public Context getOtelContext() { + return this.otelContext; + } + + public String getPartnerToken() { + return this.partnerToken; + } + public boolean isReadOnlyTokenUsed() { return this.isReadOnlyTokenUsed; } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java index c6bfd00ab828..5a1903c47df7 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaData.java @@ -45,8 +45,6 @@ import com.google.common.annotations.VisibleForTesting; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; -import io.opentelemetry.api.trace.StatusCode; -import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import java.io.BufferedReader; @@ -864,7 +862,8 @@ public ResultSet getProcedures( procedureNamePattern, procedureNameRegex, LOG); - Future> apiFuture = apiExecutor.submit(apiCallable); + Future> apiFuture = + apiExecutor.submit(Context.current().wrap(apiCallable)); apiFutures.add(apiFuture); } LOG.fine("Finished submitting " + apiFutures.size() + " findMatchingRoutines tasks."); @@ -888,9 +887,13 @@ public ResultSet getProcedures( final Routine finalRoutine = routine; Future processFuture = routineProcessorExecutor.submit( - () -> - processProcedureInfo( - finalRoutine, collectedResults, localResultSchemaFields)); + Context.current() + .wrap( + () -> + processProcedureInfo( + finalRoutine, + collectedResults, + localResultSchemaFields))); processingTaskFutures.add(processFuture); } else { LOG.finer("Skipping non-procedure routine: " + routine.getRoutineId()); @@ -1280,7 +1283,7 @@ List listMatchingProcedureIdsFromDatasets( procedureNamePattern, procedureNameRegex, logger); - listRoutineFutures.add(listRoutinesExecutor.submit(listCallable)); + listRoutineFutures.add(listRoutinesExecutor.submit(Context.current().wrap(listCallable))); } logger.fine( "Submitted " @@ -1357,7 +1360,7 @@ List fetchFullRoutineDetailsForIds( return null; } }; - getRoutineFutures.add(getRoutineDetailsExecutor.submit(getCallable)); + getRoutineFutures.add(getRoutineDetailsExecutor.submit(Context.current().wrap(getCallable))); } logger.fine("Submitted " + getRoutineFutures.size() + " getRoutine detail tasks."); @@ -1407,9 +1410,14 @@ void submitProcedureArgumentProcessingJobs( final Routine finalFullRoutine = fullRoutine; Future processFuture = processArgsExecutor.submit( - () -> - processProcedureArguments( - finalFullRoutine, columnNameRegex, collectedResults, resultSchemaFields)); + Context.current() + .wrap( + () -> + processProcedureArguments( + finalFullRoutine, + columnNameRegex, + collectedResults, + resultSchemaFields))); outArgumentProcessingFutures.add(processFuture); } else { logger.warning( @@ -4080,7 +4088,8 @@ public ResultSet getFunctions(String catalog, String schemaPattern, String funct functionNameRegex, LOG); }; - Future> apiFuture = apiExecutor.submit(apiCallable); + Future> apiFuture = + apiExecutor.submit(Context.current().wrap(apiCallable)); apiFutures.add(apiFuture); } LOG.fine( @@ -4515,7 +4524,7 @@ List listMatchingFunctionIdsFromDatasets( functionNamePattern, functionNameRegex, logger); - listRoutineFutures.add(listRoutinesExecutor.submit(listCallable)); + listRoutineFutures.add(listRoutinesExecutor.submit(Context.current().wrap(listCallable))); } logger.fine( "Submitted " @@ -5443,16 +5452,7 @@ private interface TracedMetadataOperation { private T withTracing(String spanName, TracedMetadataOperation operation) throws SQLException { - Tracer tracer = this.connection.getTracer(); - Span span = tracer.spanBuilder(spanName).startSpan(); - try (Scope scope = span.makeCurrent()) { - return operation.run(); - } catch (Exception ex) { - span.recordException(ex); - span.setStatus(StatusCode.ERROR, ex.getMessage()); - throw ex; - } finally { - span.end(); - } + return BigQueryJdbcOpenTelemetry.withTracing( + spanName, this.connection, null, () -> operation.run()); } } 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 f89c1980f925..dd3c6996a41e 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 @@ -22,14 +22,20 @@ import com.google.cloud.logging.LoggingOptions; import com.google.common.hash.Hashing; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import java.nio.charset.StandardCharsets; +import java.sql.SQLException; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Handler; import java.util.logging.Logger; @@ -39,6 +45,14 @@ 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"; + public static final String DB_SYSTEM_KEY = "db.system"; + public static final String DB_SYSTEM_VALUE = "bigquery"; + public static final String DB_CONNECTION_ID_KEY = "db.connection_id"; + public static final String DB_APPLICATION_KEY = "db.application"; + public static final String DEFAULT_APPLICATION_NAME = "Google-BigQuery-JDBC-Driver"; + public static final String DB_STATEMENT_KEY = "db.statement"; + public static final String DB_STATEMENT_COUNT_KEY = "db.statement.count"; + public static final String DB_BATCH_STATEMENTS_KEY = "db.batch.statements"; private static final String OTEL_TRACES_EXPORTER = "otel.traces.exporter"; private static final String OTEL_EXPORTER_OTLP_ENDPOINT = "otel.exporter.otlp.endpoint"; private static final String OTEL_LOGS_EXPORTER = "otel.logs.exporter"; @@ -287,4 +301,40 @@ public static OpenTelemetry getOpenTelemetry( public static Tracer getTracer(OpenTelemetry openTelemetry) { return openTelemetry.getTracer(INSTRUMENTATION_SCOPE_NAME); } + + public static T withTracing( + String spanName, BigQueryConnection connection, String sql, Callable operation) + throws SQLException { + + Context parentContext = connection.getOtelContext(); + Tracer tracer = connection.getTracer(); + Span span = tracer.spanBuilder(spanName).setParent(parentContext).startSpan(); + + span.setAttribute(DB_SYSTEM_KEY, DB_SYSTEM_VALUE); + span.setAttribute(DB_CONNECTION_ID_KEY, connection.getConnectionId()); + + String appName = connection.getPartnerToken(); + if (appName == null || appName.isEmpty()) { + appName = DEFAULT_APPLICATION_NAME; + } + span.setAttribute(DB_APPLICATION_KEY, appName); + + if (sql != null) { + span.setAttribute(DB_STATEMENT_KEY, sql); + } + + Context fullContext = parentContext.with(span); + try (Scope scope = fullContext.makeCurrent()) { + return operation.call(); + } catch (Exception ex) { + span.recordException(ex); + span.setStatus(StatusCode.ERROR, ex.getMessage()); + if (ex instanceof SQLException) { + throw (SQLException) ex; + } + throw new BigQueryJdbcRuntimeException(ex); + } finally { + span.end(); + } + } } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java index f02227e0edc3..8944e2558cc1 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryPreparedStatement.java @@ -91,6 +91,15 @@ private int getParameterCount(String query) { @Override public ResultSet executeQuery() throws SQLException { LOG.finest("++enter++"); + checkClosed(); + return BigQueryJdbcOpenTelemetry.withTracing( + "BigQueryPreparedStatement.executeQuery", + this.connection, + this.currentQuery, + () -> executeQueryImpl()); + } + + private ResultSet executeQueryImpl() throws SQLException { logQueryExecutionStart(this.currentQuery); try { QueryJobConfiguration.Builder jobConfiguration = getJobConfig(this.currentQuery); @@ -106,6 +115,15 @@ public ResultSet executeQuery() throws SQLException { @Override public long executeLargeUpdate() throws SQLException { LOG.finest("++enter++"); + checkClosed(); + return BigQueryJdbcOpenTelemetry.withTracing( + "BigQueryPreparedStatement.executeLargeUpdate", + this.connection, + this.currentQuery, + () -> executeLargeUpdateImpl()); + } + + private long executeLargeUpdateImpl() throws SQLException { logQueryExecutionStart(this.currentQuery); try { QueryJobConfiguration.Builder jobConfiguration = getJobConfig(this.currentQuery); @@ -127,6 +145,15 @@ public int executeUpdate() throws SQLException { @Override public boolean execute() throws SQLException { LOG.finest("++enter++"); + checkClosed(); + return BigQueryJdbcOpenTelemetry.withTracing( + "BigQueryPreparedStatement.execute", + this.connection, + this.currentQuery, + () -> executeImpl()); + } + + private boolean executeImpl() throws SQLException { logQueryExecutionStart(this.currentQuery); try { QueryJobConfiguration.Builder jobConfiguration = getJobConfig(this.currentQuery); diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java index 10265b5fcffe..f85640569956 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryStatement.java @@ -244,12 +244,8 @@ private BigQuerySettings generateBigQuerySettings() { public ResultSet executeQuery(String sql) throws SQLException { LOG.finest("++enter++"); checkClosed(); - return withTracing( - "BigQueryStatement.executeQuery", - (span) -> { - span.setAttribute("db.statement", sql); - return executeQueryImpl(sql); - }); + return BigQueryJdbcOpenTelemetry.withTracing( + "BigQueryStatement.executeQuery", this.connection, sql, () -> executeQueryImpl(sql)); } private ResultSet executeQueryImpl(String sql) throws SQLException { @@ -273,12 +269,11 @@ private ResultSet executeQueryImpl(String sql) throws SQLException { public long executeLargeUpdate(String sql) throws SQLException { LOG.finest("++enter++"); checkClosed(); - return withTracing( + return BigQueryJdbcOpenTelemetry.withTracing( "BigQueryStatement.executeLargeUpdate", - (span) -> { - span.setAttribute("db.statement", sql); - return executeLargeUpdateImpl(sql); - }); + this.connection, + sql, + () -> executeLargeUpdateImpl(sql)); } private long executeLargeUpdateImpl(String sql) throws SQLException { @@ -316,12 +311,8 @@ int checkUpdateCount(long updateCount) { public boolean execute(String sql) throws SQLException { LOG.finest("++enter++"); checkClosed(); - return withTracing( - "BigQueryStatement.execute", - (span) -> { - span.setAttribute("db.statement", sql); - return executeImpl(sql); - }); + return BigQueryJdbcOpenTelemetry.withTracing( + "BigQueryStatement.execute", this.connection, sql, () -> executeImpl(sql)); } private boolean executeImpl(String sql) throws SQLException { @@ -1366,12 +1357,16 @@ public void clearBatch() { @Override public int[] executeBatch() throws SQLException { LOG.finest("++enter++"); - return withTracing( + return BigQueryJdbcOpenTelemetry.withTracing( "BigQueryStatement.executeBatch", - (span) -> { - span.setAttribute("db.statement.count", this.batchQueries.size()); + this.connection, + null, + () -> { + Span span = Span.current(); span.setAttribute( - AttributeKey.stringArrayKey("db.batch.statements"), + BigQueryJdbcOpenTelemetry.DB_STATEMENT_COUNT_KEY, this.batchQueries.size()); + span.setAttribute( + AttributeKey.stringArrayKey(BigQueryJdbcOpenTelemetry.DB_BATCH_STATEMENTS_KEY), new ArrayList<>(this.batchQueries)); StringBuilder sb = new StringBuilder(); @@ -1556,25 +1551,6 @@ private void enqueueBufferEndOfStream(BlockingQueue { - T run(Span span) throws SQLException; - } - - private T withTracing(String spanName, TracedOperation operation) throws SQLException { - Tracer tracer = this.connection.getTracer(); - Span span = tracer.spanBuilder(spanName).startSpan(); - try (Scope scope = span.makeCurrent()) { - return operation.run(span); - } catch (SQLException | RuntimeException ex) { - span.recordException(ex); - span.setStatus(StatusCode.ERROR, ex.getMessage()); - throw ex; - } finally { - span.end(); - } - } - private void fetchNextPages( String firstPageToken, JobId jobId, 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 b78fc9fcfa86..c8192cf47e0c 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 @@ -56,11 +56,6 @@ public void publish(LogRecord record) { 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; } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java index c3fec1b4b5cc..172f2d74978b 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryDatabaseMetaDataTest.java @@ -30,9 +30,11 @@ import com.google.api.gax.paging.Page; import com.google.cloud.bigquery.*; import com.google.cloud.bigquery.BigQuery.RoutineListOption; +import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; import io.opentelemetry.sdk.trace.data.SpanData; @@ -81,11 +83,13 @@ public void setUp() throws SQLException { when(bigQueryConnection.getConnectionUrl()).thenReturn("jdbc:bigquery://test-project"); when(bigQueryConnection.getBigQuery()).thenReturn(bigqueryClient); when(bigQueryConnection.createStatement()).thenReturn(mockStatement); + when(bigQueryConnection.getConnectionId()).thenReturn("test-connection-id"); when(bigQueryConnection.getTracer()) .thenReturn( otelTesting .getOpenTelemetry() .getTracer(BigQueryJdbcOpenTelemetry.INSTRUMENTATION_SCOPE_NAME)); + when(bigQueryConnection.getOtelContext()).thenReturn(Context.current()); Page datasetPageMock = mock(Page.class); when(bigqueryClient.listDatasets(anyString(), any())).thenReturn(datasetPageMock); @@ -3249,6 +3253,15 @@ public void testMetadataOperation_generatesSpan( SpanData span = OpenTelemetryTestUtility.findSpanByName(otelTesting.getSpans(), expectedSpanName); OpenTelemetryTestUtility.assertSpanStatus(span, StatusCode.UNSET); + + OpenTelemetryTestUtility.assertSpanHasAttribute( + span, + AttributeKey.stringKey(BigQueryJdbcOpenTelemetry.DB_SYSTEM_KEY), + BigQueryJdbcOpenTelemetry.DB_SYSTEM_VALUE); + OpenTelemetryTestUtility.assertSpanHasAttribute( + span, + AttributeKey.stringKey(BigQueryJdbcOpenTelemetry.DB_CONNECTION_ID_KEY), + "test-connection-id"); } @FunctionalInterface diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java index 353263cd8751..648e48e45600 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java @@ -59,6 +59,7 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.testing.junit5.OpenTelemetryExtension; import io.opentelemetry.sdk.trace.data.SpanData; @@ -181,6 +182,7 @@ public void setUp() throws IOException, SQLException { .getTracer(BigQueryJdbcOpenTelemetry.INSTRUMENTATION_SCOPE_NAME)) .when(bigQueryConnection) .getTracer(); + doReturn(Context.current()).when(bigQueryConnection).getOtelContext(); rpcFactoryMock = mock(BigQueryRpcFactory.class); bigquery = mock(BigQuery.class); bigQueryConnection.bigQuery = bigquery; @@ -188,6 +190,7 @@ public void setUp() throws IOException, SQLException { jobId = JobId.newBuilder().setJob(jobIdVal).build(); doReturn(bigquery).when(bigQueryConnection).getBigQuery(); + doReturn("test-connection-id").when(bigQueryConnection).getConnectionId(); doReturn(10L).when(bigQueryConnection).getJobTimeoutInSeconds(); doReturn(10L).when(bigQueryConnection).getMaxBytesBilled(); doReturn(LABELS).when(bigQueryConnection).getLabels(); @@ -586,6 +589,15 @@ public void testExecuteOperation_generatesSpan( span, (AttributeKey) entry.getKey(), entry.getValue()); } } + + OpenTelemetryTestUtility.assertSpanHasAttribute( + span, + AttributeKey.stringKey(BigQueryJdbcOpenTelemetry.DB_SYSTEM_KEY), + BigQueryJdbcOpenTelemetry.DB_SYSTEM_VALUE); + OpenTelemetryTestUtility.assertSpanHasAttribute( + span, + AttributeKey.stringKey(BigQueryJdbcOpenTelemetry.DB_CONNECTION_ID_KEY), + "test-connection-id"); } Stream statementOperationProvider() { From 00e241d738680146dcb4769230670fe4010b56c8 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 14 May 2026 14:21:59 +0000 Subject: [PATCH 2/7] chore: address pr feedback --- .../jdbc/BigQueryJdbcOpenTelemetry.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) 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 dd3c6996a41e..0ab7ada9da43 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 @@ -22,6 +22,7 @@ import com.google.cloud.logging.LoggingOptions; import com.google.common.hash.Hashing; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; @@ -306,9 +307,8 @@ public static T withTracing( String spanName, BigQueryConnection connection, String sql, Callable operation) throws SQLException { - Context parentContext = connection.getOtelContext(); Tracer tracer = connection.getTracer(); - Span span = tracer.spanBuilder(spanName).setParent(parentContext).startSpan(); + Span span = tracer.spanBuilder(spanName).startSpan(); span.setAttribute(DB_SYSTEM_KEY, DB_SYSTEM_VALUE); span.setAttribute(DB_CONNECTION_ID_KEY, connection.getConnectionId()); @@ -323,15 +323,32 @@ public static T withTracing( span.setAttribute(DB_STATEMENT_KEY, sql); } - Context fullContext = parentContext.with(span); + String connectionId = + Baggage.fromContext(connection.getOtelContext()).getEntryValue(CONNECTION_ID_BAGGAGE_KEY); + Baggage updatedBaggage = + Baggage.fromContext(Context.current()).toBuilder() + .put(CONNECTION_ID_BAGGAGE_KEY, connectionId) + .build(); + + // Create full context with new span and updated baggage + Context fullContext = Context.current().with(span).with(updatedBaggage); + try (Scope scope = fullContext.makeCurrent()) { return operation.call(); } catch (Exception ex) { span.recordException(ex); span.setStatus(StatusCode.ERROR, ex.getMessage()); + if (ex instanceof SQLException) { throw (SQLException) ex; } + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + if (ex instanceof InterruptedException) { + Thread.currentThread().interrupt(); + throw new BigQueryJdbcRuntimeException("Operation interrupted", ex); + } throw new BigQueryJdbcRuntimeException(ex); } finally { span.end(); From e42af97376cad70462f0d2be3f03d7fd35864611 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 14 May 2026 14:30:11 +0000 Subject: [PATCH 3/7] chore: remove unnecessary connectionId extraction --- .../google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 0ab7ada9da43..c7da437dff03 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 @@ -323,11 +323,9 @@ public static T withTracing( span.setAttribute(DB_STATEMENT_KEY, sql); } - String connectionId = - Baggage.fromContext(connection.getOtelContext()).getEntryValue(CONNECTION_ID_BAGGAGE_KEY); Baggage updatedBaggage = Baggage.fromContext(Context.current()).toBuilder() - .put(CONNECTION_ID_BAGGAGE_KEY, connectionId) + .put(CONNECTION_ID_BAGGAGE_KEY, connection.getConnectionId()) .build(); // Create full context with new span and updated baggage From 0f41919d57e3a9774634349303413f521c95aa8e Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Thu, 14 May 2026 14:50:13 +0000 Subject: [PATCH 4/7] chore: add span kind to `CLIENT` --- .../google/cloud/bigquery/jdbc/BigQueryJdbcOpenTelemetry.java | 3 ++- 1 file changed, 2 insertions(+), 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 c7da437dff03..70c23f0582b4 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 @@ -24,6 +24,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; @@ -308,7 +309,7 @@ public static T withTracing( throws SQLException { Tracer tracer = connection.getTracer(); - Span span = tracer.spanBuilder(spanName).startSpan(); + Span span = tracer.spanBuilder(spanName).setSpanKind(SpanKind.CLIENT).startSpan(); span.setAttribute(DB_SYSTEM_KEY, DB_SYSTEM_VALUE); span.setAttribute(DB_CONNECTION_ID_KEY, connection.getConnectionId()); From b2d4bed0366a4c97785ef79c5fc65a4b98c90255 Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Fri, 15 May 2026 14:23:09 +0000 Subject: [PATCH 5/7] CHore: add unit tests for context propagation and exception handling of span --- .../bigquery/jdbc/BigQueryStatementTest.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java index 648e48e45600..8edfcad8b7a1 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryStatementTest.java @@ -31,6 +31,7 @@ import com.google.cloud.bigquery.BigQuery; import com.google.cloud.bigquery.BigQuery.QueryResultsOption; import com.google.cloud.bigquery.BigQuery.TableDataListOption; +import com.google.cloud.bigquery.BigQueryException; import com.google.cloud.bigquery.BigQueryOptions; import com.google.cloud.bigquery.Field; import com.google.cloud.bigquery.FieldList; @@ -47,6 +48,7 @@ import com.google.cloud.bigquery.StandardSQLTypeName; import com.google.cloud.bigquery.TableId; import com.google.cloud.bigquery.TableResult; +import com.google.cloud.bigquery.exception.BigQueryJdbcException; import com.google.cloud.bigquery.jdbc.BigQueryStatement.JobIdWrapper; import com.google.cloud.bigquery.spi.BigQueryRpcFactory; import com.google.cloud.bigquery.storage.v1.ArrowSchema; @@ -55,6 +57,7 @@ import com.google.cloud.bigquery.storage.v1.ReadSession; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; +import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; @@ -753,4 +756,70 @@ public void testUseReadAPI_ZeroPageSizeDivisionByZeroSafeguard() throws SQLExcep boolean useReadApi = statement.useReadAPI(tableResult); assertThat(useReadApi).isTrue(); // ratio = 500 / 1 = 500 > 2 -> true } + + @Test + public void testExecute_registersException() throws Exception { + // Mock bigquery to throw a backend exception + BigQueryException expectedException = new BigQueryException(500, "Backend Error"); + Mockito.doThrow(expectedException) + .when(bigquery) + .queryWithTimeout(Mockito.any(QueryJobConfiguration.class), Mockito.any(), Mockito.any()); + + BigQueryStatement spiedStatement = Mockito.spy(bigQueryStatement); + + // Execute and expect the exception to be propagated to JDBC + Assertions.assertThrows(SQLException.class, () -> spiedStatement.executeQuery("SELECT 1")); + + // Retrieve the exported span from OTel extension + List spans = otelTesting.getSpans(); + SpanData span = + OpenTelemetryTestUtility.findSpanByName(spans, "BigQueryStatement.executeQuery"); + + // Assert that the span recorded the error correctly + OpenTelemetryTestUtility.assertSpanStatus(span, StatusCode.ERROR); + OpenTelemetryTestUtility.assertSpanHasException(span, BigQueryJdbcException.class); + } + + @Test + public void testExecute_propagatesContextAndBaggage() throws Exception { + // Mock bigquery using thenAnswer to hook into the call and assert Context/Baggage + Mockito.doAnswer( + invocation -> { + // This code runs on the execution thread during the SDK call + String connectionIdBaggage = + Baggage.current() + .getEntryValue(BigQueryJdbcOpenTelemetry.CONNECTION_ID_BAGGAGE_KEY); + assertEquals("test-connection-id", connectionIdBaggage); + + Span currentSpan = Span.current(); + assertTrue(currentSpan.getSpanContext().isValid()); + + // Return a mock TableResult to allow the execution to proceed + TableResult tableResultMock = mock(TableResult.class); + doReturn(jobId).when(tableResultMock).getJobId(); + doReturn(Schema.of()).when(tableResultMock).getSchema(); + return tableResultMock; + }) + .when(bigquery) + .queryWithTimeout(Mockito.any(QueryJobConfiguration.class), Mockito.any(), Mockito.any()); + + BigQueryStatement spiedStatement = Mockito.spy(bigQueryStatement); + + // Setup connection mocks to allow the statement to execute successfully + doReturn(true).when(bigQueryConnection).getUseStatelessQueryMode(); + Job dryRunJobMock = getJobMock(null, null, StatementType.SELECT); + doReturn(dryRunJobMock).when(bigquery).create(Mockito.any(JobInfo.class)); + + BigQueryJsonResultSet resultSetMock = mock(BigQueryJsonResultSet.class); + doReturn(resultSetMock) + .when(spiedStatement) + .processJsonResultSet(Mockito.any(TableResult.class)); + + // Execute query + spiedStatement.executeQuery("SELECT 1"); + + // Verify the SDK call actually occurred + verify(bigquery) + .queryWithTimeout(Mockito.any(QueryJobConfiguration.class), Mockito.any(), Mockito.any()); + } } From 30dd483045079cf3d450f66f6b2273ed0d2518ef Mon Sep 17 00:00:00 2001 From: Keshav Dandeva Date: Tue, 19 May 2026 00:41:33 +0000 Subject: [PATCH 6/7] 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 d43f2461e4643237c82e59a42d7c7e38e8c8ead8 Mon Sep 17 00:00:00 2001 From: cloud-java-bot Date: Tue, 19 May 2026 00:52:45 +0000 Subject: [PATCH 7/7] chore: generate libraries at Tue May 19 00:51:10 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