Why Logging Matters
Logging is essential for understanding application behavior, debugging issues, monitoring performance, and maintaining audit trails. Good logging practices can dramatically reduce time spent troubleshooting production issues.
- Debugging - Understand what happened when errors occur
- Monitoring - Track application health and performance
- Auditing - Record important business events
- Analytics - Analyze usage patterns and trends
- Compliance - Meet regulatory requirements
Java Logging Landscape
Logging APIs vs Implementations
// Logging APIs (Facades) - What you code against
SLF4J // Most popular, recommended
Commons Logging // Legacy, still used
JUL API // java.util.logging (built-in)
// Logging Implementations - What actually logs
Logback // Native SLF4J implementation, recommended
Log4j 2 // Powerful, highly configurable
JUL // java.util.logging (built-in)
// The pattern: Use SLF4J API + Logback implementation
| Framework | Type | Recommendation |
|---|---|---|
| SLF4J + Logback | API + Implementation | Recommended for new projects |
| Log4j 2 | Both | High-performance needs |
| java.util.logging | Built-in | Simple projects, no dependencies |
SLF4J with Logback
SLF4J (Simple Logging Facade for Java) provides a common API, while Logback is its native implementation. This is the most popular combination in modern Java applications.
Maven Dependencies
<!-- 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.14</version>
</dependency>
<!-- Spring Boot: Just include spring-boot-starter-logging -->
<!-- It's included by default in spring-boot-starter -->
Basic Usage
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
// Create logger - conventionally named 'logger' or 'log'
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public User findUser(Long id) {
logger.debug("Finding user with id: {}", id);
try {
User user = userRepository.findById(id);
if (user == null) {
logger.warn("User not found with id: {}", id);
return null;
}
logger.info("Successfully found user: {}", user.getUsername());
return user;
} catch (Exception e) {
logger.error("Error finding user with id: {}", id, e);
throw e;
}
}
}
Logback Configuration (logback.xml)
<!-- src/main/resources/logback.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Console Appender -->
<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>
<!-- File Appender -->
<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}.log</fileNamePattern>
<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>
<!-- Logger for specific package -->
<logger name="com.example" level="DEBUG"/>
<!-- Reduce noise from frameworks -->
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
<!-- Root logger -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
Spring Boot Configuration
# application.properties
# Log levels
logging.level.root=INFO
logging.level.com.example=DEBUG
logging.level.org.springframework.web=WARN
logging.level.org.hibernate.SQL=DEBUG
# Log file
logging.file.name=logs/application.log
logging.file.max-size=10MB
logging.file.max-history=30
# Log pattern
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
Log Levels
Log levels indicate the severity or importance of log messages. Use them appropriately to control verbosity.
| Level | When to Use | Example |
|---|---|---|
| TRACE | Very detailed debugging, method entry/exit | Entering method processOrder() |
| DEBUG | Debugging information for development | Processing order with 5 items |
| INFO | Important business events, normal operation | Order #123 completed successfully |
| WARN | Potential problems, recoverable issues | Retry attempt 2 of 3 for payment |
| ERROR | Errors that need attention but app continues | Failed to process payment for order #123 |
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
public Order processOrder(Order order) {
log.trace("Entering processOrder with order: {}", order);
log.debug("Processing order {} with {} items",
order.getId(), order.getItems().size());
try {
validateOrder(order);
log.info("Order {} validated successfully", order.getId());
if (order.getTotal() > 10000) {
log.warn("Large order {} requires manual review", order.getId());
}
completeOrder(order);
log.info("Order {} completed, total: ${}", order.getId(), order.getTotal());
} catch (PaymentException e) {
log.error("Payment failed for order {}: {}", order.getId(), e.getMessage(), e);
throw e;
}
log.trace("Exiting processOrder");
return order;
}
}
Logging Best Practices
- Use parameterized logging - Avoid string concatenation
- Include context - Add IDs, user info, transaction details
- Don't log sensitive data - Passwords, credit cards, PII
- Be consistent - Use same format across application
- Log at appropriate level - Don't use ERROR for normal flow
Parameterized Logging
// BAD - String concatenation (always evaluates)
logger.debug("Processing user: " + user.getName() + " with id: " + user.getId());
// GOOD - Parameterized (only evaluates if debug enabled)
logger.debug("Processing user: {} with id: {}", user.getName(), user.getId());
// For expensive operations, check level first
if (logger.isDebugEnabled()) {
logger.debug("Order details: {}", order.toDetailedString());
}
Exception Logging
// BAD - Loses stack trace
logger.error("Error: " + e.getMessage());
// BAD - Redundant message
logger.error("Exception occurred: " + e.toString(), e);
// GOOD - Context + full stack trace
logger.error("Failed to process order {}", orderId, e);
// GOOD - With additional context
logger.error("Failed to process order {} for user {}: {}",
orderId, userId, e.getMessage(), e);
Structured Logging (JSON)
<!-- logback.xml for JSON output -->
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.4</version>
</dependency>
<!-- In logback.xml -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
// Output example:
{
"@timestamp": "2024-01-15T10:30:45.123Z",
"level": "INFO",
"logger_name": "com.example.OrderService",
"message": "Order completed",
"orderId": "12345",
"userId": "user-789"
}
MDC - Mapped Diagnostic Context
MDC allows you to add contextual information to all log messages within a thread, perfect for tracking request IDs, user sessions, etc.
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 for all logs in this request
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", getCurrentUserId());
MDC.put("clientIP", request.getRemoteAddr());
chain.doFilter(request, response);
} finally {
// Always clear to prevent memory leaks
MDC.clear();
}
}
}
// In logback.xml, include MDC values in pattern
<pattern>%d{HH:mm:ss} [%X{requestId}] [%X{userId}] %-5level %logger{36} - %msg%n</pattern>
// Log output automatically includes context:
// 10:30:45 [abc-123] [user-456] INFO c.e.OrderService - Processing order
Log4j 2
Log4j 2 is a powerful alternative with excellent performance, especially for asynchronous logging.
Maven Dependencies
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.22.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.22.0</version>
</dependency>
<!-- Bridge SLF4J to Log4j 2 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.22.0</version>
</dependency>
Log4j 2 Configuration (log4j2.xml)
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<!-- Async file appender for performance -->
<RollingRandomAccessFile name="File"
fileName="logs/app.log"
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<AsyncLogger name="com.example" level="debug"/>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
Lombok @Slf4j
Lombok can automatically generate the logger field, reducing boilerplate.
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserService {
// No need to declare logger manually!
// Lombok generates: private static final Logger log = LoggerFactory.getLogger(UserService.class);
public User findUser(Long id) {
log.info("Finding user: {}", id);
// ...
}
}
// Other Lombok logging annotations:
@Log // java.util.logging
@Log4j2 // Log4j 2
@Slf4j // SLF4J (recommended)
@CommonsLog // Apache Commons Logging
Security Considerations
- Passwords and credentials
- Credit card numbers
- Social security numbers
- API keys and tokens
- Personal health information
// BAD - Logging sensitive data
logger.info("User login: {} with password: {}", username, password);
logger.debug("Payment with card: {}", creditCardNumber);
// GOOD - Mask or omit sensitive data
logger.info("User login attempt: {}", username);
logger.debug("Payment processed for card ending in: {}",
maskCardNumber(creditCardNumber));
private String maskCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 4) {
return "****";
}
return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
}