google-site-verification: google61fe8ba583a51912.html
Loguru for Java

Introduction to Loguru-style Logging

Loguru is a popular Python logging library known for its simplicity, powerful configuration, and beautiful output. This implementation brings Loguru's philosophy to Java with easy setup, structured logging, and rich features.

Core Logging Interface

Simplified Logger API

public class Loguru { private static final Logger INSTANCE = new Logger(); private Loguru() {} // Prevent instantiation public static Logger getLogger() { return INSTANCE; } // Convenience methods for direct logging public static void info(String message) { INSTANCE.info(message); } public static void info(String format, Object... args) { INSTANCE.info(format, args); } public static void debug(String message) { INSTANCE.debug(message); } public static void warning(String message) { INSTANCE.warning(message); } public static void error(String message) { INSTANCE.error(message); } public static void error(Throwable throwable, String message) { INSTANCE.error(throwable, message); } public static void success(String message) { INSTANCE.success(message); } // Context management public static void bind(String key, Object value) { INSTANCE.bind(key, value); } public static void unbind(String key) { INSTANCE.unbind(key); } public static Context context(String key, Object value) { return INSTANCE.context(key, value); } public static Context context(Map<String, Object> context) { return INSTANCE.context(context); } } // Main Logger class public class Logger { private final List<LogHandler> handlers = new CopyOnWriteArrayList<>(); private final Map<String, Object> globalContext = new ConcurrentHashMap<>(); private final ThreadLocal<Map<String, Object>> threadContext = ThreadLocal.withInitial(HashMap::new); private Level minimumLevel = Level.INFO; private boolean enableColors = true; private DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); // Core logging methods public void info(String message) { log(Level.INFO, message); } public void info(String format, Object... args) { log(Level.INFO, format, args); } public void debug(String message) { log(Level.DEBUG, message); } public void warning(String message) { log(Level.WARNING, message); } public void error(String message) { log(Level.ERROR, message); } public void error(Throwable throwable, String message) { log(Level.ERROR, throwable, message); } public void success(String message) { log(Level.SUCCESS, message); } public void log(Level level, String message) { if (shouldLog(level)) { LogRecord record = buildLogRecord(level, message, null); handleLogRecord(record); } } public void log(Level level, String format, Object... args) { if (shouldLog(level)) { String message = String.format(format, args); LogRecord record = buildLogRecord(level, message, null); handleLogRecord(record); } } public void log(Level level, Throwable throwable, String message) { if (shouldLog(level)) { LogRecord record = buildLogRecord(level, message, throwable); handleLogRecord(record); } } // Context management public void bind(String key, Object value) { threadContext.get().put(key, value); } public void unbind(String key) { threadContext.get().remove(key); } public Context context(String key, Object value) { return new Context(this).bind(key, value); } public Context context(Map<String, Object> context) { return new Context(this).bind(context); } public void addGlobalContext(String key, Object value) { globalContext.put(key, value); } // Handler management public void addHandler(LogHandler handler) { handlers.add(handler); } public void removeHandler(LogHandler handler) { handlers.remove(handler); } public void clearHandlers() { handlers.clear(); } // Configuration public void setLevel(Level level) { this.minimumLevel = level; } public void enableColors(boolean enable) { this.enableColors = enable; } public void setTimeFormat(String pattern) { this.timeFormatter = DateTimeFormatter.ofPattern(pattern); } // Internal implementation private boolean shouldLog(Level level) { return level.ordinal() >= minimumLevel.ordinal(); } private LogRecord buildLogRecord(Level level, String message, Throwable throwable) { Map<String, Object> combinedContext = new HashMap<>(); combinedContext.putAll(globalContext); combinedContext.putAll(threadContext.get()); return new LogRecord( Instant.now(), level, Thread.currentThread().getName(), Thread.currentThread().getId(), message, throwable, Collections.unmodifiableMap(combinedContext) ); } private void handleLogRecord(LogRecord record) { for (LogHandler handler : handlers) { try { handler.handle(record); } catch (Exception e) { // Don't let handler exceptions break logging System.err.println("Log handler failed: " + e.getMessage()); } } } }

Data Models and Enums

Log Levels and Records

public enum Level { TRACE(0, "TRACE", AnsiColor.CYAN), DEBUG(1, "DEBUG", AnsiColor.BLUE), INFO(2, "INFO", AnsiColor.GREEN), SUCCESS(3, "SUCCESS", AnsiColor.BRIGHT_GREEN), WARNING(4, "WARNING", AnsiColor.YELLOW), ERROR(5, "ERROR", AnsiColor.RED), CRITICAL(6, "CRITICAL", AnsiColor.BRIGHT_RED); private final int severity; private final String name; private final AnsiColor color; Level(int severity, String name, AnsiColor color) { this.severity = severity; this.name = name; this.color = color; } public int getSeverity() { return severity; } public String getName() { return name; } public AnsiColor getColor() { return color; } } public class LogRecord { private final Instant timestamp; private final Level level; private final String threadName; private final long threadId; private final String message; private final Throwable throwable; private final Map<String, Object> context; public LogRecord(Instant timestamp, Level level, String threadName, long threadId, String message, Throwable throwable, Map<String, Object> context) { this.timestamp = timestamp; this.level = level; this.threadName = threadName; this.threadId = threadId; this.message = message; this.throwable = throwable; this.context = context; } // Getters public Instant getTimestamp() { return timestamp; } public Level getLevel() { return level; } public String getThreadName() { return threadName; } public long getThreadId() { return threadId; } public String getMessage() { return message; } public Throwable getThrowable() { return throwable; } public Map<String, Object> getContext() { return context; } } // Context wrapper for fluent API public class Context implements AutoCloseable { private final Logger logger; private final Map<String, Object> localBindings = new HashMap<>(); public Context(Logger logger) { this.logger = logger; } public Context bind(String key, Object value) { localBindings.put(key, value); logger.bind(key, value); return this; } public Context bind(Map<String, Object> context) { localBindings.putAll(context); context.forEach(logger::bind); return this; } @Override public void close() { // Remove bindings when context goes out of scope localBindings.keySet().forEach(logger::unbind); } }

Handlers and Formatters

Console Handler with Colors

public interface LogHandler { void handle(LogRecord record); void setFormatter(LogFormatter formatter); void close(); } public class ConsoleHandler implements LogHandler { private LogFormatter formatter = new DefaultFormatter(); private PrintStream output = System.out; private PrintStream errorOutput = System.err; @Override public void handle(LogRecord record) { String formatted = formatter.format(record); if (record.getLevel().getSeverity() >= Level.ERROR.getSeverity()) { errorOutput.println(formatted); } else { output.println(formatted); } // Print exception stack trace if present if (record.getThrowable() != null) { record.getThrowable().printStackTrace( record.getLevel().getSeverity() >= Level.ERROR.getSeverity() ? errorOutput : output); } } @Override public void setFormatter(LogFormatter formatter) { this.formatter = formatter; } @Override public void close() { // Nothing to close for console } public void setOutput(PrintStream output) { this.output = output; } public void setErrorOutput(PrintStream errorOutput) { this.errorOutput = errorOutput; } } // File handler with rotation public class FileHandler implements LogHandler { private final Path logPath; private final long maxFileSize; private final int backupCount; private final boolean compressBackups; private LogFormatter formatter = new DefaultFormatter(); private Writer currentWriter; private long currentFileSize; public FileHandler(String filePath) { this(filePath, 10 * 1024 * 1024, 5, true); // 10MB, 5 backups, compress } public FileHandler(String filePath, long maxFileSize, int backupCount, boolean compressBackups) { this.logPath = Paths.get(filePath); this.maxFileSize = maxFileSize; this.backupCount = backupCount; this.compressBackups = compressBackups; initializeWriter(); } private void initializeWriter() { try { Files.createDirectories(logPath.getParent()); currentWriter = Files.newBufferedWriter(logPath, StandardOpenOption.CREATE, StandardOpenOption.APPEND); currentFileSize = Files.size(logPath); } catch (IOException e) { throw new LoggingException("Failed to initialize file handler", e); } } @Override public void handle(LogRecord record) { try { String formatted = formatter.format(record) + System.lineSeparator(); byte[] bytes = formatted.getBytes(StandardCharsets.UTF_8); // Check if we need to rotate if (currentFileSize + bytes.length > maxFileSize) { rotateFiles(); } currentWriter.write(formatted); currentWriter.flush(); currentFileSize += bytes.length; } catch (IOException e) { throw new LoggingException("Failed to write log record", e); } } private void rotateFiles() throws IOException { currentWriter.close(); // Rotate existing backup files for (int i = backupCount - 1; i >= 0; i--) { Path source = getBackupPath(i); Path target = getBackupPath(i + 1); if (Files.exists(source)) { if (i + 1 >= backupCount) { Files.delete(source); // Remove oldest backup } else { Files.move(source, target); // Compress if enabled if (compressBackups && i == 0) { compressFile(target); } } } } // Move current to backup 0 Path firstBackup = getBackupPath(0); Files.move(logPath, firstBackup); initializeWriter(); } private Path getBackupPath(int backupIndex) { if (backupIndex == 0) { return logPath.resolveSibling( logPath.getFileName() + ".1"); } return logPath.resolveSibling( logPath.getFileName() + "." + backupIndex); } private void compressFile(Path file) throws IOException { Path compressedFile = file.resolveSibling(file.getFileName() + ".gz"); try (GZIPOutputStream gzos = new GZIPOutputStream( Files.newOutputStream(compressedFile)); InputStream is = Files.newInputStream(file)) { is.transferTo(gzos); } Files.delete(file); } @Override public void setFormatter(LogFormatter formatter) { this.formatter = formatter; } @Override public void close() { try { if (currentWriter != null) { currentWriter.close(); } } catch (IOException e) { // Ignore close errors } } }

Formatters

public interface LogFormatter { String format(LogRecord record); } public class DefaultFormatter implements LogFormatter { private boolean enableColors = true; private boolean showThread = true; private boolean showContext = true; @Override public String format(LogRecord record) { StringBuilder sb = new StringBuilder(); // Timestamp String timestamp = LocalDateTime.ofInstant(record.getTimestamp(), ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); sb.append(timestamp).append(" | "); // Level with color String levelName = record.getLevel().getName(); if (enableColors && record.getLevel().getColor() != null) { levelName = record.getLevel().getColor().colorize( String.format("%-8s", levelName)); } else { levelName = String.format("%-8s", levelName); } sb.append(levelName).append(" | "); // Thread info if (showThread) { sb.append(record.getThreadName()) .append("(").append(record.getThreadId()).append(") | "); } // Message sb.append(record.getMessage()); // Context if (showContext && !record.getContext().isEmpty()) { sb.append(" {"); record.getContext().forEach((key, value) -> { sb.append(key).append("=").append(value).append(", "); }); sb.setLength(sb.length() - 2); // Remove trailing comma sb.append("}"); } return sb.toString(); } public void enableColors(boolean enable) { this.enableColors = enable; } public void showThread(boolean show) { this.showThread = show; } public void showContext(boolean show) { this.showContext = show; } } // JSON formatter for structured logging public class JsonFormatter implements LogFormatter { private final ObjectMapper mapper; private boolean prettyPrint = false; public JsonFormatter() { this.mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); } @Override public String format(LogRecord record) { try { Map<String, Object> logEntry = new LinkedHashMap<>(); logEntry.put("timestamp", record.getTimestamp().toString()); logEntry.put("level", record.getLevel().name()); logEntry.put("thread", record.getThreadName()); logEntry.put("threadId", record.getThreadId()); logEntry.put("message", record.getMessage()); if (record.getThrowable() != null) { logEntry.put("exception", buildExceptionInfo(record.getThrowable())); } if (!record.getContext().isEmpty()) { logEntry.put("context", record.getContext()); } if (prettyPrint) { return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(logEntry); } else { return mapper.writeValueAsString(logEntry); } } catch (JsonProcessingException e) { return "{\"error\": \"Failed to format log entry\"}"; } } private Map<String, Object> buildExceptionInfo(Throwable throwable) { Map<String, Object> exceptionInfo = new HashMap<>(); exceptionInfo.put("type", throwable.getClass().getName()); exceptionInfo.put("message", throwable.getMessage()); // Include stack trace StringWriter sw = new StringWriter(); throwable.printStackTrace(new PrintWriter(sw)); exceptionInfo.put("stackTrace", sw.toString().split(System.lineSeparator())); if (throwable.getCause() != null) { exceptionInfo.put("cause", buildExceptionInfo(throwable.getCause())); } return exceptionInfo; } public void setPrettyPrint(boolean prettyPrint) { this.prettyPrint = prettyPrint; } }

ANSI Color Support

public enum AnsiColor { BLACK("\u001B[30m"), RED("\u001B[31m"), GREEN("\u001B[32m"), YELLOW("\u001B[33m"), BLUE("\u001B[34m"), MAGENTA("\u001B[35m"), CYAN("\u001B[36m"), WHITE("\u001B[37m"), BRIGHT_BLACK("\u001B[90m"), BRIGHT_RED("\u001B[91m"), BRIGHT_GREEN("\u001B[92m"), BRIGHT_YELLOW("\u001B[93m"), BRIGHT_BLUE("\u001B[94m"), BRIGHT_MAGENTA("\u001B[95m"), BRIGHT_CYAN("\u001B[96m"), BRIGHT_WHITE("\u001B[97m"), RESET("\u001B[0m"); private final String code; AnsiColor(String code) { this.code = code; } public String colorize(String text) { return code + text + RESET.code; } public static boolean isColorSupported() { return System.console() != null && !System.getProperty("os.name").toLowerCase().contains("win"); } }

Spring Boot Integration

Auto-Configuration

@Configuration @EnableConfigurationProperties(LoguruProperties.class) public class LoguruAutoConfiguration { @Bean @ConditionalOnMissingBean public Logger loguruLogger(LoguruProperties properties) { Logger logger = Loguru.getLogger(); // Configure based on properties logger.setLevel(Level.valueOf(properties.getLevel().toUpperCase())); logger.enableColors(properties.isEnableColors()); // Add handlers if (properties.getConsole().isEnabled()) { ConsoleHandler consoleHandler = new ConsoleHandler(); if (properties.getConsole().isJsonFormat()) { consoleHandler.setFormatter(new JsonFormatter()); } logger.addHandler(consoleHandler); } if (properties.getFile().isEnabled()) { FileHandler fileHandler = new FileHandler( properties.getFile().getPath(), properties.getFile().getMaxSize(), properties.getFile().getBackupCount(), properties.getFile().isCompress() ); if (properties.getFile().isJsonFormat()) { fileHandler.setFormatter(new JsonFormatter()); } logger.addHandler(fileHandler); } return logger; } @Bean public LoguruHealthIndicator loguruHealthIndicator() { return new LoguruHealthIndicator(); } } @ConfigurationProperties(prefix = "loguru") public class LoguruProperties { private String level = "INFO"; private boolean enableColors = true; private Console console = new Console(); private File file = new File(); public static class Console { private boolean enabled = true; private boolean jsonFormat = false; // Getters and setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public boolean isJsonFormat() { return jsonFormat; } public void setJsonFormat(boolean jsonFormat) { this.jsonFormat = jsonFormat; } } public static class File { private boolean enabled = false; private String path = "logs/app.log"; private long maxSize = 10 * 1024 * 1024; // 10MB private int backupCount = 5; private boolean compress = true; private boolean jsonFormat = false; // Getters and setters public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public long getMaxSize() { return maxSize; } public void setMaxSize(long maxSize) { this.maxSize = maxSize; } public int getBackupCount() { return backupCount; } public void setBackupCount(int backupCount) { this.backupCount = backupCount; } public boolean isCompress() { return compress; } public void setCompress(boolean compress) { this.compress = compress; } public boolean isJsonFormat() { return jsonFormat; } public void setJsonFormat(boolean jsonFormat) { this.jsonFormat = jsonFormat; } } // Getters and setters public String getLevel() { return level; } public void setLevel(String level) { this.level = level; } public boolean isEnableColors() { return enableColors; } public void setEnableColors(boolean enableColors) { this.enableColors = enableColors; } public Console getConsole() { return console; } public void setConsole(Console console) { this.console = console; } public File getFile() { return file; } public void setFile(File file) { this.file = file; } }

Usage Examples

Basic Logging

public class OrderService { public void processOrder(Order order) { // Simple logging Loguru.info("Processing order {}", order.getId()); try { // Log with context try (Context ctx = Loguru.context("order_id", order.getId()) .context("customer", order.getCustomerId())) { validateOrder(order); processPayment(order); updateInventory(order); Loguru.success("Order {} processed successfully", order.getId()); } } catch (Exception e) { Loguru.error(e, "Failed to process order {}", order.getId()); throw e; } } public void batchProcess(List<Order> orders) { Loguru.info("Starting batch processing of {} orders", orders.size()); long startTime = System.currentTimeMillis(); orders.forEach(order -> { // Add per-order context Loguru.bind("order_id", order.getId()); Loguru.bind("order_amount", order.getAmount()); processOrder(order); // Remove context Loguru.unbind("order_id"); Loguru.unbind("order_amount"); }); long duration = System.currentTimeMillis() - startTime; Loguru.info("Batch processing completed in {} ms", duration); } }

Advanced Features

public class AdvancedLoggingExamples { public void demonstrateFeatures() { // 1. Conditional logging Loguru.debug("Debug information only visible in development"); // 2. Structured logging with JSON Map<String, Object> businessContext = new HashMap<>(); businessContext.put("service", "payment"); businessContext.put("transaction_id", UUID.randomUUID().toString()); businessContext.put("amount", 99.99); try (Context ctx = Loguru.context(businessContext)) { Loguru.info("Payment processed"); } // 3. Exception handling with rich context try { riskyOperation(); } catch (Exception e) { Loguru.error(e, "Operation failed with user: {}", getCurrentUser()); } // 4. Performance logging long start = System.nanoTime(); expensiveOperation(); long duration = System.nanoTime() - start; Loguru.info("Expensive operation took {} ns", duration); } // Custom handler for metrics public class MetricsHandler implements LogHandler { private final MeterRegistry meterRegistry; public MetricsHandler(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @Override public void handle(LogRecord record) { // Count log messages by level Counter.builder("log.messages") .tag("level", record.getLevel().name()) .register(meterRegistry) .increment(); // Record error occurrences if (record.getLevel() == Level.ERROR) { Counter.builder("errors.total") .register(meterRegistry) .increment(); } } @Override public void setFormatter(LogFormatter formatter) { // Not used for metrics } @Override public void close() { // Cleanup if needed } } }

Testing Support

public class TestLogCapture implements LogHandler { private final List<LogRecord> capturedRecords = new CopyOnWriteArrayList<>(); private final Level captureLevel; public TestLogCapture() { this(Level.DEBUG); // Capture all levels by default } public TestLogCapture(Level captureLevel) { this.captureLevel = captureLevel; } @Override public void handle(LogRecord record) { if (record.getLevel().ordinal() >= captureLevel.ordinal()) { capturedRecords.add(record); } } public List<LogRecord> getCapturedRecords() { return Collections.unmodifiableList(capturedRecords); } public void clear() { capturedRecords.clear(); } public boolean containsMessage(String message) { return capturedRecords.stream() .anyMatch(record -> record.getMessage().contains(message)); } public boolean containsLevel(Level level) { return capturedRecords.stream() .anyMatch(record -> record.getLevel() == level); } @Override public void setFormatter(LogFormatter formatter) { // Not used for testing } @Override public void close() { clear(); } } @ExtendWith(LogCaptureExtension.class) public class LoguruTest { @Test public void testLoggingCapture(TestLogCapture logCapture) { Loguru.info("Test message"); Loguru.error(new RuntimeException("Test error"), "Error occurred"); assertTrue(logCapture.containsMessage("Test message")); assertTrue(logCapture.containsLevel(Level.ERROR)); assertEquals(2, logCapture.getCapturedRecords().size()); } }

Configuration Examples

application.yml

loguru: level: DEBUG enable-colors: true console: enabled: true json-format: false file: enabled: true path: "logs/application.log" max-size: 10MB backup-count: 5 compress: true json-format: true

Programmatic Configuration

public class LoguruConfig { public static void configure() { Logger logger = Loguru.getLogger(); // Set minimum level logger.setLevel(Level.DEBUG); // Console handler with colors ConsoleHandler consoleHandler = new ConsoleHandler(); DefaultFormatter consoleFormatter = new DefaultFormatter(); consoleFormatter.enableColors(true); consoleHandler.setFormatter(consoleFormatter); logger.addHandler(consoleHandler); // File handler with JSON FileHandler fileHandler = new FileHandler("logs/app.json.log"); fileHandler.setFormatter(new JsonFormatter()); logger.addHandler(fileHandler); // Add global context logger.addGlobalContext("application", "my-service"); logger.addGlobalContext("version", "1.0.0"); logger.addGlobalContext("environment", System.getenv().getOrDefault("ENV", "development")); } }

Maven Dependencies

<dependencies> <!-- Spring Boot (optional) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <optional>true</optional> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.0</version> <scope>test</scope> </dependency> </dependencies>

Conclusion

This Loguru-inspired Java logging library provides:

  1. Simple API - Easy-to-use static methods and fluent interface
  2. Powerful Context - Thread-local and request-scoped context management
  3. Beautiful Output - Colored console output with customizable formats
  4. Structured Logging - JSON support for machine-readable logs
  5. Flexible Handlers - Console, file, and custom handlers with rotation
  6. Spring Integration - Auto-configuration and health checks
  7. Testing Support - Log capture for unit testing

The library maintains the philosophy of Loguru while leveraging Java's type safety and ecosystem integration capabilities.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper