Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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}:
*
* <ul>
* <li>A trailing {@link Throwable} is unconditionally excluded from formatting.
* <li>A backslash before {@code {}} escapes it as a literal {@code {}}.
* <li>Array arguments are rendered with {@link Arrays#deepToString}.
* <li>A {@code null} format string returns {@code null}.
* </ul>
*/
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.databricks.sdk.core.logging;

/**
* A {@link LoggerFactory} backed by {@code java.util.logging}. Always available on any JRE.
*
* <p>Use this when SLF4J is not desirable:
*
* <pre>{@code
* LoggerFactory.setDefault(JulLoggerFactory.INSTANCE);
* }</pre>
*/
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.databricks.sdk.core.logging;

/**
* Logging contract used throughout the SDK.
*
* <p>Extend this class to provide a custom logging implementation, then register it via a custom
* {@link LoggerFactory} subclass and {@link LoggerFactory#setDefault}.
*/
public abstract class Logger {

public abstract 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);
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>By default, logging goes through SLF4J. Users can override the backend programmatically
* before creating any SDK client:
*
* <pre>{@code
* LoggerFactory.setDefault(JulLoggerFactory.INSTANCE);
* WorkspaceClient ws = new WorkspaceClient();
* }</pre>
*
* <p>Extend this class to provide a fully custom logging backend.
*/
public abstract class LoggerFactory {

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

/** Returns a logger for the given class, using the current default factory. */
public static Logger getLogger(Class<?> type) {
return getDefault().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.
*
* <p>Must be called before creating any SDK client or calling {@link #getLogger}. Loggers
* already obtained will not be affected by subsequent calls.
*/
public static void setDefault(LoggerFactory factory) {
if (factory == null) {
throw new IllegalArgumentException("LoggerFactory must not be null");
}
defaultFactory.set(factory);
}

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

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

/** A {@link LoggerFactory} backed by SLF4J. 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);
}
}
Loading