Skip to content

Commit 63c2968

Browse files
Add java.util.logging backend
1 parent 835a202 commit 63c2968

File tree

7 files changed

+534
-1
lines changed

7 files changed

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