Logging with SLF4J and Logback
Logging is essential for monitoring, debugging, and auditing applications. Proper logging helps you understand what your application is doing without attaching a debugger.
Logging Frameworks
| Framework | Description |
|---|---|
| SLF4J | Facade/API - use this in your code |
| Logback | Implementation - fast, feature-rich (recommended) |
| Log4j2 | Implementation - async logging, plugins |
| java.util.logging | Built-in JDK logging (limited features) |
Best Practice: Always code against SLF4J (the facade), then choose
an implementation (Logback or Log4j2) at deployment time. This allows switching
implementations without changing code.
Setup (Maven)
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<!-- Logback implementation -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
Basic Usage
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
// Create logger for this class
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public User findById(Long id) {
logger.debug("Looking up user with id: {}", id);
User user = userRepository.findById(id);
if (user == null) {
logger.warn("User not found: {}", id);
return null;
}
logger.info("Found user: {}", user.getEmail());
return user;
}
public void processPayment(Order order) {
logger.info("Processing payment for order: {}", order.getId());
try {
paymentGateway.charge(order);
logger.info("Payment successful for order: {}", order.getId());
} catch (PaymentException e) {
logger.error("Payment failed for order: {}", order.getId(), e);
throw e;
}
}
}
Log Levels
| Level | When to Use | Example |
|---|---|---|
ERROR |
Application errors, requires attention | Database connection failed |
WARN |
Potential problems, recoverable | Retry attempt, deprecated API used |
INFO |
Important business events | User registered, order placed |
DEBUG |
Detailed flow information | Method entry/exit, variable values |
TRACE |
Most detailed, rarely used | Loop iterations, low-level details |
Parameterized Logging
// BAD: String concatenation (always evaluated)
logger.debug("Processing user: " + user.getName() + " with id: " + user.getId());
// GOOD: Parameterized (only evaluated if level is enabled)
logger.debug("Processing user: {} with id: {}", user.getName(), user.getId());
// With exception (exception is always the last argument)
logger.error("Failed to process user: {}", userId, exception);
Logback Configuration
Create src/main/resources/logback.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Console output -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- File output with rotation -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Set log levels for specific packages -->
<logger name="com.example.myapp" level="DEBUG" />
<logger name="org.springframework" level="INFO" />
<logger name="org.hibernate.SQL" level="DEBUG" />
<!-- Root logger -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
MDC (Mapped Diagnostic Context)
Add contextual information to all log messages in a thread:
import org.slf4j.MDC;
public class RequestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
try {
// Add context that appears in all logs for this request
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", getCurrentUserId());
chain.doFilter(request, response);
} finally {
MDC.clear(); // Always clean up!
}
}
}
// In logback.xml, use %X{key} to include MDC values:
// <pattern>%d [%X{requestId}] [%X{userId}] %level %logger - %msg%n</pattern>
// Output: 2024-01-15 10:30:45 [abc-123] [user42] INFO UserService - User logged in
Logging Best Practices
What to Log
- Application startup/shutdown
- Configuration values (not secrets!)
- Business events (orders, registrations)
- Errors with full stack traces
- Integration points (API calls, DB queries)
What NOT to Log
- Passwords, tokens, API keys
- Credit card numbers, SSNs
- Personal data (GDPR compliance)
- Large data structures (use DEBUG level)
// BAD: Logging sensitive data
logger.info("User login: {} with password: {}", username, password);
// GOOD: Mask sensitive data
logger.info("User login: {}", username);
// BAD: Logging large objects at INFO
logger.info("Response: {}", hugeJsonResponse);
// GOOD: Log at DEBUG, or log summary
logger.debug("Full response: {}", hugeJsonResponse);
logger.info("Received {} items", response.getItems().size());
Performance Tips
// Check level before expensive operations
if (logger.isDebugEnabled()) {
logger.debug("Expensive computation result: {}", computeExpensiveValue());
}
// Use lazy evaluation (SLF4J 2.0+)
logger.atDebug()
.addArgument(() -> expensiveComputation())
.log("Result: {}");
Summary
- Use SLF4J facade with Logback implementation
- Use parameterized logging (not string concatenation)
- Choose appropriate log levels for different messages
- Configure file rotation to manage disk space
- Use MDC for request tracking across logs
- Never log passwords, tokens, or PII
Enjoying these tutorials?