Skip to content

Commit 02b6cb5

Browse files
Add java.util.logging backend
1 parent 9597522 commit 02b6cb5

File tree

7 files changed

+541
-1
lines changed

7 files changed

+541
-1
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
### Internal Changes
1515
* Introduced a logging abstraction (`com.databricks.sdk.core.logging`) that decouples the SDK from SLF4J. Users can now provide their own logging backend by extending `LoggerFactory` and calling `LoggerFactory.setDefault()` before creating any SDK client. SLF4J remains the default.
16+
* Added `java.util.logging` as a supported alternative logging backend. Activate it with `LoggerFactory.setDefault(JulLoggerFactory.INSTANCE)`.
1617

1718
### API Changes
1819
* Add `createCatalog()`, `createSyncedTable()`, `deleteCatalog()`, `deleteSyncedTable()`, `getCatalog()` and `getSyncedTable()` methods for `workspaceClient.postgres()` service.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package com.databricks.sdk.core.logging;
2+
3+
import java.util.Arrays;
4+
import java.util.logging.Level;
5+
import java.util.logging.LogRecord;
6+
7+
/** Delegates logging calls to a {@code java.util.logging.Logger}, translating SLF4J conventions. */
8+
class JulLogger extends Logger {
9+
10+
private static final String LOGGING_PACKAGE = "com.databricks.sdk.core.logging.";
11+
12+
private final java.util.logging.Logger delegate;
13+
14+
private JulLogger(java.util.logging.Logger delegate) {
15+
this.delegate = delegate;
16+
}
17+
18+
static Logger create(Class<?> type) {
19+
return create(type.getName());
20+
}
21+
22+
static Logger create(String name) {
23+
java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger(name);
24+
return new JulLogger(julLogger);
25+
}
26+
27+
@Override
28+
public boolean isDebugEnabled() {
29+
return delegate.isLoggable(Level.FINE);
30+
}
31+
32+
@Override
33+
public void debug(String msg) {
34+
log(Level.FINE, msg, null);
35+
}
36+
37+
@Override
38+
public void debug(String format, Object... args) {
39+
log(Level.FINE, format, args);
40+
}
41+
42+
@Override
43+
public void info(String msg) {
44+
log(Level.INFO, msg, null);
45+
}
46+
47+
@Override
48+
public void info(String format, Object... args) {
49+
log(Level.INFO, format, args);
50+
}
51+
52+
@Override
53+
public void warn(String msg) {
54+
log(Level.WARNING, msg, null);
55+
}
56+
57+
@Override
58+
public void warn(String format, Object... args) {
59+
log(Level.WARNING, format, args);
60+
}
61+
62+
@Override
63+
public void error(String msg) {
64+
log(Level.SEVERE, msg, null);
65+
}
66+
67+
@Override
68+
public void error(String format, Object... args) {
69+
log(Level.SEVERE, format, args);
70+
}
71+
72+
private void log(Level level, String format, Object[] args) {
73+
if (!delegate.isLoggable(level)) {
74+
return;
75+
}
76+
Throwable thrown = (args != null) ? extractThrowable(format, args) : null;
77+
String message = (args != null) ? formatMessage(format, args) : format;
78+
LogRecord record = new LogRecord(level, message);
79+
record.setLoggerName(delegate.getName());
80+
if (thrown != null) {
81+
record.setThrown(thrown);
82+
}
83+
inferCaller(record);
84+
delegate.log(record);
85+
}
86+
87+
/**
88+
* Sets the source class and method on a {@link LogRecord} by walking the call stack to find the
89+
* first frame outside this logging package.
90+
*
91+
* <p>JUL normally infers caller information automatically by scanning the stack for the first
92+
* frame after its own {@code java.util.logging.Logger} methods. Because {@code JulLogger} wraps
93+
* the JUL logger, that automatic inference stops at {@code JulLogger} or its helper methods
94+
* instead of reaching the actual SDK class that initiated the log call. Without this correction,
95+
* every log record would be attributed to {@code JulLogger}, making JUL output useless for
96+
* identifying the real call site.
97+
*/
98+
private static void inferCaller(LogRecord record) {
99+
StackTraceElement[] stack = new Throwable().getStackTrace();
100+
for (StackTraceElement frame : stack) {
101+
if (!frame.getClassName().startsWith(LOGGING_PACKAGE)) {
102+
record.setSourceClassName(frame.getClassName());
103+
record.setSourceMethodName(frame.getMethodName());
104+
return;
105+
}
106+
}
107+
}
108+
109+
/**
110+
* Replaces SLF4J-style {@code {}} placeholders with argument values, matching the semantics of
111+
* SLF4J's {@code MessageFormatter.arrayFormat}:
112+
*
113+
* <ul>
114+
* <li>A trailing {@link Throwable} is unconditionally excluded from formatting.
115+
* <li>A backslash before {@code {}} escapes it as a literal {@code {}}.
116+
* <li>Array arguments are rendered with {@link Arrays#deepToString}.
117+
* <li>A {@code null} format string returns {@code null}.
118+
* </ul>
119+
*/
120+
static String formatMessage(String format, Object[] args) {
121+
if (format == null) {
122+
return null;
123+
}
124+
if (args == null || args.length == 0) {
125+
return format;
126+
}
127+
int usableArgs = args.length;
128+
if (args[usableArgs - 1] instanceof Throwable) {
129+
usableArgs--;
130+
}
131+
StringBuilder sb = new StringBuilder(format.length() + 32);
132+
int argIdx = 0;
133+
int i = 0;
134+
while (i < format.length()) {
135+
if (i + 1 < format.length() && format.charAt(i) == '{' && format.charAt(i + 1) == '}') {
136+
if (i > 0 && format.charAt(i - 1) == '\\') {
137+
sb.setLength(sb.length() - 1);
138+
sb.append("{}");
139+
} else if (argIdx < usableArgs) {
140+
sb.append(renderArg(args[argIdx++]));
141+
} else {
142+
sb.append("{}");
143+
}
144+
i += 2;
145+
} else {
146+
sb.append(format.charAt(i));
147+
i++;
148+
}
149+
}
150+
return sb.toString();
151+
}
152+
153+
private static String renderArg(Object arg) {
154+
if (arg == null) {
155+
return "null";
156+
}
157+
if (arg instanceof Object[]) {
158+
return Arrays.deepToString((Object[]) arg);
159+
}
160+
if (arg.getClass().isArray()) {
161+
return primitiveArrayToString(arg);
162+
}
163+
return arg.toString();
164+
}
165+
166+
private static String primitiveArrayToString(Object array) {
167+
if (array instanceof boolean[]) return Arrays.toString((boolean[]) array);
168+
if (array instanceof byte[]) return Arrays.toString((byte[]) array);
169+
if (array instanceof char[]) return Arrays.toString((char[]) array);
170+
if (array instanceof short[]) return Arrays.toString((short[]) array);
171+
if (array instanceof int[]) return Arrays.toString((int[]) array);
172+
if (array instanceof long[]) return Arrays.toString((long[]) array);
173+
if (array instanceof float[]) return Arrays.toString((float[]) array);
174+
if (array instanceof double[]) return Arrays.toString((double[]) array);
175+
return Arrays.deepToString(new Object[] {array});
176+
}
177+
178+
/**
179+
* Returns the last argument if it is a {@link Throwable}, unconditionally. This matches SLF4J's
180+
* {@code NormalizedParameters.getThrowableCandidate}, which always extracts a trailing Throwable
181+
* regardless of how many {@code {}} placeholders the format string contains.
182+
*/
183+
static Throwable extractThrowable(String format, Object[] args) {
184+
if (args == null || args.length == 0) {
185+
return null;
186+
}
187+
Object last = args[args.length - 1];
188+
if (last instanceof Throwable) {
189+
return (Throwable) last;
190+
}
191+
return null;
192+
}
193+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.databricks.sdk.core.logging;
2+
3+
/**
4+
* A {@link LoggerFactory} backed by {@code java.util.logging}. Always available on any JRE.
5+
*
6+
* <p>Use this when SLF4J is not desirable:
7+
*
8+
* <pre>{@code
9+
* LoggerFactory.setDefault(JulLoggerFactory.INSTANCE);
10+
* }</pre>
11+
*/
12+
public class JulLoggerFactory extends LoggerFactory {
13+
14+
public static final JulLoggerFactory INSTANCE = new JulLoggerFactory();
15+
16+
@Override
17+
protected Logger newInstance(Class<?> type) {
18+
return JulLogger.create(type);
19+
}
20+
21+
@Override
22+
protected Logger newInstance(String name) {
23+
return JulLogger.create(name);
24+
}
25+
}

databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/LoggerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* creating any SDK client:
1010
*
1111
* <pre>{@code
12-
* LoggerFactory.setDefault(myCustomFactory);
12+
* LoggerFactory.setDefault(JulLoggerFactory.INSTANCE);
1313
* WorkspaceClient ws = new WorkspaceClient();
1414
* }</pre>
1515
*

0 commit comments

Comments
 (0)