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..244b4bba4 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 @@ -25,8 +25,8 @@ import java.time.format.DateTimeFormatter; import java.util.*; import java.util.function.Function; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.LoggerFactory; /** * Simplified REST API client with retries, JSON POJO SerDe through Jackson and exception POJO 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..014c55dc6 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 @@ -7,8 +7,8 @@ import com.databricks.sdk.support.InternalApi; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..d7cfa06c2 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 @@ -18,8 +18,8 @@ import java.util.Arrays; import java.util.List; import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..f1a830c94 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 @@ -13,8 +13,8 @@ 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; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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 b312e1a3e..60050c7fc 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 @@ -10,8 +10,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.charset.StandardCharsets; import java.util.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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 b2b0d75e4..82bcae379 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 @@ -19,8 +19,8 @@ import java.time.Duration; import java.util.*; import org.apache.http.HttpMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..10fcc2173 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 @@ -5,8 +5,8 @@ import com.google.common.base.Strings; import java.util.ArrayList; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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 463d2bab9..92bb4c01e 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 @@ -12,8 +12,8 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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 376d691c5..52f9a73a2 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 @@ -10,8 +10,8 @@ import com.google.auth.oauth2.ImpersonatedCredentials; import java.io.IOException; import java.util.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..10966f4d7 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 @@ -4,8 +4,8 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..9bf8233ad 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 @@ -35,8 +35,8 @@ 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; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..eb81aba20 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 @@ -6,8 +6,8 @@ import com.databricks.sdk.support.InternalApi; import java.util.HashMap; import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.LoggerFactory; @InternalApi abstract class AbstractErrorMapper { 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..c31fafc99 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/JulLogger.java @@ -0,0 +1,193 @@ +package com.databricks.sdk.core.logging; + +import java.util.Arrays; +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; + + private JulLogger(java.util.logging.Logger delegate) { + this.delegate = delegate; + } + + static Logger create(Class type) { + return create(type.getName()); + } + + static Logger create(String name) { + java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger(name); + return new JulLogger(julLogger); + } + + @Override + public boolean isDebugEnabled() { + return delegate.isLoggable(Level.FINE); + } + + @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 info(String msg) { + log(Level.INFO, msg, null); + } + + @Override + public void info(String format, Object... args) { + log(Level.INFO, format, args); + } + + @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 error(String msg) { + log(Level.SEVERE, msg, null); + } + + @Override + public void error(String format, Object... args) { + log(Level.SEVERE, format, args); + } + + 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}: + * + *

+ */ + 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..71c157a8a --- /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; + +/** + * A {@link LoggerFactory} 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 extends LoggerFactory { + + public static final JulLoggerFactory INSTANCE = new JulLoggerFactory(); + + @Override + protected Logger newInstance(Class type) { + return JulLogger.create(type); + } + + @Override + protected Logger newInstance(String name) { + return JulLogger.create(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..6f41da426 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Logger.java @@ -0,0 +1,28 @@ +package com.databricks.sdk.core.logging; + +/** + * Logging contract used throughout the SDK. + * + *

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 boolean isDebugEnabled(); + + public abstract void debug(String msg); + + public abstract void debug(String format, Object... args); + + public abstract void info(String msg); + + public abstract void info(String format, Object... args); + + public abstract void warn(String msg); + + public abstract void warn(String format, Object... args); + + public abstract void error(String msg); + + public abstract void error(String format, Object... args); +} 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..c0d000e6f --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/LoggerFactory.java @@ -0,0 +1,59 @@ +package com.databricks.sdk.core.logging; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * Creates and configures {@link Logger} instances for the SDK. + * + *

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();
+ * }
+ * + *

Extend this class to provide a fully custom logging backend. + */ +public abstract class LoggerFactory { + + private static final AtomicReference defaultFactory = new AtomicReference<>(); + + /** Returns a logger for the given class, using the current default factory. */ + public static Logger getLogger(Class type) { + return getDefault().newInstance(type); + } + + /** Returns a logger with the given name, using the current default factory. */ + public static Logger getLogger(String name) { + return getDefault().newInstance(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(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 new logger for the given class. */ + protected abstract Logger newInstance(Class type); + + /** Creates a new logger with the given name. */ + protected abstract Logger newInstance(String name); +} 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..267702963 --- /dev/null +++ b/databricks-sdk-java/src/main/java/com/databricks/sdk/core/logging/Slf4jLogger.java @@ -0,0 +1,64 @@ +package com.databricks.sdk.core.logging; + +/** Delegates all logging calls to an SLF4J {@code Logger}. */ +class Slf4jLogger extends Logger { + + private final org.slf4j.Logger delegate; + + private Slf4jLogger(org.slf4j.Logger delegate) { + this.delegate = delegate; + } + + static Logger create(Class type) { + return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(type)); + } + + static Logger create(String name) { + return new Slf4jLogger(org.slf4j.LoggerFactory.getLogger(name)); + } + + @Override + public boolean isDebugEnabled() { + return delegate.isDebugEnabled(); + } + + @Override + public void debug(String msg) { + delegate.debug(msg); + } + + @Override + public void debug(String format, Object... args) { + delegate.debug(format, args); + } + + @Override + public void info(String msg) { + delegate.info(msg); + } + + @Override + public void info(String format, Object... args) { + delegate.info(format, args); + } + + @Override + public void warn(String msg) { + delegate.warn(msg); + } + + @Override + public void warn(String format, Object... args) { + delegate.warn(format, args); + } + + @Override + public void error(String msg) { + delegate.error(msg); + } + + @Override + public void error(String format, Object... args) { + delegate.error(format, args); + } +} 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..c34687b02 --- /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; + +/** A {@link LoggerFactory} backed by SLF4J. This is the default. */ +public class Slf4jLoggerFactory extends LoggerFactory { + + public static final Slf4jLoggerFactory INSTANCE = new Slf4jLoggerFactory(); + + @Override + protected Logger newInstance(Class type) { + return Slf4jLogger.create(type); + } + + @Override + protected Logger newInstance(String name) { + return Slf4jLogger.create(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..a5820239a 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 @@ -6,8 +6,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.HashMap; import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..dae0d7351 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 @@ -6,8 +6,8 @@ import java.time.Instant; import java.util.Objects; import java.util.concurrent.CompletableFuture; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..96e11c865 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 @@ -21,8 +21,8 @@ import java.util.Objects; import java.util.Optional; import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..0a8ea2598 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 @@ -9,8 +9,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..d80be284d 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 @@ -7,8 +7,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..884fb91f4 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 @@ -12,8 +12,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..b2ae78d7f 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 @@ -8,8 +8,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..482b671f9 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 @@ -6,8 +6,8 @@ import com.databricks.sdk.support.InternalApi; import java.io.Serializable; import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..7b42b441a 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 @@ -6,8 +6,8 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..90b6e96b8 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 @@ -14,8 +14,8 @@ import java.util.Map; import java.util.Objects; import org.apache.http.HttpHeaders; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..a63e1eb17 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 @@ -6,8 +6,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..2d8ccf8f2 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 @@ -7,8 +7,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..68ce33d4d 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 @@ -13,8 +13,8 @@ import java.util.concurrent.TimeoutException; import java.util.function.Function; import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.databricks.sdk.core.logging.Logger; +import com.databricks.sdk.core.logging.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..ddaba64ad --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/JulLoggerTest.java @@ -0,0 +1,85 @@ +package com.databricks.sdk.core.logging; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class JulLoggerTest { + + @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[] {})); + } + + @Test + void julLoggerLevelMapping() { + Logger logger = JulLogger.create(JulLoggerTest.class); + assertNotNull(logger); + logger.debug("debug msg"); + logger.debug("debug {} {}", "a", "b"); + logger.info("info msg"); + logger.info("info {}", "val"); + logger.warn("warn msg"); + logger.warn("warn {}", "val"); + logger.error("error msg"); + logger.error("error {}", "val"); + logger.error("error {} failed", "op", new RuntimeException("cause")); + } +} 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..495e3d2a6 --- /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.MessageFormatter; +import org.slf4j.helpers.FormattingTuple; + +/** + * 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..817e583dd --- /dev/null +++ b/databricks-sdk-java/src/test/java/com/databricks/sdk/core/logging/Slf4jLoggerTest.java @@ -0,0 +1,31 @@ +package com.databricks.sdk.core.logging; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class Slf4jLoggerTest { + + @Test + void createReturnsSlf4jLogger() { + Logger logger = Slf4jLogger.create(Slf4jLoggerTest.class); + assertNotNull(logger); + assertTrue(logger instanceof Slf4jLogger); + } + + @Test + void slf4jLoggerOperations() { + Logger logger = Slf4jLogger.create(Slf4jLoggerTest.class); + logger.debug("debug"); + logger.debug("debug {}", "arg"); + logger.info("info"); + logger.info("info {}", "arg"); + logger.warn("warn"); + logger.warn("warn {}", "arg"); + logger.error("error"); + logger.error("error {}", "arg"); + logger.error("error {} failed", "op", new RuntimeException("cause")); + logger.isDebugEnabled(); + } + +}