Friends of OpenJDK Today

Effective Java Logging

July 22, 2024

Author(s)

  • Muaath Bin Ali

    Muaath Ali is a principal software engineer with 15+ years of Java experience. He runs MezoCode blog, helping Java developers design flexible and robust systems.

Effective Logging is an essential aspect of any Java application, providing insights into its operational state. It is especially crucial in production environments, where it aids in debugging, monitoring, and incident response. In this comprehensive guide, we will explore the effective practices for using SLF4J with Logback, ensuring a reliable and maintainable logging strategy.

By following these best practices, developers and operations teams can leverage SLF4J and Logback to turn logs into strategic resources for application management and incident resolution. Embracing these guidelines will lead to improved observability, quicker troubleshooting, and a deeper understanding of system behavior, establishing a solid foundation for application reliability and performance.

Key Benefits of Effective Logging

  • Improved observability: Logs provide a detailed record of application behavior, making it easier to understand how the system is operating and identify potential issues.
  • Faster troubleshooting: Well-structured and informative logs enable developers to quickly pinpoint the root cause of problems and resolve them efficiently.
  • Enhanced incident response: Logs are invaluable during incident response, providing a chronological account of events leading up to and during an issue.
  • Compliance and security: Logs can serve as evidence of compliance with regulations and help identify security breaches or suspicious activities.

Choosing SLF4J and Logback

SLF4J (Simple Logging Facade for Java) is a popular logging facade that provides a consistent API for logging across different logging frameworks. Logback is a widely used logging framework that offers a rich set of features and customization options. By combining SLF4J with Logback, you can benefit from the flexibility and power of both tools.

In this guide, we will cover 14 essential best practices for using SLF4J and Logback effectively in your Java applications. These practices will help you achieve reliable, maintainable, and informative logging that supports your application's operational needs.

1. Use SLF4J as the Logging Facade


🟢 Good Practice:

Choose SLF4J as your application's logging facade to decouple your logging architecture from the underlying logging library implementation. This abstraction allows you to switch between different logging frameworks without major code changes.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyClass {
    private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
    // ...
}

🔴 Avoid Practice:

Hardcoding a specific logging framework implementation in your application code can lead to difficulties when needing to switch libraries.

import org.apache.log4j.Logger;

public class MyClass {
private static final Logger logger = Logger.getLogger(MyClass.class);
// ...
}
2. Configure Logback for Efficient Logging


🟢 Good Practice:

Externalize your Logback configuration and use PatternLayout for improved performance and flexibility. Define different configurations for development, staging, and production environments to better manage the verbosity and detail of logs.

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

🔴 Avoid Practice:

Using an outdated or non-performant layout class and hardcoding configuration settings in the code can make it difficult to adapt to different environments.

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <!-- Non-recommended layout configuration -->
        </layout>
    </appender>
    <!-- ... -->
</configuration>
3. Use Appropriate Log Levels


🟢 Good Practice:

Log at the correct level to convey the importance and intention of the message. Use INFO for general events, DEBUG for detailed information during development, and ERROR for serious issues that need attention.

logger.info("Application has started.");
logger.debug("The value of X is {}", x);
logger.error("Unable to process the request.", e);

🔴 Avoid Practice:

Logging everything at the same level, can overwhelm the log files with noise and make it difficult to spot critical issues.

logger.error("Application has started."); // Incorrect use of log level
logger.error("The value of X is " + x); // Inefficient string concatenation
// ...
4. Log Meaningful Messages


🟢 Good Practice:

Include relevant information such as transaction or correlation IDs in your log messages to provide context. This is especially helpful in distributed systems for tracing requests across services.

logger.info("Order {} has been processed successfully.", orderId);

🔴 Avoid Practice:

Vague or generic log messages that do not provide sufficient context to understand the event or issue.

logger.info("Processed successfully."); // No context provided
5. Use Placeholders for Dynamic Content

🟢 Good Practice:

Utilize placeholders to 🔴 Avoid Practice: unnecessary string concatenation when the log level is disabled, saving memory and CPU cycles.

logger.debug("User {} logged in at {}", username, LocalDateTime.now());

🔴 Avoid Practice:

Concatenating strings within log statements is less efficient.

logger.debug("User " + username + " logged in at " + LocalDateTime.now());
6. Log Exceptions with Stack Traces


🟢 Good Practice:

Always log the full exception, including the stack trace, to provide maximum context for diagnosing issues.

try {
// some code that throws an exception
} catch (Exception e) {
logger.error("An unexpected error occurred", e);
}

🔴 Avoid Practice:

Logging only the exception message without the stack trace can omit critical diagnostic information.

try {
// some code that throws an exception
} catch (Exception e) {
logger.error("An unexpected error occurred: " + e.getMessage());
}
7. Use Asynchronous Logging for Performance


🟢 Good Practice:

Implement asynchronous logging to improve application performance by offloading logging activities to a separate thread.

<configuration>
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE" />
    </appender>

    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>application.log</file>
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="ASYNC" />
    </root>
</configuration>

🔴 Avoid Practice:

Synchronous logging in performance-critical paths without considering the potential for log-related latency.

logger.info("A time-sensitive operation has completed.");
8. Log at the Appropriate Granularity


🟢 Good Practice:

You should balance between logging too much and too little. Log at the appropriate granularity based on the specific requirements of your application. Avoid excessive logging that clutters the logs and makes it difficult to identify important information.

