Skip to content

Commit 5846197

Browse files
feat(bqjdbc): Per connection logs - Add PerConnectionFileHandler (#12899)
- PerConnectionFileHandler.java: Introduced a logging handler mapping logs via InheritableThreadLocal constructs. - BigQueryJdbcMdc.java: Reduced interface exposure to strict package scopes. - PerConnectionFileHandlerTest.java: Introduced coverage validations verifying default execution routing. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 5215b11 commit 5846197

3 files changed

Lines changed: 233 additions & 5 deletions

File tree

java-bigquery/google-cloud-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryJdbcMdc.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
* Allocates a dedicated, independent InheritableThreadLocal object per concrete BigQueryConnection
2525
* instance.
2626
*/
27-
public class BigQueryJdbcMdc {
27+
class BigQueryJdbcMdc {
2828
private static final AtomicLong nextId = new AtomicLong(1);
2929
private static final ConcurrentHashMap<BigQueryConnection, InheritableThreadLocal<String>>
3030
instanceLocals = new ConcurrentHashMap<>();
@@ -35,7 +35,7 @@ public class BigQueryJdbcMdc {
3535
private static final InheritableThreadLocal<String> currentConnectionId =
3636
new InheritableThreadLocal<>();
3737

38-
public static void registerInstance(BigQueryConnection connection, String id) {
38+
static void registerInstance(BigQueryConnection connection, String id) {
3939
if (connection != null) {
4040
String cleanId =
4141
instanceIds.computeIfAbsent(
@@ -56,12 +56,12 @@ public static void registerInstance(BigQueryConnection connection, String id) {
5656
/**
5757
* Returns the connection ID carried by any registered active connection on the current thread.
5858
*/
59-
public static String getConnectionId() {
59+
static String getConnectionId() {
6060
return currentConnectionId.get();
6161
}
6262

6363
/** Clears the connection ID context from all active connection contexts on the current thread. */
64-
public static void removeInstance(BigQueryConnection connection) {
64+
static void removeInstance(BigQueryConnection connection) {
6565
if (connection != null) {
6666
InheritableThreadLocal<String> local = instanceLocals.remove(connection);
6767
if (local != null) {
@@ -71,7 +71,7 @@ public static void removeInstance(BigQueryConnection connection) {
7171
}
7272
}
7373

74-
public static void clear() {
74+
static void clear() {
7575
currentConnectionId.remove();
7676
for (InheritableThreadLocal<String> local : instanceLocals.values()) {
7777
local.remove();
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigquery.jdbc;
18+
19+
import java.io.IOException;
20+
import java.nio.file.Files;
21+
import java.nio.file.Path;
22+
import java.nio.file.Paths;
23+
import java.util.concurrent.ConcurrentHashMap;
24+
import java.util.logging.FileHandler;
25+
import java.util.logging.Handler;
26+
import java.util.logging.Level;
27+
import java.util.logging.LogRecord;
28+
29+
/**
30+
* Custom logging handler that dynamically creates and routes log records to per-connection specific
31+
* log files using the connection ID retrieved from BigQueryJdbcMdc.
32+
*/
33+
class PerConnectionFileHandler extends Handler {
34+
private final Path baseLogPath;
35+
private final Level level;
36+
private final ConcurrentHashMap<String, FileHandler> handlers = new ConcurrentHashMap<>();
37+
private FileHandler defaultHandler;
38+
39+
PerConnectionFileHandler(String baseLogPath, Level level) {
40+
this.baseLogPath = Paths.get(baseLogPath != null ? baseLogPath : "").toAbsolutePath();
41+
this.level = level;
42+
43+
try {
44+
if (!Files.exists(this.baseLogPath)) {
45+
Files.createDirectories(this.baseLogPath);
46+
}
47+
48+
this.defaultHandler = createFileHandler("Jdbc-default");
49+
} catch (IOException e) {
50+
reportError(
51+
"Failed to initialize default log file", e, java.util.logging.ErrorManager.OPEN_FAILURE);
52+
}
53+
}
54+
55+
private String getLogFilePath(String id) {
56+
return baseLogPath.resolve("BigQuery-" + id + ".log").toString();
57+
}
58+
59+
private FileHandler createFileHandler(String id) {
60+
try {
61+
String filePath = getLogFilePath(id);
62+
FileHandler fh = new FileHandler(filePath, 0, 1, true);
63+
fh.setLevel(level);
64+
fh.setFormatter(BigQueryJdbcRootLogger.getFormatter());
65+
return fh;
66+
} catch (IOException e) {
67+
reportError(
68+
"Failed to create log file for connection " + id,
69+
e,
70+
java.util.logging.ErrorManager.OPEN_FAILURE);
71+
return null;
72+
}
73+
}
74+
75+
@Override
76+
public void publish(LogRecord record) {
77+
if (!isLoggable(record)) {
78+
return;
79+
}
80+
81+
String connectionId = BigQueryJdbcMdc.getConnectionId();
82+
FileHandler handler = defaultHandler;
83+
84+
if (connectionId != null && !connectionId.isEmpty()) {
85+
handler = handlers.computeIfAbsent(connectionId, this::createFileHandler);
86+
if (handler == null) {
87+
handler = defaultHandler;
88+
}
89+
}
90+
91+
if (handler != null) {
92+
handler.publish(record);
93+
}
94+
}
95+
96+
@Override
97+
public void flush() {
98+
if (defaultHandler != null) {
99+
defaultHandler.flush();
100+
}
101+
for (FileHandler h : handlers.values()) {
102+
h.flush();
103+
}
104+
}
105+
106+
@Override
107+
public void close() throws SecurityException {
108+
for (FileHandler h : handlers.values()) {
109+
try {
110+
h.close();
111+
} catch (Exception e) {
112+
}
113+
}
114+
try {
115+
if (defaultHandler != null) defaultHandler.close();
116+
} finally {
117+
handlers.clear();
118+
}
119+
}
120+
121+
public void closeHandler(String connectionId) {
122+
if (connectionId != null) {
123+
FileHandler handler = handlers.remove(connectionId);
124+
if (handler != null) {
125+
handler.close();
126+
}
127+
}
128+
}
129+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.bigquery.jdbc;
18+
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.util.logging.Level;
25+
import java.util.logging.LogRecord;
26+
import org.junit.jupiter.api.AfterEach;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
import org.junit.jupiter.api.io.TempDir;
30+
import org.mockito.Mockito;
31+
32+
public class PerConnectionFileHandlerTest {
33+
34+
@TempDir Path tempDir;
35+
36+
private PerConnectionFileHandler handler;
37+
private BigQueryConnection mockConnection;
38+
39+
@BeforeEach
40+
public void setUp() {
41+
handler = new PerConnectionFileHandler(tempDir.toString(), Level.INFO);
42+
mockConnection = Mockito.mock(BigQueryConnection.class);
43+
BigQueryJdbcMdc.clear();
44+
}
45+
46+
@AfterEach
47+
public void tearDown() {
48+
if (handler != null) {
49+
handler.close();
50+
}
51+
BigQueryJdbcMdc.clear();
52+
}
53+
54+
@Test
55+
public void testInitialization() {
56+
Path defaultLog = tempDir.resolve("BigQuery-Jdbc-default.log");
57+
assertTrue(Files.exists(defaultLog));
58+
}
59+
60+
@Test
61+
public void testPublishDefault() throws IOException {
62+
LogRecord record = new LogRecord(Level.INFO, "Test message default");
63+
handler.publish(record);
64+
handler.flush();
65+
66+
Path defaultLog = tempDir.resolve("BigQuery-Jdbc-default.log");
67+
String content = new String(Files.readAllBytes(defaultLog));
68+
assertTrue(content.contains("Test message default"));
69+
}
70+
71+
@Test
72+
public void testPublishConnectionSpecific() throws IOException {
73+
BigQueryJdbcMdc.registerInstance(mockConnection, "123");
74+
75+
LogRecord record = new LogRecord(Level.INFO, "Test message connection 123");
76+
handler.publish(record);
77+
handler.flush();
78+
79+
Path connLog = tempDir.resolve("BigQuery-JdbcConnection-123.log");
80+
assertTrue(Files.exists(connLog));
81+
String content = new String(Files.readAllBytes(connLog));
82+
assertTrue(content.contains("Test message connection 123"));
83+
}
84+
85+
@Test
86+
public void testCloseHandler() {
87+
BigQueryJdbcMdc.registerInstance(mockConnection, "456");
88+
89+
LogRecord record = new LogRecord(Level.INFO, "Test message connection 456");
90+
handler.publish(record);
91+
handler.flush();
92+
93+
Path connLog = tempDir.resolve("BigQuery-JdbcConnection-456.log");
94+
assertTrue(Files.exists(connLog));
95+
96+
handler.closeHandler("JdbcConnection-456");
97+
assertTrue(Files.exists(connLog));
98+
}
99+
}

0 commit comments

Comments
 (0)