com.databricks.jdbc.log.JulLogger Maven / Gradle / Ivy
package com.databricks.jdbc.log;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Set;
import java.util.logging.*;
import java.util.stream.Stream;
/**
* The {@code JulLogger} class provides an implementation of the {@link JdbcLogger} interface using
* the Java Util Logging (JUL) framework. It supports logging messages at different levels such as
* trace, debug, info, warn, and error, both with and without associated {@link Throwable} objects.
*
* This class also includes a static method to initialize the logger with custom configurations
* such as log level, log directory, log file size, and log file count. It supports logging to both
* the console and file system based on the provided configuration.
*
*
Log messages include the name of the class and method from where the logging request was made,
* providing a clear context for the log messages. This is achieved by analyzing the stack trace to
* find the caller information.
*/
public class JulLogger implements JdbcLogger {
public static final String STDOUT = "STDOUT";
public static final String PARENT_CLASS_PREFIX = "com.databricks.jdbc";
public static final String DATABRICKS_LOG_FILE = "databricks_jdbc.log";
public static final String JAVA_UTIL_LOGGING_CONFIG_FILE = "java.util.logging.config.file";
private static final Set logMethods = Set.of("debug", "error", "info", "trace", "warn");
protected Logger logger;
protected static volatile boolean isLoggerInitialized = false;
/** Constructs a new {@code JulLogger} object with the specified name. */
public JulLogger(String name) {
this.logger = Logger.getLogger(name);
}
/** {@inheritDoc} */
@Override
public void trace(String message) {
log(Level.FINEST, message, null);
}
/** {@inheritDoc} */
@Override
public void debug(String message) {
log(Level.FINE, message, null);
}
/** {@inheritDoc} */
@Override
public void info(String message) {
log(Level.INFO, message, null);
}
/** {@inheritDoc} */
@Override
public void warn(String message) {
log(Level.WARNING, message, null);
}
/** {@inheritDoc} */
@Override
public void error(String message) {
log(Level.SEVERE, message, null);
}
/** {@inheritDoc} */
@Override
public void error(String message, Throwable throwable) {
log(Level.SEVERE, message, throwable);
}
/**
* Initializes the logger with the specified configuration. This method is synchronized to prevent
* concurrent modifications to the logger configuration.
*
* @param level the log level
* @param logDir the directory for log files or {@code STDOUT} for console output
* @param logFileSizeBytes the maximum size of a single log file in bytes
* @param logFileCount the number of log files to rotate
* @throws IOException if an I/O error occurs
*/
public static synchronized void initLogger(
Level level, String logDir, int logFileSizeBytes, int logFileCount) throws IOException {
if (!isLoggerInitialized) {
isLoggerInitialized = true;
// java.util.logging uses hierarchical loggers, so we just need to set the log level on the
// parent package logger
Logger jdbcJulLogger = Logger.getLogger(PARENT_CLASS_PREFIX);
jdbcJulLogger.setLevel(level);
String logPattern = getLogPattern(logDir);
Handler handler;
if (logPattern.equalsIgnoreCase(STDOUT)) {
handler = new ConsoleHandler();
} else {
handler = new FileHandler(logPattern, logFileSizeBytes, logFileCount, true);
}
handler.setLevel(level);
handler.setFormatter(new Slf4jFormatter());
jdbcJulLogger.addHandler(handler);
}
}
private void log(Level level, String message, Throwable throwable) {
String[] callerClassMethod = getCaller();
if (throwable == null) {
logger.logp(level, callerClassMethod[0], callerClassMethod[1], message);
} else {
logger.logp(level, callerClassMethod[0], callerClassMethod[1], message, throwable);
}
}
/**
* Retrieves the class name and method name of the caller that initiated the logging request. This
* method navigates the stack trace to find the first method outside the known logging methods,
* providing the context from where the log was called. This is particularly useful for including
* in log messages to identify the source of the log entry.
*
* The method uses a two-step filtering process on the stack trace:
*
*
* - It first drops stack trace elements until it finds one whose method name is a known
* logging method (e.g., trace, debug, info, warn, error).
*
- Then, it continues to drop elements until it finds the first method not in the set of
* logging methods, which is considered the caller.
*
*/
protected static String[] getCaller() {
return Stream.of(Thread.currentThread().getStackTrace())
.dropWhile(stackTrace -> !logMethods.contains(stackTrace.getMethodName()))
.dropWhile(stackTrace -> logMethods.contains(stackTrace.getMethodName()))
.findFirst()
.map(stackTrace -> new String[] {stackTrace.getClassName(), stackTrace.getMethodName()})
.orElse(
new String[] {
"unknownClass", "unknownMethod"
}); // lost in the stack trace wonderland :)
}
/**
* Generates the log file pattern based on the provided log directory. If the log directory is
* specified as "STDOUT", logging will be directed to the console. Otherwise, it ensures the
* directory exists and resolves the log file path within it.
*/
protected static String getLogPattern(String logDir) {
if (logDir.equalsIgnoreCase(STDOUT)) {
return STDOUT;
}
Path dirPath = Paths.get(logDir);
if (Files.notExists(dirPath)) {
try {
Files.createDirectories(dirPath);
} catch (IOException e) {
// If the directory cannot be created, log to the console instead
return STDOUT;
}
}
return dirPath.resolve(DATABRICKS_LOG_FILE).toString();
}
}