-
Notifications
You must be signed in to change notification settings - Fork 35
Add logging abstraction with SLF4J backend #740
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mihaimitrea-db
merged 2 commits into
main
from
mihaimitrea-db/stack/logging-abstraction
Apr 14, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/ILoggerFactory.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package com.databricks.sdk.core.logging; | ||
|
|
||
| /** | ||
| * Provides {@link Logger} instances for a specific logging backend. | ||
| * | ||
| * <p>Implement this interface to provide a custom logging backend, then register it via {@link | ||
| * LoggerFactory#setDefault(ILoggerFactory)}. | ||
| */ | ||
| public interface ILoggerFactory { | ||
|
|
||
| /** Returns a logger for the given class. */ | ||
| Logger getLogger(Class<?> type); | ||
|
|
||
| /** Returns a logger with the given name. */ | ||
| Logger getLogger(String name); | ||
| } |
36 changes: 36 additions & 0 deletions
36
databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Logger.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package com.databricks.sdk.core.logging; | ||
|
|
||
| import java.util.function.Supplier; | ||
|
|
||
| /** | ||
| * Logging contract used throughout the SDK. | ||
| * | ||
| * <p>Extend this class to provide a custom logging implementation, then register it via a custom | ||
| * {@link ILoggerFactory} and {@link LoggerFactory#setDefault}. | ||
| */ | ||
| public abstract class Logger { | ||
|
|
||
| public abstract void debug(String msg); | ||
|
|
||
| public abstract void debug(String format, Object... args); | ||
|
|
||
| public abstract void debug(Supplier<String> msgSupplier); | ||
|
|
||
| public abstract void info(String msg); | ||
|
|
||
| public abstract void info(String format, Object... args); | ||
|
|
||
| public abstract void info(Supplier<String> msgSupplier); | ||
|
|
||
| public abstract void warn(String msg); | ||
|
|
||
| public abstract void warn(String format, Object... args); | ||
|
|
||
| public abstract void warn(Supplier<String> msgSupplier); | ||
|
|
||
| public abstract void error(String msg); | ||
|
|
||
| public abstract void error(String format, Object... args); | ||
|
|
||
| public abstract void error(Supplier<String> msgSupplier); | ||
| } |
55 changes: 55 additions & 0 deletions
55
databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/LoggerFactory.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package com.databricks.sdk.core.logging; | ||
|
|
||
| import java.util.concurrent.atomic.AtomicReference; | ||
|
|
||
| /** | ||
| * Static entry point for obtaining {@link Logger} instances. | ||
| * | ||
| * <p>By default, logging goes through SLF4J. Users can override the backend programmatically before | ||
| * creating any SDK client: | ||
| * | ||
| * <pre>{@code | ||
| * LoggerFactory.setDefault(myCustomFactory); | ||
| * WorkspaceClient ws = new WorkspaceClient(); | ||
| * }</pre> | ||
| * | ||
| * <p>Implement {@link ILoggerFactory} to provide a fully custom logging backend. | ||
| */ | ||
| public final class LoggerFactory { | ||
|
|
||
| private static final AtomicReference<ILoggerFactory> defaultFactory = new AtomicReference<>(); | ||
|
|
||
| private LoggerFactory() {} | ||
|
|
||
| /** Returns a logger for the given class, using the current default factory. */ | ||
| public static Logger getLogger(Class<?> type) { | ||
| return getDefault().getLogger(type); | ||
| } | ||
|
|
||
| /** Returns a logger with the given name, using the current default factory. */ | ||
| public static Logger getLogger(String name) { | ||
| return getDefault().getLogger(name); | ||
| } | ||
|
|
||
| /** | ||
| * Overrides the logging backend used by the SDK. | ||
| * | ||
| * <p>Must be called before creating any SDK client or calling {@link #getLogger}. Loggers already | ||
| * obtained will not be affected by subsequent calls. | ||
| */ | ||
| public static void setDefault(ILoggerFactory factory) { | ||
| if (factory == null) { | ||
| throw new IllegalArgumentException("ILoggerFactory must not be null"); | ||
| } | ||
| defaultFactory.set(factory); | ||
| } | ||
|
|
||
| static ILoggerFactory getDefault() { | ||
| ILoggerFactory f = defaultFactory.get(); | ||
| if (f != null) { | ||
| return f; | ||
| } | ||
| defaultFactory.compareAndSet(null, Slf4jLoggerFactory.INSTANCE); | ||
| return defaultFactory.get(); | ||
| } | ||
| } |
81 changes: 81 additions & 0 deletions
81
databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLogger.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package com.databricks.sdk.core.logging; | ||
|
|
||
| import java.util.function.Supplier; | ||
|
|
||
| /** Delegates all logging calls to an SLF4J {@code Logger}. */ | ||
| class Slf4jLogger extends Logger { | ||
|
|
||
| private final org.slf4j.Logger delegate; | ||
|
|
||
| Slf4jLogger(org.slf4j.Logger delegate) { | ||
| this.delegate = delegate; | ||
| } | ||
|
|
||
| @Override | ||
| public void debug(String msg) { | ||
| delegate.debug(msg); | ||
| } | ||
|
|
||
| @Override | ||
| public void debug(String format, Object... args) { | ||
| delegate.debug(format, args); | ||
| } | ||
|
|
||
| @Override | ||
| public void debug(Supplier<String> msgSupplier) { | ||
| if (delegate.isDebugEnabled()) { | ||
| delegate.debug(msgSupplier.get()); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void info(String msg) { | ||
| delegate.info(msg); | ||
| } | ||
|
|
||
| @Override | ||
| public void info(String format, Object... args) { | ||
| delegate.info(format, args); | ||
| } | ||
|
|
||
| @Override | ||
| public void info(Supplier<String> msgSupplier) { | ||
| if (delegate.isInfoEnabled()) { | ||
| delegate.info(msgSupplier.get()); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void warn(String msg) { | ||
| delegate.warn(msg); | ||
| } | ||
|
|
||
| @Override | ||
| public void warn(String format, Object... args) { | ||
| delegate.warn(format, args); | ||
| } | ||
|
|
||
| @Override | ||
| public void warn(Supplier<String> msgSupplier) { | ||
| if (delegate.isWarnEnabled()) { | ||
| delegate.warn(msgSupplier.get()); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void error(String msg) { | ||
| delegate.error(msg); | ||
| } | ||
|
|
||
| @Override | ||
| public void error(String format, Object... args) { | ||
| delegate.error(format, args); | ||
| } | ||
|
|
||
| @Override | ||
| public void error(Supplier<String> msgSupplier) { | ||
| if (delegate.isErrorEnabled()) { | ||
| delegate.error(msgSupplier.get()); | ||
| } | ||
| } | ||
| } | ||
17 changes: 17 additions & 0 deletions
17
databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLoggerFactory.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package com.databricks.sdk.core.logging; | ||
|
|
||
| /** An {@link ILoggerFactory} backed by SLF4J. */ | ||
| public class Slf4jLoggerFactory implements ILoggerFactory { | ||
|
|
||
| public static final Slf4jLoggerFactory INSTANCE = new Slf4jLoggerFactory(); | ||
|
|
||
| @Override | ||
| public Logger getLogger(Class<?> type) { | ||
| return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(type)); | ||
| } | ||
|
|
||
| @Override | ||
| public Logger getLogger(String name) { | ||
| return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(name)); | ||
| } | ||
| } |
33 changes: 33 additions & 0 deletions
33
databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/LoggerFactoryTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.databricks.sdk.core.logging; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| public class LoggerFactoryTest { | ||
|
|
||
| @AfterEach | ||
| void resetFactory() { | ||
| LoggerFactory.setDefault(Slf4jLoggerFactory.INSTANCE); | ||
| } | ||
|
|
||
| @Test | ||
| void defaultFactoryIsSLF4J() { | ||
| Logger logger = LoggerFactory.getLogger(LoggerFactoryTest.class); | ||
| assertNotNull(logger); | ||
| logger.info("LoggerFactory defaultFactoryIsSLF4J test message"); | ||
| } | ||
|
|
||
| @Test | ||
| void setDefaultRejectsNull() { | ||
| assertThrows(IllegalArgumentException.class, () -> LoggerFactory.setDefault(null)); | ||
| } | ||
|
|
||
| @Test | ||
| void getLoggerByNameWorks() { | ||
| Logger logger = LoggerFactory.getLogger("com.example.Test"); | ||
| assertNotNull(logger); | ||
| logger.info("getLoggerByNameWorks test message"); | ||
| } | ||
| } |
119 changes: 119 additions & 0 deletions
119
databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/Slf4jLoggerTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| package com.databricks.sdk.core.logging; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.*; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.stream.Stream; | ||
| import org.apache.log4j.AppenderSkeleton; | ||
| import org.apache.log4j.spi.LoggingEvent; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.junit.jupiter.params.ParameterizedTest; | ||
| import org.junit.jupiter.params.provider.Arguments; | ||
| import org.junit.jupiter.params.provider.MethodSource; | ||
|
|
||
| public class Slf4jLoggerTest { | ||
|
|
||
| @Test | ||
| void getLoggerReturnsSlf4jLogger() { | ||
| Logger logger = LoggerFactory.getLogger(Slf4jLoggerTest.class); | ||
| assertNotNull(logger); | ||
| assertTrue(logger instanceof Slf4jLogger); | ||
| } | ||
|
|
||
| static Stream<Arguments> logCalls() { | ||
| RuntimeException ex = new RuntimeException("boom"); | ||
| return Stream.of( | ||
| Arguments.of("debug", "hello", null, "hello", null), | ||
| Arguments.of("info", "hello", null, "hello", null), | ||
| Arguments.of("warn", "hello", null, "hello", null), | ||
| Arguments.of("error", "hello", null, "hello", null), | ||
| Arguments.of( | ||
| "info", "user {} logged in", new Object[] {"alice"}, "user alice logged in", null), | ||
| Arguments.of("info", "a={}, b={}", new Object[] {1, 2}, "a=1, b=2", null), | ||
| Arguments.of("error", "failed: {}", new Object[] {"op", ex}, "failed: op", ex), | ||
| Arguments.of("error", "Error: {}", new Object[] {ex}, "Error: {}", ex), | ||
| Arguments.of("error", "Something broke", new Object[] {ex}, "Something broke", ex)); | ||
| } | ||
|
|
||
| @ParameterizedTest(name = "[{index}] {0}(\"{1}\")") | ||
| @MethodSource("logCalls") | ||
| void deliversCorrectOutput( | ||
| String level, String format, Object[] args, String expectedMsg, Throwable expectedThrown) { | ||
| CapturingAppender appender = new CapturingAppender(); | ||
| org.apache.log4j.Logger log4jLogger = org.apache.log4j.Logger.getLogger(Slf4jLoggerTest.class); | ||
| log4jLogger.addAppender(appender); | ||
| try { | ||
| Logger logger = new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(Slf4jLoggerTest.class)); | ||
| dispatch(logger, level, format, args); | ||
|
|
||
| assertEquals(1, appender.events.size(), "Expected exactly one log event"); | ||
| LoggingEvent event = appender.events.get(0); | ||
| assertEquals(expectedMsg, event.getRenderedMessage()); | ||
| assertEquals(toLog4jLevel(level), event.getLevel()); | ||
| if (expectedThrown != null) { | ||
| assertNotNull(event.getThrowableInformation(), "Expected throwable to be attached"); | ||
| assertSame(expectedThrown, event.getThrowableInformation().getThrowable()); | ||
| } else { | ||
| assertNull(event.getThrowableInformation(), "Expected no throwable"); | ||
| } | ||
| } finally { | ||
| log4jLogger.removeAppender(appender); | ||
| } | ||
| } | ||
|
|
||
| private static void dispatch(Logger logger, String level, String format, Object[] args) { | ||
| switch (level) { | ||
| case "debug": | ||
| if (args != null) logger.debug(format, args); | ||
| else logger.debug(format); | ||
| break; | ||
| case "info": | ||
| if (args != null) logger.info(format, args); | ||
| else logger.info(format); | ||
| break; | ||
| case "warn": | ||
| if (args != null) logger.warn(format, args); | ||
| else logger.warn(format); | ||
| break; | ||
| case "error": | ||
| if (args != null) logger.error(format, args); | ||
| else logger.error(format); | ||
| break; | ||
| default: | ||
| throw new IllegalArgumentException("Unknown level: " + level); | ||
| } | ||
| } | ||
|
|
||
| private static org.apache.log4j.Level toLog4jLevel(String level) { | ||
| switch (level) { | ||
| case "debug": | ||
| return org.apache.log4j.Level.DEBUG; | ||
| case "info": | ||
| return org.apache.log4j.Level.INFO; | ||
| case "warn": | ||
| return org.apache.log4j.Level.WARN; | ||
| case "error": | ||
| return org.apache.log4j.Level.ERROR; | ||
| default: | ||
| throw new IllegalArgumentException("Unknown level: " + level); | ||
| } | ||
| } | ||
|
|
||
| static class CapturingAppender extends AppenderSkeleton { | ||
| final List<LoggingEvent> events = new ArrayList<>(); | ||
|
|
||
| @Override | ||
| protected void append(LoggingEvent event) { | ||
| events.add(event); | ||
| } | ||
|
|
||
| @Override | ||
| public void close() {} | ||
|
|
||
| @Override | ||
| public boolean requiresLayout() { | ||
| return false; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.