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 a6723441550e..8c213ae8c4a0 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 @@ -16,6 +16,7 @@ package com.google.cloud.bigquery.jdbc; +import com.google.common.base.Strings; import java.io.IOException; import java.lang.management.ManagementFactory; import java.nio.file.Files; @@ -24,7 +25,6 @@ import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.Date; -import java.util.Optional; import java.util.logging.ConsoleHandler; import java.util.logging.FileHandler; import java.util.logging.Formatter; @@ -49,6 +49,32 @@ class BigQueryJdbcRootLogger { private static Path currentLogPath = null; private static int fileCounter = 0; + static final String PROCESS_ID = ManagementFactory.getRuntimeMXBean().getName().split("@")[0]; + + private static final ThreadLocal DATE_FORMATTER = + ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")); + + static String getThreadName(long threadId) { + Thread current = Thread.currentThread(); + if (current.getId() == threadId) { + return current.getName(); + } + ThreadGroup rootGroup = current.getThreadGroup(); + while (rootGroup.getParent() != null) { + rootGroup = rootGroup.getParent(); + } + + int count = rootGroup.activeCount(); + Thread[] threads = new Thread[count * 2]; + int actualCount = rootGroup.enumerate(threads); + for (int i = 0; i < actualCount; i++) { + if (threads[i].getId() == threadId) { + return threads[i].getName(); + } + } + return ""; + } + static { logger.setUseParentHandlers(false); storageLogger.setUseParentHandlers(true); @@ -62,49 +88,43 @@ class BigQueryJdbcRootLogger { public static Formatter getFormatter() { return new Formatter() { - private static final String PATTERN = "yyyy-MM-dd HH:mm:ss.SSS"; - private static final String FORMAT = - "%1$s %2$5s %3$d --- [%4$-7.15s] %5$-50s %6$-20s: %7$s%8$s"; private static final int MAX_THREAD_NAME_LENGTH = 15; - /** - * Returns the thread for the given thread id. - * - * @param threadId ID for the thread being logged. - * @return returns the thread - */ - Optional getThread(long threadId) { - return Thread.getAllStackTraces().keySet().stream() - .filter(thread -> thread.getId() == threadId) - .findFirst(); - } - @Override public String format(LogRecord record) { - String date = new SimpleDateFormat(PATTERN).format(new Date(record.getMillis())); - String threadName = - getThread(record.getThreadID()) - .map(Thread::getName) - .map( - name -> - name.length() > MAX_THREAD_NAME_LENGTH - ? name.substring(name.length() - MAX_THREAD_NAME_LENGTH) - : name) - .orElse(""); - long processId = - Long.parseLong(ManagementFactory.getRuntimeMXBean().getName().split("@")[0]); + String date = DATE_FORMATTER.get().format(new Date(record.getMillis())); + + long threadId = record.getThreadID(); + String threadName = getThreadName(threadId); + + if (threadName.length() > MAX_THREAD_NAME_LENGTH) { + threadName = threadName.substring(threadName.length() - MAX_THREAD_NAME_LENGTH); + } + String sourceClassName = record.getLoggerName(); String sourceMethodName = record.getSourceMethodName(); - return String.format( - FORMAT, - date, - record.getLevel().getName(), - processId, - threadName, - sourceClassName, - sourceMethodName, - record.getMessage(), - System.lineSeparator()); + + // Expected log format: yyyy-MM-dd HH:mm:ss.SSS LEVEL PID --- [THREAD] CLASS METHOD: MESSAGE + // Example: 2026-04-22 10:16:00.123 INFO 12345 --- [main ] + // com.google.cloud.bigquery.jdbc.BigQueryConnection connect : Connection + // successful + StringBuilder sb = new StringBuilder(256); + sb.append(date) + .append(" ") + .append(Strings.padStart(record.getLevel().getName(), 5, ' ')) + .append(" ") + .append(PROCESS_ID) + .append(" --- [") + .append(Strings.padEnd(threadName, 7, ' ')) + .append("] ") + .append(Strings.padEnd(sourceClassName != null ? sourceClassName : "", 50, ' ')) + .append(" ") + .append(Strings.padEnd(sourceMethodName != null ? sourceMethodName : "", 20, ' ')) + .append(": ") + .append(record.getMessage()) + .append(System.lineSeparator()); + + return sb.toString(); } }; } diff --git a/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLoggerTest.java b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLoggerTest.java new file mode 100644 index 000000000000..addae7b907bc --- /dev/null +++ b/java-bigquery/google-cloud-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcRootLoggerTest.java @@ -0,0 +1,74 @@ +/* + * 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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import org.junit.jupiter.api.Test; + +public class BigQueryJdbcRootLoggerTest { + + @Test + public void testGetFormatterFormat() { + Formatter formatter = BigQueryJdbcRootLogger.getFormatter(); + assertNotNull(formatter); + + LogRecord record = new LogRecord(Level.INFO, "Test message"); + record.setMillis(1713715200000L); + record.setLoggerName("TestLogger"); + record.setSourceMethodName("testMethod"); + + String formatted = formatter.format(record); + + assertTrue(formatted.contains("INFO")); + assertTrue(formatted.contains("Test message")); + assertTrue(formatted.contains("TestLogger")); + assertTrue(formatted.contains("testMethod")); + assertTrue(formatted.contains("---")); + } + + @Test + public void testThreadNameTruncation() { + Formatter formatter = BigQueryJdbcRootLogger.getFormatter(); + LogRecord record = new LogRecord(Level.INFO, "Test message"); + + String formatted = formatter.format(record); + int startIndex = formatted.indexOf("--- [") + 5; + int endIndex = formatted.indexOf("]", startIndex); + String threadPart = formatted.substring(startIndex, endIndex).trim(); + + assertTrue(threadPart.length() <= 15); + } + + @Test + public void testGetThreadName() { + Thread current = Thread.currentThread(); + String name = BigQueryJdbcRootLogger.getThreadName(current.getId()); + assertEquals(current.getName(), name); + } + + @Test + public void testGetThreadNameNotFound() { + String name = BigQueryJdbcRootLogger.getThreadName(-1); + assertEquals("", name); + } +}