Skip to content

Commit a8043ce

Browse files
Add java.util.logging backend
1 parent cbc43b9 commit a8043ce

File tree

7 files changed

+546
-1
lines changed

7 files changed

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