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
- Choosing the Right Logging Framework
- Logging Levels and When to Use Them
- Structured Logging
- Performance Considerations
- Security and Compliance
- Configuration Best Practices
- Testing and Monitoring
- 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
- Use SLF4J as facade and Logback as implementation
- Structured Logging with JSON format in production
- Parameterized logging - avoid string concatenation
- Appropriate log levels - DEBUG for development, INFO for production
- Async logging in production for performance
- Sensitive data protection - never log passwords, tokens, PII
- Contextual logging - include request IDs, user IDs, etc.
- Monitoring and alerting on error patterns
- Log rotation and retention policies
- 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.