diff --git a/java-bigquery-jdbc/.gitignore b/java-bigquery-jdbc/.gitignore index 4d2063438a20..b919e39bbe0f 100644 --- a/java-bigquery-jdbc/.gitignore +++ b/java-bigquery-jdbc/.gitignore @@ -9,6 +9,8 @@ tools/**/*.class tools/**/drivers/** tools/**/logs/** tools/**/*.jfr +tools/**/odbc/ +tools/**/native_odbc_perf # Gemini/Jetski agent custom skills .agents/ \ No newline at end of file diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryConfiguration.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryConfiguration.java index b1e54b66085b..85e10c5fe376 100644 --- a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryConfiguration.java +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryConfiguration.java @@ -113,6 +113,10 @@ static Builder newBuilder() { return new Builder(); } + static Builder builder() { + return new Builder(); + } + /** Builder for {@link TelemetryConfiguration}. */ static class Builder { private boolean enabled = DEFAULT_ENABLED; diff --git a/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryManager.java b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryManager.java new file mode 100644 index 000000000000..a688cca15f24 --- /dev/null +++ b/java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryManager.java @@ -0,0 +1,119 @@ +/* + * 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 + * + * https://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.telemetry.v1; + +import com.google.cloud.bigquery.jdbc.BigQueryJdbcCustomLogger; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Thread-safe singleton hub managing the lifecycle of {@link TelemetryBatcher} for client-side + * diagnostic and usage telemetry in the BigQuery JDBC driver. + * + *
All telemetry operations are wrapped in exception safeguards so that failures in metric + * collection or batch dispatching never impact standard JDBC functionality. + */ +final class TelemetryManager implements AutoCloseable { + private static final Logger logger = + new BigQueryJdbcCustomLogger(TelemetryManager.class.getName()); + + private static volatile TelemetryManager instance; + + private final TelemetryBatcher batcher; + + private TelemetryManager(TelemetryBatcher batcher) { + this.batcher = batcher; + } + + /** + * Initializes or replaces the shared {@link TelemetryManager} instance with default configuration + * and transport. + */ + static TelemetryManager getInstance() { + TelemetryManager localRef = instance; + if (localRef == null) { + synchronized (TelemetryManager.class) { + localRef = instance; + if (localRef == null) { + TelemetryConfiguration config = TelemetryConfiguration.builder().build(); + ClearcutTransport transport = new ClearcutTransport(config); + TelemetryBatcher batcher = new TelemetryBatcher(config, transport); + localRef = new TelemetryManager(batcher); + instance = localRef; + } + } + } + return localRef; + } + + /** Package-private lifecycle initialisation method for explicit configuration or unit testing. */ + static synchronized void init(TelemetryConfiguration config, ClearcutTransport transport) { + closeInstance(); + if (config != null && config.isEnabled() && transport != null) { + TelemetryBatcher batcher = new TelemetryBatcher(config, transport); + instance = new TelemetryManager(batcher); + } + } + + /** + * Returns the underlying {@link TelemetryBatcher} managed by this instance, or {@code null} if + * telemetry is closed or uninitialized. + */ + TelemetryBatcher getBatcher() { + return batcher; + } + + /** + * Executes a telemetry logging operation safely inside an exception-isolated block. Guaranteed to + * catch all {@link Throwable} exceptions to protect JDBC driver operations. + */ + static void runSafely(Runnable action) { + if (action == null) { + return; + } + try { + action.run(); + } catch (Throwable t) { + logger.log(Level.FINE, "Diagnostic telemetry operation encountered non-fatal error", t); + } + } + + /** Package-private helper to check if an active instance is present and initialized. */ + static boolean isInitialized() { + return instance != null; + } + + /** Flushes pending buffered metrics and shuts down the shared instance. */ + static synchronized void closeInstance() { + TelemetryManager localRef = instance; + instance = null; + if (localRef != null) { + try { + localRef.close(); + } catch (Throwable t) { + logger.log(Level.FINE, "Error closing TelemetryManager instance", t); + } + } + } + + @Override + public void close() { + if (batcher != null) { + batcher.close(); + } + } +} diff --git a/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryManagerTest.java b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryManagerTest.java new file mode 100644 index 000000000000..0c71941cd5dc --- /dev/null +++ b/java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/telemetry/v1/TelemetryManagerTest.java @@ -0,0 +1,105 @@ +/* + * 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 + * + * https://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.telemetry.v1; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class TelemetryManagerTest { + + @BeforeEach + @AfterEach + public void cleanUp() { + TelemetryManager.closeInstance(); + } + + @Test + public void testGetInstance_returnsSameSingletonInstance() { + TelemetryManager manager1 = TelemetryManager.getInstance(); + TelemetryManager manager2 = TelemetryManager.getInstance(); + + assertNotNull(manager1); + assertSame(manager1, manager2); + assertTrue(TelemetryManager.isInitialized()); + assertNotNull(manager1.getBatcher()); + } + + @Test + public void testInit_withDisabledConfig_doesNotInitializeInstance() { + TelemetryConfiguration config = TelemetryConfiguration.builder().setEnabled(false).build(); + ClearcutTransport transport = new ClearcutTransport(config); + + TelemetryManager.init(config, transport); + + assertFalse(TelemetryManager.isInitialized()); + } + + @Test + public void testInit_withEnabledConfig_initializesCustomInstance() { + TelemetryConfiguration config = TelemetryConfiguration.builder().setEnabled(true).build(); + ClearcutTransport transport = new ClearcutTransport(config); + + TelemetryManager.init(config, transport); + + assertTrue(TelemetryManager.isInitialized()); + assertNotNull(TelemetryManager.getInstance().getBatcher()); + } + + @Test + public void testCloseInstance_resetsSingletonState() { + TelemetryManager.getInstance(); + assertTrue(TelemetryManager.isInitialized()); + + TelemetryManager.closeInstance(); + assertFalse(TelemetryManager.isInitialized()); + } + + @Test + public void testRunSafely_executesRunnableSuccessfully() { + AtomicBoolean executed = new AtomicBoolean(false); + + TelemetryManager.runSafely(() -> executed.set(true)); + + assertTrue(executed.get()); + } + + @Test + public void testRunSafely_swallowsExceptionsWithoutThrowing() { + AtomicBoolean lineAfterException = new AtomicBoolean(false); + + TelemetryManager.runSafely( + () -> { + throw new RuntimeException("Simulated telemetry exception"); + }); + lineAfterException.set(true); + + assertTrue(lineAfterException.get()); + } + + @Test + public void testRunSafely_handlesNullActionGracefully() { + TelemetryManager.runSafely(null); + // Verifies no NullPointerException is thrown + } +}