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