Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### Documentation

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

### API Changes
* Add `createCatalog()`, `createSyncedTable()`, `deleteCatalog()`, `deleteSyncedTable()`, `getCatalog()` and `getSyncedTable()` methods for `workspaceClient.postgres()` service.
Expand Down
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 LoggerFactory} subclass 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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.databricks.sdk.core.logging;

import java.util.concurrent.atomic.AtomicReference;

/**
* Creates and configures {@link Logger} instances for the SDK.
*
* <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>Extend this class to provide a fully custom logging backend.
*/
public abstract class LoggerFactory {

private static final AtomicReference<LoggerFactory> defaultFactory = new AtomicReference<>();

/** Returns a logger for the given class, using the current default factory. */
public static Logger getLogger(Class<?> type) {
return getDefault().createLogger(type);
}

/** Returns a logger with the given name, using the current default factory. */
public static Logger getLogger(String name) {
return getDefault().createLogger(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(LoggerFactory factory) {
if (factory == null) {
throw new IllegalArgumentException("LoggerFactory must not be null");
}
defaultFactory.set(factory);
}

static LoggerFactory getDefault() {
LoggerFactory f = defaultFactory.get();
if (f != null) {
return f;
}
defaultFactory.compareAndSet(null, Slf4jLoggerFactory.INSTANCE);
return defaultFactory.get();
}

/**
* Creates a {@link Logger} for the given class. Subclasses obtain the backend logger (e.g. SLF4J)
* and return an adapter.
*/
protected abstract Logger createLogger(Class<?> type);

/**
* Creates a {@link Logger} for the given name. Subclasses obtain the backend logger and return an
* adapter.
*/
protected abstract Logger createLogger(String name);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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) {
if (delegate.isDebugEnabled()) {
delegate.debug(msg);
}
}

@Override
public void debug(String format, Object... args) {
if (delegate.isDebugEnabled()) {
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());
}
Comment thread
mihaimitrea-db marked this conversation as resolved.
}

@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());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.databricks.sdk.core.logging;

/** A {@link LoggerFactory} backed by SLF4J. */
public class Slf4jLoggerFactory extends LoggerFactory {

public static final Slf4jLoggerFactory INSTANCE = new Slf4jLoggerFactory();

@Override
protected Logger createLogger(Class<?> type) {
return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(type));
}

@Override
protected Logger createLogger(String name) {
return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(name));
}
}
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");
}
}
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;
}
}
}
Loading