Skip to content

Commit 31ca46e

Browse files
Add logging abstraction with SLF4J backend
1 parent 69918b1 commit 31ca46e

File tree

7 files changed

+356
-0
lines changed

7 files changed

+356
-0
lines changed

NEXT_CHANGELOG.md

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

1515
### Internal Changes
16+
* Introduced a logging abstraction (`com.databricks.sdk.core.logging`) to decouple the SDK from a specific logging backend.
1617

1718
### API Changes
1819
* Add `createCatalog()`, `createSyncedTable()`, `deleteCatalog()`, `deleteSyncedTable()`, `getCatalog()` and `getSyncedTable()` methods for `workspaceClient.postgres()` service.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.databricks.sdk.core.logging;
2+
3+
import java.util.function.Supplier;
4+
5+
/**
6+
* Logging contract used throughout the SDK.
7+
*
8+
* <p>Extend this class to provide a custom logging implementation, then register it via a custom
9+
* {@link LoggerFactory} subclass and {@link LoggerFactory#setDefault}.
10+
*/
11+
public abstract class Logger {
12+
13+
public abstract void debug(String msg);
14+
15+
public abstract void debug(String format, Object... args);
16+
17+
public abstract void debug(Supplier<String> msgSupplier);
18+
19+
public abstract void info(String msg);
20+
21+
public abstract void info(String format, Object... args);
22+
23+
public abstract void info(Supplier<String> msgSupplier);
24+
25+
public abstract void warn(String msg);
26+
27+
public abstract void warn(String format, Object... args);
28+
29+
public abstract void warn(Supplier<String> msgSupplier);
30+
31+
public abstract void error(String msg);
32+
33+
public abstract void error(String format, Object... args);
34+
35+
public abstract void error(Supplier<String> msgSupplier);
36+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.databricks.sdk.core.logging;
2+
3+
import java.util.concurrent.atomic.AtomicReference;
4+
5+
/**
6+
* Creates and configures {@link Logger} instances for the SDK.
7+
*
8+
* <p>By default, logging goes through SLF4J. Users can override the backend programmatically before
9+
* creating any SDK client:
10+
*
11+
* <pre>{@code
12+
* LoggerFactory.setDefault(myCustomFactory);
13+
* WorkspaceClient ws = new WorkspaceClient();
14+
* }</pre>
15+
*
16+
* <p>Extend this class to provide a fully custom logging backend.
17+
*/
18+
public abstract class LoggerFactory {
19+
20+
private static final AtomicReference<LoggerFactory> defaultFactory = new AtomicReference<>();
21+
22+
/** Returns a logger for the given class, using the current default factory. */
23+
public static Logger getLogger(Class<?> type) {
24+
return getDefault().createLogger(type);
25+
}
26+
27+
/** Returns a logger with the given name, using the current default factory. */
28+
public static Logger getLogger(String name) {
29+
return getDefault().createLogger(name);
30+
}
31+
32+
/**
33+
* Overrides the logging backend used by the SDK.
34+
*
35+
* <p>Must be called before creating any SDK client or calling {@link #getLogger}. Loggers already
36+
* obtained will not be affected by subsequent calls.
37+
*/
38+
public static void setDefault(LoggerFactory factory) {
39+
if (factory == null) {
40+
throw new IllegalArgumentException("LoggerFactory must not be null");
41+
}
42+
defaultFactory.set(factory);
43+
}
44+
45+
static LoggerFactory getDefault() {
46+
LoggerFactory f = defaultFactory.get();
47+
if (f != null) {
48+
return f;
49+
}
50+
defaultFactory.compareAndSet(null, Slf4jLoggerFactory.INSTANCE);
51+
return defaultFactory.get();
52+
}
53+
54+
/**
55+
* Creates a {@link Logger} for the given class. Subclasses obtain the backend logger (e.g. SLF4J)
56+
* and return an adapter.
57+
*/
58+
protected abstract Logger createLogger(Class<?> type);
59+
60+
/**
61+
* Creates a {@link Logger} for the given name. Subclasses obtain the backend logger and return an
62+
* adapter.
63+
*/
64+
protected abstract Logger createLogger(String name);
65+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.databricks.sdk.core.logging;
2+
3+
import java.util.function.Supplier;
4+
5+
/** Delegates all logging calls to an SLF4J {@code Logger}. */
6+
class Slf4jLogger extends Logger {
7+
8+
private final org.slf4j.Logger delegate;
9+
10+
Slf4jLogger(org.slf4j.Logger delegate) {
11+
this.delegate = delegate;
12+
}
13+
14+
@Override
15+
public void debug(String msg) {
16+
if (delegate.isDebugEnabled()) {
17+
delegate.debug(msg);
18+
}
19+
}
20+
21+
@Override
22+
public void debug(String format, Object... args) {
23+
if (delegate.isDebugEnabled()) {
24+
delegate.debug(format, args);
25+
}
26+
}
27+
28+
@Override
29+
public void debug(Supplier<String> msgSupplier) {
30+
if (delegate.isDebugEnabled()) {
31+
delegate.debug(msgSupplier.get());
32+
}
33+
}
34+
35+
@Override
36+
public void info(String msg) {
37+
delegate.info(msg);
38+
}
39+
40+
@Override
41+
public void info(String format, Object... args) {
42+
delegate.info(format, args);
43+
}
44+
45+
@Override
46+
public void info(Supplier<String> msgSupplier) {
47+
if (delegate.isInfoEnabled()) {
48+
delegate.info(msgSupplier.get());
49+
}
50+
}
51+
52+
@Override
53+
public void warn(String msg) {
54+
delegate.warn(msg);
55+
}
56+
57+
@Override
58+
public void warn(String format, Object... args) {
59+
delegate.warn(format, args);
60+
}
61+
62+
@Override
63+
public void warn(Supplier<String> msgSupplier) {
64+
if (delegate.isWarnEnabled()) {
65+
delegate.warn(msgSupplier.get());
66+
}
67+
}
68+
69+
@Override
70+
public void error(String msg) {
71+
delegate.error(msg);
72+
}
73+
74+
@Override
75+
public void error(String format, Object... args) {
76+
delegate.error(format, args);
77+
}
78+
79+
@Override
80+
public void error(Supplier<String> msgSupplier) {
81+
if (delegate.isErrorEnabled()) {
82+
delegate.error(msgSupplier.get());
83+
}
84+
}
85+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.databricks.sdk.core.logging;
2+
3+
/** A {@link LoggerFactory} backed by SLF4J. */
4+
public class Slf4jLoggerFactory extends LoggerFactory {
5+
6+
public static final Slf4jLoggerFactory INSTANCE = new Slf4jLoggerFactory();
7+
8+
@Override
9+
protected Logger createLogger(Class<?> type) {
10+
return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(type));
11+
}
12+
13+
@Override
14+
protected Logger createLogger(String name) {
15+
return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(name));
16+
}
17+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.databricks.sdk.core.logging;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import org.junit.jupiter.api.AfterEach;
6+
import org.junit.jupiter.api.Test;
7+
8+
public class LoggerFactoryTest {
9+
10+
@AfterEach
11+
void resetFactory() {
12+
LoggerFactory.setDefault(Slf4jLoggerFactory.INSTANCE);
13+
}
14+
15+
@Test
16+
void defaultFactoryIsSLF4J() {
17+
Logger logger = LoggerFactory.getLogger(LoggerFactoryTest.class);
18+
assertNotNull(logger);
19+
logger.info("LoggerFactory defaultFactoryIsSLF4J test message");
20+
}
21+
22+
@Test
23+
void setDefaultRejectsNull() {
24+
assertThrows(IllegalArgumentException.class, () -> LoggerFactory.setDefault(null));
25+
}
26+
27+
@Test
28+
void getLoggerByNameWorks() {
29+
Logger logger = LoggerFactory.getLogger("com.example.Test");
30+
assertNotNull(logger);
31+
logger.info("getLoggerByNameWorks test message");
32+
}
33+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.databricks.sdk.core.logging;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
import java.util.stream.Stream;
8+
import org.apache.log4j.AppenderSkeleton;
9+
import org.apache.log4j.spi.LoggingEvent;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.params.ParameterizedTest;
12+
import org.junit.jupiter.params.provider.Arguments;
13+
import org.junit.jupiter.params.provider.MethodSource;
14+
15+
public class Slf4jLoggerTest {
16+
17+
@Test
18+
void getLoggerReturnsSlf4jLogger() {
19+
Logger logger = LoggerFactory.getLogger(Slf4jLoggerTest.class);
20+
assertNotNull(logger);
21+
assertTrue(logger instanceof Slf4jLogger);
22+
}
23+
24+
static Stream<Arguments> logCalls() {
25+
RuntimeException ex = new RuntimeException("boom");
26+
return Stream.of(
27+
Arguments.of("debug", "hello", null, "hello", null),
28+
Arguments.of("info", "hello", null, "hello", null),
29+
Arguments.of("warn", "hello", null, "hello", null),
30+
Arguments.of("error", "hello", null, "hello", null),
31+
Arguments.of(
32+
"info", "user {} logged in", new Object[] {"alice"}, "user alice logged in", null),
33+
Arguments.of("info", "a={}, b={}", new Object[] {1, 2}, "a=1, b=2", null),
34+
Arguments.of("error", "failed: {}", new Object[] {"op", ex}, "failed: op", ex),
35+
Arguments.of("error", "Error: {}", new Object[] {ex}, "Error: {}", ex),
36+
Arguments.of("error", "Something broke", new Object[] {ex}, "Something broke", ex));
37+
}
38+
39+
@ParameterizedTest(name = "[{index}] {0}(\"{1}\")")
40+
@MethodSource("logCalls")
41+
void deliversCorrectOutput(
42+
String level, String format, Object[] args, String expectedMsg, Throwable expectedThrown) {
43+
CapturingAppender appender = new CapturingAppender();
44+
org.apache.log4j.Logger log4jLogger = org.apache.log4j.Logger.getLogger(Slf4jLoggerTest.class);
45+
log4jLogger.addAppender(appender);
46+
try {
47+
Logger logger = new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(Slf4jLoggerTest.class));
48+
dispatch(logger, level, format, args);
49+
50+
assertEquals(1, appender.events.size(), "Expected exactly one log event");
51+
LoggingEvent event = appender.events.get(0);
52+
assertEquals(expectedMsg, event.getRenderedMessage());
53+
assertEquals(toLog4jLevel(level), event.getLevel());
54+
if (expectedThrown != null) {
55+
assertNotNull(event.getThrowableInformation(), "Expected throwable to be attached");
56+
assertSame(expectedThrown, event.getThrowableInformation().getThrowable());
57+
} else {
58+
assertNull(event.getThrowableInformation(), "Expected no throwable");
59+
}
60+
} finally {
61+
log4jLogger.removeAppender(appender);
62+
}
63+
}
64+
65+
private static void dispatch(Logger logger, String level, String format, Object[] args) {
66+
switch (level) {
67+
case "debug":
68+
if (args != null) logger.debug(format, args);
69+
else logger.debug(format);
70+
break;
71+
case "info":
72+
if (args != null) logger.info(format, args);
73+
else logger.info(format);
74+
break;
75+
case "warn":
76+
if (args != null) logger.warn(format, args);
77+
else logger.warn(format);
78+
break;
79+
case "error":
80+
if (args != null) logger.error(format, args);
81+
else logger.error(format);
82+
break;
83+
default:
84+
throw new IllegalArgumentException("Unknown level: " + level);
85+
}
86+
}
87+
88+
private static org.apache.log4j.Level toLog4jLevel(String level) {
89+
switch (level) {
90+
case "debug":
91+
return org.apache.log4j.Level.DEBUG;
92+
case "info":
93+
return org.apache.log4j.Level.INFO;
94+
case "warn":
95+
return org.apache.log4j.Level.WARN;
96+
case "error":
97+
return org.apache.log4j.Level.ERROR;
98+
default:
99+
throw new IllegalArgumentException("Unknown level: " + level);
100+
}
101+
}
102+
103+
static class CapturingAppender extends AppenderSkeleton {
104+
final List<LoggingEvent> events = new ArrayList<>();
105+
106+
@Override
107+
protected void append(LoggingEvent event) {
108+
events.add(event);
109+
}
110+
111+
@Override
112+
public void close() {}
113+
114+
@Override
115+
public boolean requiresLayout() {
116+
return false;
117+
}
118+
}
119+
}

0 commit comments

Comments
 (0)