diff --git a/java-bigquery/google-cloud-bigquery-jdbc/.gitignore b/java-bigquery/google-cloud-bigquery-jdbc/.gitignore index 75b2db720ed5..0af2440505c9 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/.gitignore +++ b/java-bigquery/google-cloud-bigquery-jdbc/.gitignore @@ -1,3 +1,3 @@ drivers/** target-it/** -*logs/** \ No newline at end of file +*logs*/** \ No newline at end of file diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java index 5fc2c15bbe09..9cbfacc1555e 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseArray.java @@ -71,23 +71,27 @@ public final int getBaseType() { @Override public final Object getArray(Map> map) throws SQLException { + LOG.severe(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); throw new BigQueryJdbcSqlFeatureNotSupportedException(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); } @Override public final Object getArray(long index, int count, Map> map) throws SQLException { + LOG.severe(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); throw new BigQueryJdbcSqlFeatureNotSupportedException(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); } @Override public final ResultSet getResultSet(Map> map) throws SQLException { + LOG.severe(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); throw new BigQueryJdbcSqlFeatureNotSupportedException(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); } @Override public final ResultSet getResultSet(long index, int count, Map> map) throws SQLException { + LOG.severe(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); throw new BigQueryJdbcSqlFeatureNotSupportedException(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); } @@ -106,6 +110,7 @@ protected Object getArrayInternal(int fromIndex, int toIndexExclusive) { protected void ensureValid() throws IllegalStateException { LOG.finest("++enter++"); if (!this.valid) { + LOG.severe(INVALID_ARRAY); throw new IllegalStateException(INVALID_ARRAY); } } @@ -127,6 +132,8 @@ protected Tuple createRange(long index, int count, int size) // jdbc array follows 1 based array indexing long normalisedFromIndex = index - 1; if (normalisedFromIndex + count > size) { + LOG.severe( + "The array index is out of range: %d, number of elements: %d.", index + count, size); throw new IllegalArgumentException( String.format( "The array index is out of range: %d, number of elements: %d.", index + count, size)); @@ -166,6 +173,7 @@ public String toString() { } return Arrays.deepToString(array); } catch (SQLException e) { + LOG.warning("Error converting array to string"); return "[Error converting array to string: " + e.getMessage() + "]"; } } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java index ab9cf61cb85d..fc815a4e17bf 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryBaseStruct.java @@ -42,11 +42,13 @@ abstract class BigQueryBaseStruct implements java.sql.Struct { @Override public final String getSQLTypeName() throws SQLException { + LOG.severe(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); throw new BigQueryJdbcSqlFeatureNotSupportedException(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); } @Override public final Object[] getAttributes(Map> map) throws SQLException { + LOG.severe(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); throw new BigQueryJdbcSqlFeatureNotSupportedException(CUSTOMER_TYPE_MAPPING_NOT_SUPPORTED); } @@ -91,6 +93,7 @@ public String toString() { sb.append("}"); return sb.toString(); } catch (SQLException e) { + LOG.severe(e, "Error converting struct to string"); return "{ \"error\": \"Error converting struct to string: " + e.getMessage() + "\" }"; } } 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 73bbb985799f..816d0a8547df 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 @@ -297,6 +297,7 @@ BigQueryReadClient getBigQueryReadClient() { this.bigQueryReadClient = getBigQueryReadClientConnection(); } } catch (IOException e) { + LOG.severe(e, "Failed to initialize BigQueryReadClient"); throw new BigQueryJdbcRuntimeException(e); } return this.bigQueryReadClient; @@ -308,6 +309,7 @@ BigQueryWriteClient getBigQueryWriteClient() { this.bigQueryWriteClient = getBigQueryWriteClientConnection(); } } catch (IOException e) { + LOG.severe(e, "Failed to initialize BigQueryWriteClient"); throw new BigQueryJdbcRuntimeException(e); } return this.bigQueryWriteClient; @@ -536,6 +538,7 @@ private void beginTransaction() { } this.transactionStarted = true; } catch (InterruptedException ex) { + LOG.severe(ex, "Failed to begin transaction"); throw new BigQueryJdbcRuntimeException(ex); } } @@ -773,6 +776,7 @@ public void rollback() throws SQLException { beginTransaction(); } } catch (InterruptedException | BigQueryException ex) { + LOG.severe(ex, "Failed to rollback transaction"); throw new BigQueryJdbcException(ex); } } @@ -848,8 +852,10 @@ public void close() throws SQLException { } this.openStatements.clear(); } catch (ConcurrentModificationException ex) { + LOG.severe(ex, "Concurrent modification during close"); throw new BigQueryJdbcException(ex); } catch (InterruptedException e) { + LOG.severe(e, "Interrupted during close"); throw new BigQueryJdbcRuntimeException(e); } this.isClosed = true; diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDaemonPollingTask.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDaemonPollingTask.java index 386785660a20..9c7141f542e0 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDaemonPollingTask.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryDaemonPollingTask.java @@ -113,9 +113,11 @@ else if (referenceQueueJsonRs != null) { reference.clear(); } } else { + LOG.severe("Null Reference Queue"); throw new BigQueryJdbcRuntimeException("Null Reference Queue"); } } catch (InterruptedException ex) { + LOG.severe(ex, "Interrupted in GC daemon task"); throw new BigQueryJdbcRuntimeException(ex); } } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java index 9412b2fd795e..31b8b8ba7a88 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLogger.java @@ -16,6 +16,9 @@ package com.google.cloud.bigquery.jdbc; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.LogRecord; import java.util.logging.Logger; class BigQueryJdbcCustomLogger extends Logger { @@ -30,31 +33,80 @@ protected BigQueryJdbcCustomLogger(String name, String resourceBundleName) { this.setParent(BigQueryJdbcRootLogger.getRootLogger()); } + private void logWithCaller(Level level, Supplier msgSupplier) { + logWithCaller(level, null, msgSupplier); + } + + private void logWithCaller(Level level, Throwable thrown, Supplier msgSupplier) { + if (!isLoggable(level)) { + return; + } + + StackTraceElement[] stackTrace = new Throwable().getStackTrace(); + String sourceClass = "unknown"; + String sourceMethod = "unknown"; + + for (StackTraceElement element : stackTrace) { + String className = element.getClassName(); + if (!className.equals(BigQueryJdbcCustomLogger.class.getName())) { + sourceClass = className; + sourceMethod = element.getMethodName(); + break; + } + } + + if (thrown == null) { + logp(level, sourceClass, sourceMethod, msgSupplier); + } else { + LogRecord record = new LogRecord(level, msgSupplier.get()); + record.setSourceClassName(sourceClass); + record.setSourceMethodName(sourceMethod); + record.setThrown(thrown); + log(record); + } + } + void finest(String format, Object... args) { - this.finest(() -> String.format(format, args)); + logWithCaller(Level.FINEST, () -> String.format(format, args)); } void finer(String format, Object... args) { - this.finer(() -> String.format(format, args)); + logWithCaller(Level.FINER, () -> String.format(format, args)); } void fine(String format, Object... args) { - this.fine(() -> String.format(format, args)); + logWithCaller(Level.FINE, () -> String.format(format, args)); } void config(String format, Object... args) { - this.config(() -> String.format(format, args)); + logWithCaller(Level.CONFIG, () -> String.format(format, args)); } void info(String format, Object... args) { - this.info(() -> String.format(format, args)); + logWithCaller(Level.INFO, () -> String.format(format, args)); } void warning(String format, Object... args) { - this.warning(() -> String.format(format, args)); + logWithCaller(Level.WARNING, () -> String.format(format, args)); + } + + void warning(Throwable thrown, String msg) { + logWithCaller(Level.WARNING, thrown, () -> msg); + } + + void warning(Throwable thrown, String format, Object... args) { + logWithCaller(Level.WARNING, thrown, () -> String.format(format, args)); } void severe(String format, Object... args) { - this.severe(() -> String.format(format, args)); + logWithCaller(Level.SEVERE, () -> String.format(format, args)); + } + + void severe(Throwable thrown, String msg) { + logWithCaller(Level.SEVERE, thrown, () -> msg); + } + + void severe(Throwable thrown, String format, Object... args) { + logWithCaller(Level.SEVERE, thrown, () -> String.format(format, args)); } } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java index 52eef2739d24..571754e82996 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcProxyUtility.java @@ -71,6 +71,8 @@ static Map parseProxyProperties(DataSource ds, String callerClas String proxyPort = ds.getProxyPort(); if (proxyPort != null) { if (!Pattern.compile(validPortRegex).matcher(proxyPort).find()) { + LOG.severe( + "Illegal port number provided %s. Please provide a valid port number.", proxyPort); throw new IllegalArgumentException( String.format( "Illegal port number provided %s. Please provide a valid port number.", proxyPort)); @@ -89,6 +91,8 @@ static Map parseProxyProperties(DataSource ds, String callerClas boolean isMissingProxyHostOrPortWhenProxySet = (proxyHost == null && proxyPort != null) || (proxyHost != null && proxyPort == null); if (isMissingProxyHostOrPortWhenProxySet) { + LOG.severe( + "Both ProxyHost and ProxyPort parameters need to be specified. No defaulting behavior occurs."); throw new IllegalArgumentException( "Both ProxyHost and ProxyPort parameters need to be specified. No defaulting behavior" + " occurs."); @@ -96,11 +100,14 @@ static Map parseProxyProperties(DataSource ds, String callerClas boolean isMissingProxyUidOrPwdWhenAuthSet = (proxyUid == null && proxyPwd != null) || (proxyUid != null && proxyPwd == null); if (isMissingProxyUidOrPwdWhenAuthSet) { + LOG.severe("Both ProxyUid and ProxyPwd parameters need to be specified for authentication."); throw new IllegalArgumentException( "Both ProxyUid and ProxyPwd parameters need to be specified for authentication."); } boolean isProxyAuthSetWithoutProxySettings = proxyUid != null && proxyHost == null; if (isProxyAuthSetWithoutProxySettings) { + LOG.severe( + "Proxy authentication provided via connection string with no proxy host or port set."); throw new IllegalArgumentException( "Proxy authentication provided via connection string with no proxy host or port set."); } @@ -189,6 +196,7 @@ private static HttpTransportFactory getHttpTransportFactory( .setSSLSocketFactory(sslSocketFactory) .build()); } catch (IOException | GeneralSecurityException e) { + LOG.severe(e, "Failed to configure SSL TrustStore for HTTP transport"); throw new BigQueryJdbcRuntimeException(e); } } @@ -278,6 +286,7 @@ public ProxiedSocketAddress proxyFor(SocketAddress socketAddress) { .sslContext(grpcSslContext); } catch (IOException | GeneralSecurityException e) { + LOG.severe(e, "Failed to configure SSL TrustStore for GRPC channel"); throw new BigQueryJdbcRuntimeException(e); } } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java index 8c213ae8c4a0..cb9b6d0835dc 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLogger.java @@ -124,6 +124,12 @@ public String format(LogRecord record) { .append(record.getMessage()) .append(System.lineSeparator()); + if (record.getThrown() != null) { + java.io.StringWriter sw = new java.io.StringWriter(); + record.getThrown().printStackTrace(new java.io.PrintWriter(sw)); + sb.append(sw.toString()).append(System.lineSeparator()); + } + return sb.toString(); } }; diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryParameterHandler.java b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryParameterHandler.java index 5dbf731a0fba..fd0ab97ab129 100644 --- a/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryParameterHandler.java +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryParameterHandler.java @@ -66,6 +66,7 @@ QueryJobConfiguration.Builder configureParameters( QueryParameterValue.of(parameterValue, sqlType)); } } catch (NullPointerException e) { + LOG.severe(e, "Null parameter mapping encountered."); if (e.getMessage().contains("Null type")) { throw new BigQueryJdbcException("One or more parameters missing in Prepared statement.", e); } @@ -103,6 +104,7 @@ void setParameter(int parameterIndex, Object value, Class type) private void checkValidIndex(int parameterIndex) { if (parameterIndex > this.parametersArraySize) { + LOG.severe("All parameters already provided."); throw new IndexOutOfBoundsException("All parameters already provided."); } } @@ -151,6 +153,7 @@ void setParameter( LOG.finest("++enter++"); LOG.finest("setParameter called by : %s", type.getName()); if (paramName == null || paramName.isEmpty()) { + LOG.severe("paramName cannot be null or empty"); throw new IllegalArgumentException("paramName cannot be null or empty"); } BigQueryJdbcParameter parameter = null; diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java new file mode 100644 index 000000000000..ee38c3bfdeec --- /dev/null +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcCustomLoggerTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.bigquery.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class BigQueryJdbcCustomLoggerTest { + + private BigQueryJdbcCustomLogger logger; + private TestHandler testHandler; + + private static class TestHandler extends Handler { + private final List records = new ArrayList<>(); + + @Override + public void publish(LogRecord record) { + records.add(record); + } + + @Override + public void flush() {} + + @Override + public void close() throws SecurityException {} + + public List getRecords() { + return records; + } + } + + @BeforeEach + public void setUp() { + logger = new BigQueryJdbcCustomLogger("TestLogger"); + testHandler = new TestHandler(); + logger.addHandler(testHandler); + logger.setLevel(Level.ALL); + } + + @AfterEach + public void tearDown() { + if (logger != null && testHandler != null) { + logger.removeHandler(testHandler); + } + } + + @Test + public void testLogWithCallerInference() { + logger.fine("Test message with format %s", "arg"); + + List records = testHandler.getRecords(); + assertEquals(1, records.size()); + LogRecord record = records.get(0); + + assertEquals("testLogWithCallerInference", record.getSourceMethodName()); + assertEquals(BigQueryJdbcCustomLoggerTest.class.getName(), record.getSourceClassName()); + } + + @Test + public void testLogWithException() { + Exception ex = new Exception("Test exception"); + logger.severe(ex, "Error occurred: %s", "detail"); + + List records = testHandler.getRecords(); + assertEquals(1, records.size()); + LogRecord record = records.get(0); + + assertEquals("testLogWithException", record.getSourceMethodName()); + assertEquals(BigQueryJdbcCustomLoggerTest.class.getName(), record.getSourceClassName()); + assertTrue(record.getMessage().contains("Error occurred: detail")); + assertEquals(ex, record.getThrown()); + } +}