public void processOrder(Order order) {

    logger.info("Processing order: {}", order.getId());

    // Logging at a finer granularity for debugging purposes
    logger.debug("Order details: {}", order);

    // Process the order
    orderService.save(order);

    logger.info("Order processed successfully");
}

🔴 Avoid Practice:

Excessive logging at a high granularity in production, can lead to performance issues and log flooding.

public void processOrder(Order order) {

    logger.trace("Entering processOrder method");
    logger.debug("Received order: {}", order);
    logger.info("Processing order: {}", order.getId());

    // Logging every step of order processing
    logger.debug("Step 1: Validating order");
    // ...
    logger.debug("Step 2: Calculating total amount");
    // ...
    logger.debug("Step 3: Updating inventory");
    // ...

    logger.info("Order processed successfully");
    logger.trace("Exiting processOrder method");
}
9. Monitor and Rotate Log Files


🟢 Good Practice:

Configure log file rotation based on size or time to prevent logs from consuming excessive disk space. Set up monitoring for log files to trigger alerts when nearing capacity.

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>logs/myapp-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
        <maxHistory>30</maxHistory>
        <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
            <maxFileSize>100MB</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
    </rollingPolicy>
    <!-- ... -->
</appender>

🔴 Avoid Practice:
Letting log files grow indefinitely, can lead to disk space exhaustion and potential system failure.

10. Secure Sensitive Information


🟢 Good Practice:

Implement filters or custom converters in your logging framework to redact or hash sensitive data before it's written to the logs.

log.info("Processing payment with card: {}", maskCreditCard(creditCardNumber));

public String maskCreditCard(String creditCardNumber) {
    int length = creditCardNumber.length();
    if (length < 4) return "Invalid number";
    return "****-****-****-" + creditCardNumber.substring(length - 4);
}


🔴 Avoid Practice:

Logging sensitive information such as passwords, API keys, Credit Cards, or personally identifiable information (PII).

log.info("Processing payment with card: {}", creditCardNumber);
11. Structured Logging


🟢 Good Practice:

Adopt structured logging to output logs in a machine-readable format like JSON, facilitating better searching and indexing in log management systems.

<configuration>
    <appender name="JSON_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp>
                    <timeZone>UTC</timeZone>
                </timestamp>
                <version />
                <logLevel />
                <threadName />
                <loggerName />
                <message />
                <context />
                <stackTrace />
            </providers>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="JSON_CONSOLE" />
    </root>
</configuration>

Let's take a look at an example log message that is printed in JSON format:

logger.info("Order has been processed");

The output of the above log message will be printed as below:

{"@timestamp":"2024-03-26T15:52:00.789Z","@version":"1","message":"Order has been processed","logger_name":"Application","thread_name":"main","level":"INFO"}


🔴 Avoid Practice:

Using unstructured log formats that are difficult to parse and analyze programmatically.

12. Integration with Monitoring Tools

🟢 Good Practice:

Link your logging with monitoring and alerting tools to automatically detect anomalies and notify the concerned teams.

🔴 Avoid Practice:
Ignoring the integration of logs with monitoring systems can delay the detection of issues.

13. Log Aggregation

🟢 Good Practice:

In distributed environments, use centralized log aggregation to collect logs from multiple services, simplifying analysis and correlation of events.

🔴 Avoid Practice:

Allowing logs to remain scattered across various systems, complicates the troubleshooting process.

14. Smart Logging

We have great content here for implementing Smart Logging using AOP.

References
Conclusion

Effective logging is not just about capturing data; it's about capturing the right data at the right time and in the right format. By implementing these best practices, developers and operations teams can leverage SLF4J and Logback to turn logs into strategic resources for application management and incident resolution.

Embracing these guidelines will lead to improved observability, quicker troubleshooting, and a deeper understanding of system behavior, establishing a solid foundation for application reliability and performance.

Free Webinar: Unraveling Insights from The Java Performance Benchmark Report

Join us for a conversation about how to improve Java application performance! November 19th, 9am PT (11am CT | 12pm ET | 6pm CET)

Sign Up!

Topics:

Related Articles

View All

Author(s)

  • Muaath Bin Ali

    Muaath Ali is a principal software engineer with 15+ years of Java experience. He runs MezoCode blog, helping Java developers design flexible and robust systems.

Comments (2)

Your email address will not be published. Required fields are marked *

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Java Weekly, Issue 552 | Baeldung

[…] >> Effective Java Logging [foojay.io] […]

Will S.

I have to strongly disagree with you on logging of the exception stack trace. I have been working in Java since 1.0.7 back in 1996. One of the worst habits in a production application, especially at scale, is logging the full stack dump. Writing good messages to the log that are unique and provide non-private, contextual information is what you need without all the stack dump noise. If you are implementing better practices and logging the exception as close to the point of occurrence as possible and you have a unique message for every exception you won’t need the stack.

To number 4 I would add the word “unique” to have it be “Log Unique Meaningful Messages”. Troubleshooting applications where every message is “Exception happened” makes it harder to track down the location of the failure. If this is the habit then the only way is to have the exception stack trace but if you have unique messages you can go straight to the code location.

As you state, ensuring the proper level of severity is used when writing a log message and using good tooling to pick up on true errors that indicate broken function is essential. Then notifying the support team when an error message is written gets to problem resolution rapidly.

I have worked on many systems running at 10’s of thousands of simultaneous users/operations without ever having to dump a stack frame to debug an issue.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard