google-site-verification: google61fe8ba583a51912.html
Logging Best Practices in Java: Complete Guide

Logging is a critical component of any production application. Proper logging can mean the difference between quickly diagnosing issues and spending hours debugging in production. Here's a comprehensive guide to logging best practices in Java.

Table of Contents

  1. Choosing the Right Logging Framework
  2. Logging Levels and When to Use Them
  3. Structured Logging
  4. Performance Considerations
  5. Security and Compliance
  6. Configuration Best Practices
  7. Testing and Monitoring
  8. Complete Implementation Example

1. Choosing the Right Logging Framework

Framework Comparison

SLF4J with Logback (Recommended)

<!-- pom.xml --> <dependencies> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.9</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.11</version> </dependency> <!-- For JSON logging --> <dependency> <groupId>ch.qos.logback.contrib</groupId> <artifactId>logback-json-classic</artifactId> <version>0.1.5</version> </dependency> <dependency> <groupId>ch.qos.logback.contrib</groupId> <artifactId>logback-jackson</artifactId> <version>0.1.5</version> </dependency> </dependencies>

Unified Logging Interface

LoggerFactory.java - Consistent logger creation

import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class ApplicationLogger { private ApplicationLogger() { // Utility class } public static Logger getLogger(Class<?> clazz) { return LoggerFactory.getLogger(clazz); } public static Logger getLogger(String name) { return LoggerFactory.getLogger(name); } }

2. Logging Levels and When to Use Them

Log Level Guidelines

LogLevelGuide.java - Practical usage examples

import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LogLevelGuide { private static final Logger logger = LoggerFactory.getLogger(LogLevelGuide.class); public void processUserRequest(UserRequest request) { // TRACE: Very detailed information, typically for development logger.trace("Entering processUserRequest with request: {}", request); // DEBUG: Detailed information for debugging logger.debug("Processing request for user: {}", request.getUserId()); try { // Business logic validateRequest(request); // INFO: Important business process information logger.info("Successfully processed request for user: {}, type: {}", request.getUserId(), request.getType()); } catch (ValidationException e) { // WARN: Unexpected but recoverable situations logger.warn("Validation failed for user {}: {}", request.getUserId(), e.getMessage()); throw e; } catch (Exception e) { // ERROR: Error events that might still allow application to continue logger.error("Failed to process request for user: {}", request.getUserId(), e); throw new ProcessingException("Request processing failed", e); } } public void startup() { // INFO: Application lifecycle events logger.info("Application starting up on port: {}", getServerPort()); logger.info("Connected to database: {}", getDatabaseInfo()); } public void shutdown() { logger.info("Application shutting down gracefully"); } public void fatalError() { // In SLF4J, use ERROR for fatal conditions, or implement specific handling logger.error("CRITICAL: Database connection lost, application cannot continue"); System.exit(1); } }

3. Structured Logging

Structured Logging Implementation

StructuredLogger.java - Enhanced logging with context

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import java.util.Map; import java.util.HashMap; public class StructuredLogger { private final Logger logger; private final Map<String, String> context; public StructuredLogger(Class<?> clazz) { this.logger = LoggerFactory.getLogger(clazz); this.context = new HashMap<>(); } public StructuredLogger withContext(String key, String value) { context.put(key, value); return this; } public void info(String message, Object... args) { withMDC(() -> logger.info(message, args)); } public void info(String message, Map<String, Object> structuredData) { withMDC(() -> { String structuredMessage = buildStructuredMessage(message, structuredData); logger.info(structuredMessage); }); } public void error(String message, Throwable throwable, Map<String, Object> context) { withMDC(() -> { String structuredMessage = buildStructuredMessage(message, context); logger.error(structuredMessage, throwable); }); } public void warn(String message, Map<String, Object> context) { withMDC(() -> { String structuredMessage = buildStructuredMessage(message, context); logger.warn(structuredMessage); }); } public void debug(String message, Map<String, Object> context) { withMDC(() -> { String structuredMessage = buildStructuredMessage(message, context); logger.debug(structuredMessage); }); } private void withMDC(Runnable loggingOperation) { // Set MDC context Map<String, String> previousContext = MDC.getCopyOfContextMap(); try { // Clear and set new context MDC.clear(); if (context != null) { context.forEach(MDC::put); } loggingOperation.run(); } finally { // Restore previous context MDC.clear(); if (previousContext != null) { previousContext.forEach(MDC::put); } } } private String buildStructuredMessage(String message, Map<String, Object> data) { if (data == null || data.isEmpty()) { return message; } StringBuilder sb = new StringBuilder(message); if (!message.endsWith(" ") && !message.isEmpty()) { sb.append(" "); } data.forEach((key, value) -> { sb.append(key).append("="); if (value instanceof String) { sb.append("\"").append(escapeString((String) value)).append("\""); } else { sb.append(value); } sb.append(" "); }); return sb.toString().trim(); } private String escapeString(String value) { return value.replace("\"", "\\\""); } // Fluent API for common contexts public StructuredLogger forUser(String userId) { return withContext("userId", userId); } public StructuredLogger forRequest(String requestId) { return withContext("requestId", requestId); } public StructuredLogger forSession(String sessionId) { return withContext("sessionId", sessionId); } public StructuredLogger forComponent(String component) { return withContext("component", component); } }

JSON Logging Configuration

logback-spring.xml - JSON logging configuration

<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- JSON Layout --> <appender name="JSON" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder"> <layout class="ch.qos.logback.contrib.json.classic.JsonLayout"> <jsonFormatter class="ch.qos.logback.contrib.jackson.JacksonJsonFormatter"> <prettyPrint>false</prettyPrint> </jsonFormatter> <timestampFormat>yyyy-MM-dd'T'HH:mm:ss.SSS'Z'</timestampFormat> <timestampFormatTimezoneId>UTC</timestampFormatTimezoneId> <appendLineSeparator>true</appendLineSeparator> <!-- Custom fields --> <jsonGenerator class="ch.qos.logback.contrib.jackson.JacksonJsonGenerator"> <includeContext>true</includeContext> <includeMDC>true</includeMDC> <includeLevel>true</includeLevel> <includeLoggerName>true</includeLoggerName> <includeMessage>true</includeMessage> <includeException>true</includeException> <includeThreadName>true</includeThreadName> </jsonGenerator> </layout> </encoder> </appender> <!-- Async Appender for Production --> <appender name="ASYNC_JSON" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="JSON" /> <queueSize>10000</queueSize> <discardingThreshold>0</discardingThreshold> <includeCallerData>false</includeCallerData> <neverBlock>true</neverBlock> </appender> <!-- Human-readable for local development --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- Profile-based configuration --> <springProfile name="local | dev"> <root level="INFO"> <appender-ref ref="CONSOLE" /> </root> </springProfile> <springProfile name="staging | production"> <root level="INFO"> <appender-ref ref="ASYNC_JSON" /> </root> <!-- Application-specific loggers --> <logger name="com.yourcompany" level="DEBUG" additivity="false"> <appender-ref ref="ASYNC_JSON" /> </logger> <!-- Reduce noise from third-party libraries --> <logger name="org.hibernate" level="WARN" /> <logger name="org.springframework" level="WARN" /> <logger name="org.apache" level="WARN" /> </springProfile> </configuration>

4. Performance Considerations

Performance-Optimized Logging

PerformanceAwareLogger.java - Avoid performance pitfalls

import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PerformanceAwareLogger { private static final Logger logger = LoggerFactory.getLogger(PerformanceAwareLogger.class); // ❌ BAD: String concatenation happens regardless of log level public void badLogging(User user) { logger.debug("User object: " + user.toString()); // Always executed } // ✅ GOOD: Parameterized logging - only executed if level is enabled public void goodLogging(User user) { logger.debug("User object: {}", user); // Only toString() if DEBUG enabled } // ✅ BETTER: Explicit level checking for expensive operations public void betterLogging(ExpensiveObject expensive) { if (logger.isDebugEnabled()) { logger.debug("Expensive data: {}", expensive.calculateExpensiveValue()); } } // ❌ BAD: Complex operation in log statement public void badComplexLogging(List<User> users) { logger.debug("User count: {}", users.stream().count()); // Always executed } // ✅ GOOD: Move complex operations out of log statements public void goodComplexLogging(List<User> users) { if (logger.isDebugEnabled()) { long count = users.stream().count(); // Only executed if DEBUG enabled logger.debug("User count: {}", count); } } } // Example of expensive object class ExpensiveObject { public String calculateExpensiveValue() { // Simulate expensive operation try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return "expensive result"; } }

Lazy Evaluation Support

LazyLogger.java - Support for lazy message evaluation

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.function.Supplier; public class LazyLogger { private final Logger logger; public LazyLogger(Class<?> clazz) { this.logger = LoggerFactory.getLogger(clazz); } public void debug(Supplier<String> messageSupplier) { if (logger.isDebugEnabled()) { logger.debug(messageSupplier.get()); } } public void debug(Supplier<String> messageSupplier, Supplier<Object[]> argSupplier) { if (logger.isDebugEnabled()) { logger.debug(messageSupplier.get(), argSupplier.get()); } } public void trace(Supplier<String> messageSupplier) { if (logger.isTraceEnabled()) { logger.trace(messageSupplier.get()); } } // Usage example public void processData(DataProcessor processor) { debug(() -> "Processing data: " + processor.getExpensiveDiagnostics()); trace(() -> "Detailed trace: " + processor.getVeryExpensiveTraceInfo()); } }

5. Security and Compliance

Security-Aware Logging

SecureLogger.java - Prevent sensitive data leakage

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.regex.Pattern; public class SecureLogger { private static final Logger logger = LoggerFactory.getLogger(SecureLogger.class); // Patterns for sensitive data private static final Pattern CREDIT_CARD_PATTERN = Pattern.compile("\\b(?:\\d[ -]*?){13,16}\\b"); private static final Pattern SSN_PATTERN = Pattern.compile("\\b\\d{3}-?\\d{2}-?\\d{4}\\b"); private static final Pattern EMAIL_PATTERN = Pattern.compile("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b"); public static String sanitize(String input) { if (input == null) return null; String sanitized = input; sanitized = CREDIT_CARD_PATTERN.matcher(sanitized).replaceAll("[CREDIT_CARD]"); sanitized = SSN_PATTERN.matcher(sanitized).replaceAll("[SSN]"); sanitized = EMAIL_PATTERN.matcher(sanitized).replaceAll("[EMAIL]"); return sanitized; } public static void infoSanitized(String message, Object... args) { Object[] sanitizedArgs = sanitizeArgs(args); logger.info(message, sanitizedArgs); } public static void errorSanitized(String message, Throwable t, Object... args) { Object[] sanitizedArgs = sanitizeArgs(args); logger.error(message, sanitizedArgs); logger.error("Original exception: ", t); // Log stack trace separately } private static Object[] sanitizeArgs(Object[] args) { if (args == null) return null; Object[] sanitized = new Object[args.length]; for (int i = 0; i < args.length; i++) { if (args[i] instanceof String) { sanitized[i] = sanitize((String) args[i]); } else { sanitized[i] = args[i]; } } return sanitized; } } // Secure logging wrapper class SecurityAwareLogger { private final Logger logger; public SecurityAwareLogger(Class<?> clazz) { this.logger = LoggerFactory.getLogger(clazz); } public void logUserAction(User user, String action, String details) { Map<String, Object> context = Map.of( "userId", user.getId(), // OK to log "action", action, "details", SecureLogger.sanitize(details) // Sanitize details ); logger.info("User action performed: {}", context); } public void logPayment(Payment payment) { Map<String, Object> context = Map.of( "paymentId", payment.getId(), "amount", payment.getAmount(), "currency", payment.getCurrency(), "cardLast4", payment.getMaskedCardNumber() // Only log safe parts // Don't log full card number, CVV, etc. ); logger.info("Payment processed: {}", context); } }

6. Configuration Best Practices

Environment-Specific Configuration

LoggingConfig.java - Programmatic configuration

import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.core.util.StatusPrinter; import org.slf4j.LoggerFactory; public class LoggingConfig { public static void initializeLogging(String environment) { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); try { switch (environment.toLowerCase()) { case "production": configureProductionLogging(context); break; case "staging": configureStagingLogging(context); break; case "development": configureDevelopmentLogging(context); break; default: configureDefaultLogging(context); } } catch (Exception e) { System.err.println("Failed to configure logging: " + e.getMessage()); // Fall back to basic configuration } StatusPrinter.printInCaseOfErrorsOrWarnings(context); } private static void configureProductionLogging(LoggerContext context) { // Production: JSON format, async, WARN level for third-party libraries setLoggerLevel("com.yourcompany", Level.INFO); setLoggerLevel("org.springframework", Level.WARN); setLoggerLevel("org.hibernate", Level.WARN); setLoggerLevel("org.apache", Level.WARN); } private static void configureStagingLogging(LoggerContext context) { // Staging: More verbose for debugging setLoggerLevel("com.yourcompany", Level.DEBUG); setLoggerLevel("org.springframework", Level.INFO); } private static void configureDevelopmentLogging(LoggerContext context) { // Development: Maximum verbosity setLoggerLevel("com.yourcompany", Level.TRACE); setLoggerLevel("org.springframework", Level.DEBUG); } private static void setLoggerLevel(String loggerName, Level level) { ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(loggerName); logger.setLevel(level); } }

Log Rotation and Retention

logback-production.xml - Production logging configuration

<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- File appender with rotation --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>100MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <maxHistory>30</maxHistory> <totalSizeCap>3GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- Error-specific appender --> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/error.log</file> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/error.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>90</maxHistory> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- Async appenders for performance --> <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="FILE" /> <queueSize>10000</queueSize> <discardingThreshold>0</discardingThreshold> <includeCallerData>false</includeCallerData> </appender> <appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="ERROR_FILE" /> <queueSize>5000</queueSize> </appender> <root level="INFO"> <appender-ref ref="ASYNC_FILE" /> <appender-ref ref="ASYNC_ERROR" /> </root> </configuration>

7. Testing and Monitoring

Logging Test Utilities

LoggingTestUtils.java - Test your logging

import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import org.slf4j.LoggerFactory; import java.util.List; import java.util.stream.Collectors; public class LoggingTestUtils { public static ListAppender<ILoggingEvent> setupListAppender(Class<?> clazz) { Logger logger = (Logger) LoggerFactory.getLogger(clazz); ListAppender<ILoggingEvent> listAppender = new ListAppender<>(); listAppender.start(); logger.addAppender(listAppender); return listAppender; } public static void cleanupListAppender(Class<?> clazz, ListAppender<ILoggingEvent> appender) { Logger logger = (Logger) LoggerFactory.getLogger(clazz); logger.detachAppender(appender); } public static List<String> getLogMessages(ListAppender<ILoggingEvent> appender) { return appender.list.stream() .map(ILoggingEvent::getFormattedMessage) .collect(Collectors.toList()); } public static boolean containsLogMessage(ListAppender<ILoggingEvent> appender, String expectedMessage) { return appender.list.stream() .anyMatch(event -> event.getFormattedMessage().contains(expectedMessage)); } public static boolean containsLogLevel(ListAppender<ILoggingEvent> appender, ch.qos.logback.classic.Level level) { return appender.list.stream() .anyMatch(event -> event.getLevel().equals(level)); } } // JUnit 5 test example class LoggingTest { private ListAppender<ILoggingEvent> listAppender; @BeforeEach void setUp() { listAppender = LoggingTestUtils.setupListAppender(MyService.class); } @AfterEach void tearDown() { LoggingTestUtils.cleanupListAppender(MyService.class, listAppender); } @Test void shouldLogErrorWhenProcessingFails() { MyService service = new MyService(); try { service.process(null); } catch (Exception e) { // Expected } assertTrue(LoggingTestUtils.containsLogMessage(listAppender, "Failed to process")); assertTrue(LoggingTestUtils.containsLogLevel(listAppender, ch.qos.logback.classic.Level.ERROR)); } }

Log Monitoring and Metrics

LoggingMetrics.java - Monitor logging health

import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; public class LoggingMetrics { private static final Logger logger = LoggerFactory.getLogger(LoggingMetrics.class); private final MeterRegistry meterRegistry; private final ConcurrentHashMap<String, Counter> errorCounters; private final AtomicLong totalLogs = new AtomicLong(); private final Timer logProcessingTimer; public LoggingMetrics(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.errorCounters = new ConcurrentHashMap<>(); this.logProcessingTimer = Timer.builder("log.processing.time") .description("Time spent in logging operations") .register(meterRegistry); initializeMetrics(); } private void initializeMetrics() { // Gauge for total logs meterRegistry.gauge("log.total.count", totalLogs); // Counters for each log level for (ch.qos.logback.classic.Level level : ch.qos.logback.classic.Level.values()) { Counter.builder("log.level.count") .tag("level", level.toString()) .register(meterRegistry); } } public void recordLogEvent(ch.qos.logback.classic.Level level, String loggerName, long durationNanos) { totalLogs.increment(); // Record level counter Counter.builder("log.level.count") .tag("level", level.toString()) .register(meterRegistry) .increment(); // Record processing time logProcessingTimer.record(durationNanos, TimeUnit.NANOSECONDS); // Record error by logger for alerting if (level == ch.qos.logback.classic.Level.ERROR) { String simplifiedLogger = simplifyLoggerName(loggerName); errorCounters.computeIfAbsent(simplifiedLogger, this::createErrorCounter) .increment(); } } private Counter createErrorCounter(String loggerName) { return Counter.builder("log.error.by_logger") .tag("logger", loggerName) .description("Error count by logger name") .register(meterRegistry); } private String simplifyLoggerName(String loggerName) { if (loggerName == null) return "unknown"; // Convert full class name to package-level int lastDot = loggerName.lastIndexOf('.'); if (lastDot > 0) { return loggerName.substring(0, lastDot); } return loggerName; } public long getTotalLogs() { return totalLogs.get(); } public void reset() { totalLogs.set(0); errorCounters.clear(); } }

8. Complete Implementation Example

Comprehensive Logging Setup

ApplicationLogging.java - Complete logging solution

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** * Comprehensive logging utility following best practices */ public final class ApplicationLogging { private static final Map<Class<?>, ApplicationLogger> LOGGERS = new ConcurrentHashMap<>(); private ApplicationLogging() { // Utility class } public static ApplicationLogger getLogger(Class<?> clazz) { return LOGGERS.computeIfAbsent(clazz, ApplicationLogger::new); } public static class ApplicationLogger { private final Logger logger; private final String className; private ApplicationLogger(Class<?> clazz) { this.logger = LoggerFactory.getLogger(clazz); this.className = clazz.getSimpleName(); } // Basic logging methods with context public void info(String message, Object... args) { withContext(() -> logger.info(message, args)); } public void info(String message, Map<String, Object> context) { withContext(context, () -> logger.info("{} - {}", message, formatContext(context))); } public void warn(String message, Object... args) { withContext(() -> logger.warn(message, args)); } public void warn(String message, Throwable throwable, Map<String, Object> context) { withContext(context, () -> logger.warn("{} - {}", message, formatContext(context)), throwable); } public void error(String message, Object... args) { withContext(() -> logger.error(message, args)); } public void error(String message, Throwable throwable, Map<String, Object> context) { withContext(context, () -> logger.error("{} - {}", message, formatContext(context)), throwable); } public void debug(String message, Object... args) { withContext(() -> logger.debug(message, args)); } public void debug(String message, Map<String, Object> context) { withContext(context, () -> logger.debug("{} - {}", message, formatContext(context))); } public void trace(String message, Object... args) { withContext(() -> logger.trace(message, args)); } // Business-specific logging methods public void audit(String action, String resource, String userId, Map<String, Object> details) { Map<String, Object> context = Map.of( "action", action, "resource", resource, "userId", userId, "audit", true, "details", SecureLogger.sanitize(details.toString()) ); info("AUDIT: {} performed on {}", action, resource, context); } public void performance(String operation, long durationMs, Map<String, Object> metrics) { Map<String, Object> context = Map.of( "operation", operation, "durationMs", durationMs, "performance", true ); context.putAll(metrics); if (durationMs > 1000) { // Slow operation threshold warn("Slow operation detected: {} took {}ms", operation, durationMs, context); } else { debug("Operation completed: {} took {}ms", operation, durationMs, context); } } // Request-scoped logging public ApplicationLogger forRequest(String requestId) { return withContext("requestId", requestId); } public ApplicationLogger forUser(String userId) { return withContext("userId", userId); } public ApplicationLogger forSession(String sessionId) { return withContext("sessionId", sessionId); } public ApplicationLogger withContext(String key, Object value) { return new ContextualLogger(this, Map.of(key, value)); } public ApplicationLogger withContext(Map<String, Object> additionalContext) { return new ContextualLogger(this, additionalContext); } // Private helper methods private void withContext(Runnable loggingOperation) { withContext(Map.of(), loggingOperation, null); } private void withContext(Map<String, Object> context, Runnable loggingOperation) { withContext(context, loggingOperation, null); } private void withContext(Map<String, Object> context, Runnable loggingOperation, Throwable throwable) { Map<String, String> previousMDC = MDC.getCopyOfContextMap(); try { MDC.clear(); // Set basic context MDC.put("logger", className); MDC.put("timestamp", String.valueOf(System.currentTimeMillis())); // Set additional context context.forEach((key, value) -> MDC.put(key, String.valueOf(value))); loggingOperation.run(); if (throwable != null) { // Log throwable separately for better formatting logger.error("Exception details: ", throwable); } } finally { MDC.clear(); if (previousMDC != null) { previousMDC.forEach(MDC::put); } } } private String formatContext(Map<String, Object> context) { if (context == null || context.isEmpty()) { return ""; } StringBuilder sb = new StringBuilder(); context.forEach((key, value) -> { if (sb.length() > 0) sb.append(", "); sb.append(key).append("="); if (value instanceof String) { String stringValue = (String) value; if (stringValue.length() > 100) { // Truncate long values sb.append(stringValue, 0, 100).append("..."); } else { sb.append(stringValue); } } else { sb.append(value); } }); return sb.toString(); } } // Contextual logger for fluent API private static class ContextualLogger extends ApplicationLogger { private final ApplicationLogger parent; private final Map<String, Object> context; private ContextualLogger(ApplicationLogger parent, Map<String, Object> context) { super(parent.logger.getClass()); this.parent = parent; this.context = new ConcurrentHashMap<>(context); } @Override public ApplicationLogger withContext(String key, Object value) { Map<String, Object> newContext = new ConcurrentHashMap<>(context); newContext.put(key, value); return new ContextualLogger(parent, newContext); } @Override public ApplicationLogger withContext(Map<String, Object> additionalContext) { Map<String, Object> newContext = new ConcurrentHashMap<>(context); newContext.putAll(additionalContext); return new ContextualLogger(parent, newContext); } @Override public void info(String message, Map<String, Object> context) { Map<String, Object> mergedContext = new ConcurrentHashMap<>(this.context); mergedContext.putAll(context); parent.info(message, mergedContext); } // Override other methods similarly... } } // Usage examples class ServiceExample { private static final ApplicationLogging.ApplicationLogger logger = ApplicationLogging.getLogger(ServiceExample.class); public void processOrder(Order order) { // Create request-scoped logger var requestLogger = logger.forRequest(order.getRequestId()) .forUser(order.getUserId()); requestLogger.info("Starting order processing", Map.of( "orderId", order.getId(), "amount", order.getAmount(), "items", order.getItemCount() )); long startTime = System.currentTimeMillis(); try { // Business logic validateOrder(order); processPayment(order); updateInventory(order); long duration = System.currentTimeMillis() - startTime; requestLogger.performance("order.processing", duration, Map.of( "success", true, "orderValue", order.getAmount() )); requestLogger.info("Order processed successfully"); } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; requestLogger.performance("order.processing", duration, Map.of( "success", false, "error", e.getClass().getSimpleName() )); requestLogger.error("Failed to process order", e, Map.of( "errorType", e.getClass().getName(), "errorMessage", e.getMessage() )); throw e; } } }

Key Best Practices Summary

  1. Use SLF4J as facade and Logback as implementation
  2. Structured Logging with JSON format in production
  3. Parameterized logging - avoid string concatenation
  4. Appropriate log levels - DEBUG for development, INFO for production
  5. Async logging in production for performance
  6. Sensitive data protection - never log passwords, tokens, PII
  7. Contextual logging - include request IDs, user IDs, etc.
  8. Monitoring and alerting on error patterns
  9. Log rotation and retention policies
  10. Testing your logging configuration

This comprehensive approach ensures your logging is performant, secure, and provides the necessary information for debugging and monitoring in production environments.

Leave a Reply

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


Macro Nepal Helper