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.