Logging Frameworks

Effective Logging in Java Applications

← Back to Index

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.

Logging Benefits
  • 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

Logging Guidelines
  • 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

Never Log Sensitive Data
  • 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);
}