diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md
index 0bd9e224b..35d6284fc 100755
--- a/NEXT_CHANGELOG.md
+++ b/NEXT_CHANGELOG.md
@@ -13,6 +13,9 @@
### Documentation
### Internal Changes
+* Introduced a logging abstraction (`com.databricks.sdk.core.logging`) to decouple the SDK from a specific logging backend.
+* Added `java.util.logging` as a supported alternative logging backend. Activate it with `LoggerFactory.setDefault(JulLoggerFactory.INSTANCE)`.
+* Migrated internal SDK classes to the logging abstraction. The SDK now supports SLF4J, `java.util.logging`, or a custom backend via `LoggerFactory.setDefault()`.
### API Changes
* Add `createCatalog()`, `createSyncedTable()`, `deleteCatalog()`, `deleteSyncedTable()`, `getCatalog()` and `getSyncedTable()` methods for `workspaceClient.postgres()` service.
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ApiClient.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ApiClient.java
index 6507d1eb6..3cbd0d61a 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ApiClient.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ApiClient.java
@@ -6,6 +6,8 @@
import com.databricks.sdk.core.http.Request;
import com.databricks.sdk.core.http.RequestOptions;
import com.databricks.sdk.core.http.Response;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.retry.NoRetryStrategyPicker;
import com.databricks.sdk.core.retry.RequestBasedRetryStrategyPicker;
import com.databricks.sdk.core.retry.RetryStrategy;
@@ -25,8 +27,6 @@
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Simplified REST API client with retries, JSON POJO SerDe through Jackson and exception POJO
@@ -253,9 +253,8 @@ private Response executeInner(Request in, String path, RequestOptions options) {
try {
response = httpClient.execute(in);
- if (LOG.isDebugEnabled()) {
- LOG.debug(makeLogRecord(in, response));
- }
+ Response resp = response;
+ LOG.debug(() -> makeLogRecord(in, resp));
if (isResponseSuccessful(response)) {
return response; // stop here if the request succeeded
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/AzureCliCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/AzureCliCredentialsProvider.java
index 7e67ca72e..1c97a7ac9 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/AzureCliCredentialsProvider.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/AzureCliCredentialsProvider.java
@@ -1,5 +1,7 @@
package com.databricks.sdk.core;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.oauth.CachedTokenSource;
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
import com.databricks.sdk.core.oauth.Token;
@@ -7,8 +9,6 @@
import com.databricks.sdk.support.InternalApi;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@InternalApi
public class AzureCliCredentialsProvider implements CredentialsProvider {
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
index 58aaf7655..7855b73c7 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/CliTokenSource.java
@@ -1,5 +1,7 @@
package com.databricks.sdk.core;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.oauth.Token;
import com.databricks.sdk.core.oauth.TokenSource;
import com.databricks.sdk.core.utils.Environment;
@@ -18,8 +20,6 @@
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.IOUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@InternalApi
public class CliTokenSource implements TokenSource {
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ConfigLoader.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ConfigLoader.java
index ae531ffc0..d7fb9af6c 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ConfigLoader.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/ConfigLoader.java
@@ -1,5 +1,7 @@
package com.databricks.sdk.core;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.utils.Environment;
import com.databricks.sdk.support.InternalApi;
import java.io.FileNotFoundException;
@@ -13,8 +15,6 @@
import org.apache.commons.configuration2.INIConfiguration;
import org.apache.commons.configuration2.SubnodeConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@InternalApi
public class ConfigLoader {
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java
index c7ee7b9f5..ae401280d 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksCliCredentialsProvider.java
@@ -1,5 +1,7 @@
package com.databricks.sdk.core;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.oauth.CachedTokenSource;
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
import com.databricks.sdk.core.oauth.Token;
@@ -10,8 +12,6 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.util.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@InternalApi
public class DatabricksCliCredentialsProvider implements CredentialsProvider {
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java
index 8cf3d9597..7d002ca6c 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksConfig.java
@@ -4,6 +4,8 @@
import com.databricks.sdk.core.http.HttpClient;
import com.databricks.sdk.core.http.Request;
import com.databricks.sdk.core.http.Response;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.oauth.ErrorTokenSource;
import com.databricks.sdk.core.oauth.HostMetadata;
import com.databricks.sdk.core.oauth.OAuthHeaderFactory;
@@ -19,8 +21,6 @@
import java.time.Duration;
import java.util.*;
import org.apache.http.HttpMessage;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class DatabricksConfig {
private static final Logger LOG = LoggerFactory.getLogger(DatabricksConfig.class);
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java
index 99716890f..e405a8eb9 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/DefaultCredentialsProvider.java
@@ -1,12 +1,12 @@
package com.databricks.sdk.core;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.oauth.*;
import com.databricks.sdk.support.InternalApi;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* The DefaultCredentialsProvider is the primary authentication handler for the Databricks SDK. It
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleCredentialsCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleCredentialsCredentialsProvider.java
index 624371b29..1852355f2 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleCredentialsCredentialsProvider.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleCredentialsCredentialsProvider.java
@@ -3,6 +3,8 @@
import static com.databricks.sdk.core.utils.GoogleUtils.GCP_SCOPES;
import static com.databricks.sdk.core.utils.GoogleUtils.SA_ACCESS_TOKEN_HEADER;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import com.google.auth.oauth2.*;
import com.google.auth.oauth2.IdTokenProvider.Option;
@@ -12,8 +14,6 @@
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@InternalApi
public class GoogleCredentialsCredentialsProvider implements CredentialsProvider {
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleIdCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleIdCredentialsProvider.java
index 57765541c..675f95b69 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleIdCredentialsProvider.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/GoogleIdCredentialsProvider.java
@@ -3,6 +3,8 @@
import static com.databricks.sdk.core.utils.GoogleUtils.GCP_SCOPES;
import static com.databricks.sdk.core.utils.GoogleUtils.SA_ACCESS_TOKEN_HEADER;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
@@ -10,8 +12,6 @@
import com.google.auth.oauth2.ImpersonatedCredentials;
import java.io.IOException;
import java.util.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@InternalApi
public class GoogleIdCredentialsProvider implements CredentialsProvider {
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/NotebookNativeCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/NotebookNativeCredentialsProvider.java
index ef6571e61..ec3259ae3 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/NotebookNativeCredentialsProvider.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/NotebookNativeCredentialsProvider.java
@@ -1,11 +1,11 @@
package com.databricks.sdk.core;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* A CredentialsProvider that uses the API token from the command context to authenticate.
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/commons/CommonsHttpClient.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/commons/CommonsHttpClient.java
index b1e9ca61a..6183ff5a6 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/commons/CommonsHttpClient.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/commons/CommonsHttpClient.java
@@ -8,6 +8,8 @@
import com.databricks.sdk.core.http.HttpClient;
import com.databricks.sdk.core.http.Request;
import com.databricks.sdk.core.http.Response;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.utils.CustomCloseInputStream;
import com.databricks.sdk.core.utils.ProxyUtils;
import com.databricks.sdk.support.InternalApi;
@@ -35,8 +37,6 @@
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@InternalApi
public class CommonsHttpClient implements HttpClient {
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/AbstractErrorMapper.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/AbstractErrorMapper.java
index cf0c81853..5db56052b 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/AbstractErrorMapper.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/AbstractErrorMapper.java
@@ -3,11 +3,11 @@
import com.databricks.sdk.core.DatabricksError;
import com.databricks.sdk.core.error.details.ErrorDetails;
import com.databricks.sdk.core.http.Response;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import java.util.HashMap;
import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@InternalApi
abstract class AbstractErrorMapper {
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/ILoggerFactory.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/ILoggerFactory.java
new file mode 100644
index 000000000..54ed0136c
--- /dev/null
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/ILoggerFactory.java
@@ -0,0 +1,16 @@
+package com.databricks.sdk.core.logging;
+
+/**
+ * Provides {@link Logger} instances for a specific logging backend.
+ *
+ *
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);
+}
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/JulLogger.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/JulLogger.java
new file mode 100644
index 000000000..bfc814b2a
--- /dev/null
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/JulLogger.java
@@ -0,0 +1,208 @@
+package com.databricks.sdk.core.logging;
+
+import java.util.Arrays;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+
+/** Delegates logging calls to a {@code java.util.logging.Logger}, translating SLF4J conventions. */
+class JulLogger extends Logger {
+
+ private static final String LOGGING_PACKAGE = "com.databricks.sdk.core.logging.";
+
+ private final java.util.logging.Logger delegate;
+
+ JulLogger(java.util.logging.Logger delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void debug(String msg) {
+ log(Level.FINE, msg, null);
+ }
+
+ @Override
+ public void debug(String format, Object... args) {
+ log(Level.FINE, format, args);
+ }
+
+ @Override
+ public void debug(Supplier msgSupplier) {
+ if (delegate.isLoggable(Level.FINE)) {
+ log(Level.FINE, msgSupplier.get(), null);
+ }
+ }
+
+ @Override
+ public void info(String msg) {
+ log(Level.INFO, msg, null);
+ }
+
+ @Override
+ public void info(String format, Object... args) {
+ log(Level.INFO, format, args);
+ }
+
+ @Override
+ public void info(Supplier msgSupplier) {
+ if (delegate.isLoggable(Level.INFO)) {
+ log(Level.INFO, msgSupplier.get(), null);
+ }
+ }
+
+ @Override
+ public void warn(String msg) {
+ log(Level.WARNING, msg, null);
+ }
+
+ @Override
+ public void warn(String format, Object... args) {
+ log(Level.WARNING, format, args);
+ }
+
+ @Override
+ public void warn(Supplier msgSupplier) {
+ if (delegate.isLoggable(Level.WARNING)) {
+ log(Level.WARNING, msgSupplier.get(), null);
+ }
+ }
+
+ @Override
+ public void error(String msg) {
+ log(Level.SEVERE, msg, null);
+ }
+
+ @Override
+ public void error(String format, Object... args) {
+ log(Level.SEVERE, format, args);
+ }
+
+ @Override
+ public void error(Supplier msgSupplier) {
+ if (delegate.isLoggable(Level.SEVERE)) {
+ log(Level.SEVERE, msgSupplier.get(), null);
+ }
+ }
+
+ private void log(Level level, String format, Object[] args) {
+ if (!delegate.isLoggable(level)) {
+ return;
+ }
+ Throwable thrown = (args != null) ? extractThrowable(format, args) : null;
+ String message = (args != null) ? formatMessage(format, args) : format;
+ LogRecord record = new LogRecord(level, message);
+ record.setLoggerName(delegate.getName());
+ if (thrown != null) {
+ record.setThrown(thrown);
+ }
+ inferCaller(record);
+ delegate.log(record);
+ }
+
+ /**
+ * Sets the source class and method on a {@link LogRecord} by walking the call stack to find the
+ * first frame outside this logging package.
+ *
+ * JUL normally infers caller information automatically by scanning the stack for the first
+ * frame after its own {@code java.util.logging.Logger} methods. Because {@code JulLogger} wraps
+ * the JUL logger, that automatic inference stops at {@code JulLogger} or its helper methods
+ * instead of reaching the actual SDK class that initiated the log call. Without this correction,
+ * every log record would be attributed to {@code JulLogger}, making JUL output useless for
+ * identifying the real call site.
+ */
+ private static void inferCaller(LogRecord record) {
+ StackTraceElement[] stack = new Throwable().getStackTrace();
+ for (StackTraceElement frame : stack) {
+ if (!frame.getClassName().startsWith(LOGGING_PACKAGE)) {
+ record.setSourceClassName(frame.getClassName());
+ record.setSourceMethodName(frame.getMethodName());
+ return;
+ }
+ }
+ }
+
+ /**
+ * Replaces SLF4J-style {@code {}} placeholders with argument values, matching the semantics of
+ * SLF4J's {@code MessageFormatter.arrayFormat}:
+ *
+ *
+ * - A trailing {@link Throwable} is unconditionally excluded from formatting.
+ *
- A backslash before {@code {}} escapes it as a literal {@code {}}.
+ *
- Array arguments are rendered with {@link Arrays#deepToString}.
+ *
- A {@code null} format string returns {@code null}.
+ *
+ */
+ static String formatMessage(String format, Object[] args) {
+ if (format == null) {
+ return null;
+ }
+ if (args == null || args.length == 0) {
+ return format;
+ }
+ int usableArgs = args.length;
+ if (args[usableArgs - 1] instanceof Throwable) {
+ usableArgs--;
+ }
+ StringBuilder sb = new StringBuilder(format.length() + 32);
+ int argIdx = 0;
+ int i = 0;
+ while (i < format.length()) {
+ if (i + 1 < format.length() && format.charAt(i) == '{' && format.charAt(i + 1) == '}') {
+ if (i > 0 && format.charAt(i - 1) == '\\') {
+ sb.setLength(sb.length() - 1);
+ sb.append("{}");
+ } else if (argIdx < usableArgs) {
+ sb.append(renderArg(args[argIdx++]));
+ } else {
+ sb.append("{}");
+ }
+ i += 2;
+ } else {
+ sb.append(format.charAt(i));
+ i++;
+ }
+ }
+ return sb.toString();
+ }
+
+ private static String renderArg(Object arg) {
+ if (arg == null) {
+ return "null";
+ }
+ if (arg instanceof Object[]) {
+ return Arrays.deepToString((Object[]) arg);
+ }
+ if (arg.getClass().isArray()) {
+ return primitiveArrayToString(arg);
+ }
+ return arg.toString();
+ }
+
+ private static String primitiveArrayToString(Object array) {
+ if (array instanceof boolean[]) return Arrays.toString((boolean[]) array);
+ if (array instanceof byte[]) return Arrays.toString((byte[]) array);
+ if (array instanceof char[]) return Arrays.toString((char[]) array);
+ if (array instanceof short[]) return Arrays.toString((short[]) array);
+ if (array instanceof int[]) return Arrays.toString((int[]) array);
+ if (array instanceof long[]) return Arrays.toString((long[]) array);
+ if (array instanceof float[]) return Arrays.toString((float[]) array);
+ if (array instanceof double[]) return Arrays.toString((double[]) array);
+ return Arrays.deepToString(new Object[] {array});
+ }
+
+ /**
+ * Returns the last argument if it is a {@link Throwable}, unconditionally. This matches SLF4J's
+ * {@code NormalizedParameters.getThrowableCandidate}, which always extracts a trailing Throwable
+ * regardless of how many {@code {}} placeholders the format string contains.
+ */
+ static Throwable extractThrowable(String format, Object[] args) {
+ if (args == null || args.length == 0) {
+ return null;
+ }
+ Object last = args[args.length - 1];
+ if (last instanceof Throwable) {
+ return (Throwable) last;
+ }
+ return null;
+ }
+}
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/JulLoggerFactory.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/JulLoggerFactory.java
new file mode 100644
index 000000000..45c19eef3
--- /dev/null
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/JulLoggerFactory.java
@@ -0,0 +1,25 @@
+package com.databricks.sdk.core.logging;
+
+/**
+ * An {@link ILoggerFactory} backed by {@code java.util.logging}. Always available on any JRE.
+ *
+ * Use this when SLF4J is not desirable:
+ *
+ *
{@code
+ * LoggerFactory.setDefault(JulLoggerFactory.INSTANCE);
+ * }
+ */
+public class JulLoggerFactory implements ILoggerFactory {
+
+ public static final JulLoggerFactory INSTANCE = new JulLoggerFactory();
+
+ @Override
+ public Logger getLogger(Class> type) {
+ return new JulLogger(java.util.logging.Logger.getLogger(type.getName()));
+ }
+
+ @Override
+ public Logger getLogger(String name) {
+ return new JulLogger(java.util.logging.Logger.getLogger(name));
+ }
+}
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Logger.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Logger.java
new file mode 100644
index 000000000..d91d14d84
--- /dev/null
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Logger.java
@@ -0,0 +1,36 @@
+package com.databricks.sdk.core.logging;
+
+import java.util.function.Supplier;
+
+/**
+ * Logging contract used throughout the SDK.
+ *
+ * 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 msgSupplier);
+
+ public abstract void info(String msg);
+
+ public abstract void info(String format, Object... args);
+
+ public abstract void info(Supplier msgSupplier);
+
+ public abstract void warn(String msg);
+
+ public abstract void warn(String format, Object... args);
+
+ public abstract void warn(Supplier msgSupplier);
+
+ public abstract void error(String msg);
+
+ public abstract void error(String format, Object... args);
+
+ public abstract void error(Supplier msgSupplier);
+}
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/LoggerFactory.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/LoggerFactory.java
new file mode 100644
index 000000000..4f59aa28c
--- /dev/null
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/LoggerFactory.java
@@ -0,0 +1,55 @@
+package com.databricks.sdk.core.logging;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Static entry point for obtaining {@link Logger} instances.
+ *
+ * By default, logging goes through SLF4J. Users can override the backend programmatically before
+ * creating any SDK client:
+ *
+ *
{@code
+ * LoggerFactory.setDefault(JulLoggerFactory.INSTANCE);
+ * WorkspaceClient ws = new WorkspaceClient();
+ * }
+ *
+ * Implement {@link ILoggerFactory} to provide a fully custom logging backend.
+ */
+public final class LoggerFactory {
+
+ private static final AtomicReference 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.
+ *
+ * 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();
+ }
+}
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLogger.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLogger.java
new file mode 100644
index 000000000..351fdfc0d
--- /dev/null
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLogger.java
@@ -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 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 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 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 msgSupplier) {
+ if (delegate.isErrorEnabled()) {
+ delegate.error(msgSupplier.get());
+ }
+ }
+}
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLoggerFactory.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLoggerFactory.java
new file mode 100644
index 000000000..7c7323df0
--- /dev/null
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLoggerFactory.java
@@ -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));
+ }
+}
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureServicePrincipalCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureServicePrincipalCredentialsProvider.java
index eca52808d..3ee6679b4 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureServicePrincipalCredentialsProvider.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/AzureServicePrincipalCredentialsProvider.java
@@ -1,13 +1,13 @@
package com.databricks.sdk.core.oauth;
import com.databricks.sdk.core.*;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.utils.AzureUtils;
import com.databricks.sdk.support.InternalApi;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Adds refreshed Azure Active Directory (AAD) Service Principal OAuth tokens to every request,
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/CachedTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/CachedTokenSource.java
index 6f3d63ec5..1b676e758 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/CachedTokenSource.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/CachedTokenSource.java
@@ -1,13 +1,13 @@
package com.databricks.sdk.core.oauth;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.utils.ClockSupplier;
import com.databricks.sdk.core.utils.UtcClockSupplier;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* An OAuth TokenSource which can be refreshed.
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Consent.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Consent.java
index 19619d127..39951f2bc 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Consent.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/Consent.java
@@ -3,6 +3,8 @@
import com.databricks.sdk.core.DatabricksException;
import com.databricks.sdk.core.commons.CommonsHttpClient;
import com.databricks.sdk.core.http.HttpClient;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
@@ -21,8 +23,6 @@
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.io.IOUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Consent provides a mechanism to retrieve an authorization code and exchange it for an OAuth token
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSource.java
index 026197e12..8613515b0 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSource.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/DatabricksOAuthTokenSource.java
@@ -2,6 +2,8 @@
import com.databricks.sdk.core.DatabricksException;
import com.databricks.sdk.core.http.HttpClient;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.google.common.base.Strings;
import java.time.Instant;
import java.util.Arrays;
@@ -9,8 +11,6 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Implementation of TokenSource that handles OAuth token exchange for Databricks authentication.
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/EndpointTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/EndpointTokenSource.java
index afc40024e..bd95dd598 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/EndpointTokenSource.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/EndpointTokenSource.java
@@ -2,13 +2,13 @@
import com.databricks.sdk.core.DatabricksException;
import com.databricks.sdk.core.http.HttpClient;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Represents a token source that exchanges a control plane token for an endpoint-specific dataplane
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/ExternalBrowserCredentialsProvider.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/ExternalBrowserCredentialsProvider.java
index 1d8b48dac..7c8d0fe4a 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/ExternalBrowserCredentialsProvider.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/ExternalBrowserCredentialsProvider.java
@@ -3,6 +3,8 @@
import com.databricks.sdk.core.CredentialsProvider;
import com.databricks.sdk.core.DatabricksConfig;
import com.databricks.sdk.core.DatabricksException;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import java.io.IOException;
import java.nio.file.Path;
@@ -12,8 +14,6 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* A {@code CredentialsProvider} which implements the Authorization Code + PKCE flow by opening a
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/FileTokenCache.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/FileTokenCache.java
index b1805c131..83554b3d0 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/FileTokenCache.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/FileTokenCache.java
@@ -1,5 +1,7 @@
package com.databricks.sdk.core.oauth;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.core.utils.SerDeUtils;
import com.databricks.sdk.support.InternalApi;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -8,8 +10,6 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/** A TokenCache implementation that stores tokens as plain files. */
@InternalApi
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentials.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentials.java
index 34a1878d1..645c69239 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentials.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentials.java
@@ -3,11 +3,11 @@
import com.databricks.sdk.core.CredentialsProvider;
import com.databricks.sdk.core.DatabricksConfig;
import com.databricks.sdk.core.http.HttpClient;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import java.io.Serializable;
import java.util.Optional;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* An implementation of RefreshableTokenSource implementing the refresh_token OAuth grant type.
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentialsTokenSource.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentialsTokenSource.java
index 3c42e0490..419d030ec 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentialsTokenSource.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/SessionCredentialsTokenSource.java
@@ -2,12 +2,12 @@
import com.databricks.sdk.core.DatabricksException;
import com.databricks.sdk.core.http.HttpClient;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* TokenSource that handles OAuth token refresh for SessionCredentials.
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/TokenEndpointClient.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/TokenEndpointClient.java
index 8931ec162..0ce38703e 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/TokenEndpointClient.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/oauth/TokenEndpointClient.java
@@ -6,6 +6,8 @@
import com.databricks.sdk.core.http.HttpClient;
import com.databricks.sdk.core.http.Request;
import com.databricks.sdk.core.http.Response;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
@@ -14,8 +16,6 @@
import java.util.Map;
import java.util.Objects;
import org.apache.http.HttpHeaders;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Client for interacting with an OAuth token endpoint.
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/retry/NonIdempotentRequestRetryStrategy.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/retry/NonIdempotentRequestRetryStrategy.java
index db4740700..8f91bf650 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/retry/NonIdempotentRequestRetryStrategy.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/retry/NonIdempotentRequestRetryStrategy.java
@@ -1,13 +1,13 @@
package com.databricks.sdk.core.retry;
import com.databricks.sdk.core.DatabricksError;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import java.net.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* This class is used to determine if a non-idempotent request should be retried. We essentially
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/OSUtils.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/OSUtils.java
index 66da25cfa..3b2574557 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/OSUtils.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/utils/OSUtils.java
@@ -1,14 +1,14 @@
package com.databricks.sdk.core.utils;
import com.databricks.sdk.core.DatabricksException;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.support.InternalApi;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* OSUtils is an interface that provides utility methods for determining the current operating
diff --git a/databricks-sdk-java/src/main/java/com/databricks/sdk/mixin/ClustersExt.java b/databricks-sdk-java/src/main/java/com/databricks/sdk/mixin/ClustersExt.java
index dc86da730..4ee8ffea6 100644
--- a/databricks-sdk-java/src/main/java/com/databricks/sdk/mixin/ClustersExt.java
+++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/mixin/ClustersExt.java
@@ -4,6 +4,8 @@
import com.databricks.sdk.core.ApiClient;
import com.databricks.sdk.core.DatabricksError;
+import com.databricks.sdk.core.logging.Logger;
+import com.databricks.sdk.core.logging.LoggerFactory;
import com.databricks.sdk.service.compute.*;
import java.time.Duration;
import java.util.ArrayList;
@@ -13,8 +15,6 @@
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.stream.Collectors;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class ClustersExt extends ClustersAPI {
private static final Logger LOG = LoggerFactory.getLogger(ClustersExt.class);
diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/JulLoggerTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/JulLoggerTest.java
new file mode 100644
index 000000000..566b4b6fe
--- /dev/null
+++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/JulLoggerTest.java
@@ -0,0 +1,207 @@
+package com.databricks.sdk.core.logging;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.stream.Stream;
+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 JulLoggerTest {
+
+ // ---- Formatter unit tests ----
+
+ @Test
+ void formatMessageNoPlaceholders() {
+ assertEquals("hello world", JulLogger.formatMessage("hello world", new Object[] {}));
+ }
+
+ @Test
+ void formatMessageNullArgs() {
+ assertEquals("hello", JulLogger.formatMessage("hello", (Object[]) null));
+ }
+
+ @Test
+ void formatMessageSinglePlaceholder() {
+ assertEquals("hello world", JulLogger.formatMessage("hello {}", new Object[] {"world"}));
+ }
+
+ @Test
+ void formatMessageMultiplePlaceholders() {
+ assertEquals("a=1, b=2", JulLogger.formatMessage("a={}, b={}", new Object[] {"1", "2"}));
+ }
+
+ @Test
+ void formatMessageTrailingThrowableExcluded() {
+ Exception ex = new RuntimeException("boom");
+ String result = JulLogger.formatMessage("Failed at {}: {}", new Object[] {"host", "msg", ex});
+ assertEquals("Failed at host: msg", result);
+ }
+
+ @Test
+ void formatMessageThrowableIsAlwaysExcluded() {
+ Exception ex = new RuntimeException("boom");
+ String result = JulLogger.formatMessage("Error: {}", new Object[] {ex});
+ assertEquals("Error: {}", result);
+ }
+
+ @Test
+ void extractThrowableWhenTrailing() {
+ Exception ex = new RuntimeException("boom");
+ Throwable result = JulLogger.extractThrowable("Failed: {}", new Object[] {"msg", ex});
+ assertSame(ex, result);
+ }
+
+ @Test
+ void extractThrowableNullWhenNotTrailing() {
+ assertNull(JulLogger.extractThrowable("a={}, b={}", new Object[] {"1", "2"}));
+ }
+
+ @Test
+ void extractThrowableAlwaysWhenLastArgIsThrowable() {
+ Exception ex = new RuntimeException("boom");
+ assertSame(ex, JulLogger.extractThrowable("Error: {}", new Object[] {ex}));
+ }
+
+ @Test
+ void extractThrowableNullArgs() {
+ assertNull(JulLogger.extractThrowable("msg", (Object[]) null));
+ }
+
+ @Test
+ void extractThrowableEmptyArgs() {
+ assertNull(JulLogger.extractThrowable("msg", new Object[] {}));
+ }
+
+ // ---- End-to-end capturing tests ----
+
+ static Stream 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) {
+ java.util.logging.Logger julLogger =
+ java.util.logging.Logger.getLogger(JulLoggerTest.class.getName());
+ Level originalLevel = julLogger.getLevel();
+ julLogger.setLevel(Level.ALL);
+ CapturingHandler handler = new CapturingHandler();
+ julLogger.addHandler(handler);
+ try {
+ Logger logger =
+ new JulLogger(java.util.logging.Logger.getLogger(JulLoggerTest.class.getName()));
+ dispatch(logger, level, format, args);
+
+ assertEquals(1, handler.records.size(), "Expected exactly one log record");
+ LogRecord record = handler.records.get(0);
+ assertEquals(expectedMsg, record.getMessage());
+ assertEquals(toJulLevel(level), record.getLevel());
+ if (expectedThrown != null) {
+ assertSame(expectedThrown, record.getThrown());
+ } else {
+ assertNull(record.getThrown(), "Expected no throwable");
+ }
+ } finally {
+ julLogger.removeHandler(handler);
+ julLogger.setLevel(originalLevel);
+ }
+ }
+
+ @Test
+ void callerInferenceSkipsLoggingPackage() {
+ java.util.logging.Logger julLogger =
+ java.util.logging.Logger.getLogger(JulLoggerTest.class.getName());
+ Level originalLevel = julLogger.getLevel();
+ julLogger.setLevel(Level.ALL);
+ CapturingHandler handler = new CapturingHandler();
+ julLogger.addHandler(handler);
+ try {
+ Logger logger =
+ new JulLogger(java.util.logging.Logger.getLogger(JulLoggerTest.class.getName()));
+ logger.info("test");
+
+ assertEquals(1, handler.records.size());
+ String sourceClass = handler.records.get(0).getSourceClassName();
+ assertFalse(
+ sourceClass.startsWith("com.databricks.sdk.core.logging."),
+ "Source class should not be in the logging package, but was: " + sourceClass);
+ } finally {
+ julLogger.removeHandler(handler);
+ julLogger.setLevel(originalLevel);
+ }
+ }
+
+ // ---- Helpers ----
+
+ 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 Level toJulLevel(String level) {
+ switch (level) {
+ case "debug":
+ return Level.FINE;
+ case "info":
+ return Level.INFO;
+ case "warn":
+ return Level.WARNING;
+ case "error":
+ return Level.SEVERE;
+ default:
+ throw new IllegalArgumentException("Unknown level: " + level);
+ }
+ }
+
+ static class CapturingHandler extends Handler {
+ final List records = new ArrayList<>();
+
+ @Override
+ public void publish(LogRecord record) {
+ records.add(record);
+ }
+
+ @Override
+ public void flush() {}
+
+ @Override
+ public void close() {}
+ }
+}
diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/LoggerFactoryTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/LoggerFactoryTest.java
new file mode 100644
index 000000000..652c26d7e
--- /dev/null
+++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/LoggerFactoryTest.java
@@ -0,0 +1,49 @@
+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 setDefaultSwitchesToJul() {
+ LoggerFactory.setDefault(JulLoggerFactory.INSTANCE);
+ Logger logger = LoggerFactory.getLogger(LoggerFactoryTest.class);
+ assertNotNull(logger);
+ logger.info("setDefaultSwitchesToJul test message via JUL");
+ }
+
+ @Test
+ void getLoggerByNameWorks() {
+ Logger logger = LoggerFactory.getLogger("com.example.Test");
+ assertNotNull(logger);
+ logger.info("getLoggerByNameWorks test message");
+ }
+
+ @Test
+ void getLoggerByNameWorksWithJul() {
+ LoggerFactory.setDefault(JulLoggerFactory.INSTANCE);
+ Logger logger = LoggerFactory.getLogger("com.example.Test");
+ assertNotNull(logger);
+ logger.info("getLoggerByNameWorksWithJul test message");
+ }
+}
diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/LoggingParityTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/LoggingParityTest.java
new file mode 100644
index 000000000..e57e2051b
--- /dev/null
+++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/LoggingParityTest.java
@@ -0,0 +1,88 @@
+package com.databricks.sdk.core.logging;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import org.junit.jupiter.api.Test;
+import org.slf4j.helpers.FormattingTuple;
+import org.slf4j.helpers.MessageFormatter;
+
+/**
+ * Verifies that JulLogger's placeholder formatting and Throwable extraction produce the same
+ * results as SLF4J's {@link MessageFormatter#arrayFormat}, so the two backends behave identically
+ * for any given Logger call.
+ */
+public class LoggingParityTest {
+
+ @Test
+ void singleThrowableArgIsExtractedNotFormatted() {
+ Exception ex = new RuntimeException("boom");
+ Object[] args = {ex};
+ FormattingTuple slf4j = MessageFormatter.arrayFormat("Error: {}", args);
+
+ assertEquals(slf4j.getMessage(), JulLogger.formatMessage("Error: {}", args));
+ assertEquals(slf4j.getThrowable(), JulLogger.extractThrowable("Error: {}", args));
+ }
+
+ @Test
+ void trailingThrowableBeyondPlaceholders() {
+ Exception ex = new RuntimeException("boom");
+ Object[] args = {"op", ex};
+ FormattingTuple slf4j = MessageFormatter.arrayFormat("Error: {} failed", args);
+
+ assertEquals(slf4j.getMessage(), JulLogger.formatMessage("Error: {} failed", args));
+ assertEquals(slf4j.getThrowable(), JulLogger.extractThrowable("Error: {} failed", args));
+ }
+
+ @Test
+ void noPlaceholdersTrailingThrowable() {
+ Exception ex = new RuntimeException("boom");
+ Object[] args = {ex};
+ FormattingTuple slf4j = MessageFormatter.arrayFormat("Something broke", args);
+
+ assertEquals(slf4j.getMessage(), JulLogger.formatMessage("Something broke", args));
+ assertEquals(slf4j.getThrowable(), JulLogger.extractThrowable("Something broke", args));
+ }
+
+ @Test
+ void nonThrowableArgsNoExtraction() {
+ Object[] args = {"a", "b"};
+ FormattingTuple slf4j = MessageFormatter.arrayFormat("{} {}", args);
+
+ assertEquals(slf4j.getMessage(), JulLogger.formatMessage("{} {}", args));
+ assertEquals(slf4j.getThrowable(), JulLogger.extractThrowable("{} {}", args));
+ }
+
+ @Test
+ void multipleArgsWithTrailingThrowable() {
+ Exception ex = new RuntimeException("boom");
+ Object[] args = {"host", 8080, ex};
+ FormattingTuple slf4j = MessageFormatter.arrayFormat("Connect to {}:{}", args);
+
+ assertEquals(slf4j.getMessage(), JulLogger.formatMessage("Connect to {}:{}", args));
+ assertEquals(slf4j.getThrowable(), JulLogger.extractThrowable("Connect to {}:{}", args));
+ }
+
+ @Test
+ void arrayArgumentRenderedLikeSlf4j() {
+ Object[] args = {new String[] {"a", "b"}};
+ FormattingTuple slf4j = MessageFormatter.arrayFormat("arr {}", args);
+
+ assertEquals(slf4j.getMessage(), JulLogger.formatMessage("arr {}", args));
+ }
+
+ @Test
+ void escapedPlaceholderRenderedLikeSlf4j() {
+ Object[] args = {"x"};
+ FormattingTuple slf4j = MessageFormatter.arrayFormat("escaped \\{} {}", args);
+
+ assertEquals(slf4j.getMessage(), JulLogger.formatMessage("escaped \\{} {}", args));
+ }
+
+ @Test
+ void nullFormatRenderedLikeSlf4j() {
+ Object[] args = {"x"};
+ FormattingTuple slf4j = MessageFormatter.arrayFormat(null, args);
+
+ assertEquals(slf4j.getMessage(), JulLogger.formatMessage(null, args));
+ }
+}
diff --git a/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/Slf4jLoggerTest.java b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/Slf4jLoggerTest.java
new file mode 100644
index 000000000..54582b184
--- /dev/null
+++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/Slf4jLoggerTest.java
@@ -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 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 events = new ArrayList<>();
+
+ @Override
+ protected void append(LoggingEvent event) {
+ events.add(event);
+ }
+
+ @Override
+ public void close() {}
+
+ @Override
+ public boolean requiresLayout() {
+ return false;
+ }
+ }
+}