Best Practices in Java Logging for Better Application Logging

Examining Java logs is usually the quickest way to figure out why your application is experiencing trouble, so it's critical to have it in place. Best practices for Java logging can help you troubleshoot and address issues before they affect your users or business. In many circumstances, this entails utilizing a Java logging tool capable of automating your processes and delivering faster and more accurate results than manual logging.

There are some differences in logging between monolithic architectures and current microservice architectures. Microservices' distributed nature requires a rise in the amount of log data required, making serialization and centralization of logs more difficult than with monoliths.

With these concerns in mind, we'll look at the best practices for Java logging and explain why they're important.

We will go over the following:

  1. Introduction
  2. Java Logging Framework
  3. Components of Java Logging
  4. SLF4J Abstractions
  5. Setting Up
  6. Add Log4j in SLF4J Interface
  7. Simple Java Application for Java Logging
  8. Add Context to Your Logs
  9. Making Queryable Logs
  10. Five Simple Tips for Java Logging

Introduction

Every Java application will require logging at some point. When working on an application that isn't running on your desktop, logging messages are frequently your sole way of rapidly figuring out why something isn't working properly.

  • It's possible that you merely want to log the state of your system or user actions to a file so that your operations team knows what's going on.
  • It's possible that every time an exception occurs, you'll need to capture error messages and subsequently send an e-mail or text message to a user for immediate assistance.
  • Likewise, one of your batch operations might want to log and send warnings to a central, GUI-based log server whenever a CSV file import fails.

Whatever you intend to accomplish, you must first ensure that you have a suitable logging library, which you must then configure and utilize effectively.

Sure, APM tools can warn you about memory leaks and performance bottlenecks, but they rarely provide enough data to help you fix a specific problem, such as why this person can't log in or why this record isn't being processed.

Unfortunately, there are numerous logging libraries accessible in the Java ecosystem, and a developer should have a basic understanding of why there are so many options and when to utilize which one.

Java Logging Framework

Java logging involves the use of one or more logging frameworks. The objects, methods, and configuration required to create and transmit log messages are provided by these frameworks. The java.util.logging package includes a built-in framework for logging.

Log4j, Logback, and tinylog are just a few of the third-party frameworks available. You can also use an abstraction layer like SLF4J or Apache Commons Logging to isolate your code from the underlying logging framework, allowing you to switch between them on the fly.

The features available, the complexity of your logging needs, the convenience of use, and personal preference all play a role in deciding on a logging solution. Compatibility with other projects is another thing to consider.

Although you can send logs to a different framework, Apache Tomcat is hard-coded to use java.util.logging. When selecting a framework, take into account your environment and dependencies.

Log4j is a solid choice for most developers since it provides good performance, is extremely flexible, and has a vibrant development community. For the best compatibility, use SLF4J with the Log4j binding if you plan on integrating other Java libraries or applications into your own.

Components of Java Logging

Logging in Java is very customizable and extendable. While the java.util.logging package provides a basic logging API, you may easily utilize one or more different logging solutions instead. These systems use various ways for generating log data, but they all follow the same basic pattern.

The three main components of the Java logging API are:

  1. Loggers are in charge of recording events (known as LogRecords) and passing them on to the relevant Appender.
  2. Appenders (also known as Handlers in some logging frameworks) are in charge of writing log events to a specified location. Before sending events to an output, Appenders employ Layouts to format them.
  3. Layouts (also known as Formatters in some logging frameworks) convert and format data in log events. The appearance of data when it appears in a log entry is determined by layouts.

The Logger records the event in a LogRecord and transmits it to the relevant Appender when your application performs a logging call. After that, the Appender uses a Layout to format the record before sending it to a destination like a console, a file, or another application.

You may also define which Appenders to be utilized for which events by using one or more Filters. Filters aren't necessary, but they do provide you more control over how your log messages flow.

SLF4J Abstractions

SLF4J helps you reduce the dependency between your application and your logging framework by offering a logging API for injecting any runtime Java logging framework. We lessen dependencies between components by moving to a more decoupled approach, providing us more flexibility when it comes to maintaining our application.

To better explain my perspective, consider the following relationship that SLF4J will have in our sample application:

Because of how SLF4J fits into our application architecture, the abstraction it provides should be evident. As a result, we can switch to any logging framework at any time throughout the runtime without being stymied by a single dependence.

If we had chosen a framework rather than a logging abstraction like SLF4J from the start, we would have been restricted to that framework barring code changes.

Without further ado, let's dive into some real-world examples of how SLF4J works, and we'll start to see some of the benefits it provides.

Setting Up

We are going to create a simple maven application. We won't go into great detail because there are thousands of tutorials available online. You'll require a simple application with a ready-to-use pom.xml.

Once you have this, you should update your pom file to include the SLF4J API and SLF4J Simple dependencies. After that, make a java file called JavaLogging.java in the package folder. Copy and paste the code below:

package com.acme.bestpractices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JavaLogging {
    public static void main(String[] args) {
        // Creating our custom Logger
        Logger logger = LoggerFactory.getLogger("FirstLogger");

        // Some simple outputs
        logger.info("Simply an update");
        logger.error("Example of an error");
    }
}

If we execute the file, we should see something like this:

[main] INFO FirstLogger - Simply an update
[main] ERROR FirstLogger - Example of an error

We're using the severity levels 'INFO' and 'ERROR' here, because SLF4J has varying severity levels depending on the sort of message you want to log.

Add Log4j in SLF4J Interface

We've seen a simple example of logging with SLF4J. We can now move on to adding Log4j. Log4j is a lightweight logging framework that allows for thread-safe behavior configuration at runtime. We can smoothly configure our logging utility at runtime by using Log4j beneath the SLF4J abstraction hood, should we choose to switch to a different logging framework, for example.

To use Log4j and SLF4J together, we must first download the binding. Remove the SLF4J-simple dependency from your POM and replace it with the code below:

<dependency>
  <groupId> org.slf4j </groupId>
  <artifactId> slf4j-log4j12 </artifactId>
  <version> 1.7.30 </version>
  <scope> test </scope>
</dependency>

We'll also need to include Log4j as a dependency. We currently have the adapter to make it work with SLF4J, but we also require the jar file for the dependence. To accomplish so, include the following code in your POM file:

<dependency>
  <groupId> log4j </groupId>
  <artifactId> log4j </artifactId>
  <version> 2.17.1 </version>
</dependency>

The use of Log4j's configuration file will assist us in achieving the best practices. There is no need to manually alter the massive quantities of logging statements that unavoidably accumulate within any application while using Log4j.

Cutting the amount of time spent manually updating line by line is a no-brainer when it comes to logging. For the time being, we'll leave JavaLogging.java alone. Create the following structure in the src directory:

src/main/resources

Create a file called log4j.properties in /resources. This will maintain our logging framework's runtime configuration. To log4j.properties, add the following:

log4j.rootLogger = DEBUG, A1
log4j.appender.A1 = org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout = org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern = % d[ % t] % -5 p % c - % m % n

From the Log4j manual, these are all the default beginning parameters. We need to make sure Eclipse knows to include this file in the classpath before rerunning the program.

Select Run -> Run Configurations -> Dependencies -> Classpath Entries ->   On the right, select Advanced -> Add Folders. Add the resource folder in src/main/resources.

If we run the file, we should get the following results:

2021-01-24 00:56:18,256 [main] INFO  FirstLogger - Simply an update
2021-01-24 00:56:18,256 [main] ERROR FirstLogger - Example of an error

The only difference is that timestamp has been added on the left. We haven't touched the main source code yet, as you can see. At runtime, we configure and use our formatting.

Simple Java Application for Java Logging

We can put our logger to good use now that we have it set up. We'll need something a little more complicated than the simple Java project we're using to adopt best practices. This section of the article can be skipped if you already have a Java project in which you want to implement this logging method.

We'll only make a simple input-based application that lets a user look for available days of the week for an emergency doctor appointment. This will allow us to flesh out our logger while also avoiding any additional dependencies required for a REST API.

The program will prompt the user to select a doctor and a time when they are available for an appointment. We'll use our logger wherever it's needed to demonstrate excellent practices.

JavaLogging.java should be replaced with the following:

package bestPractices;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Scanner;
public class JavaLogging {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger("FirstLogger");
        System.out.print("Which doctor do you prefer?\n");
        Scanner in = new Scanner(System.in);
        String doctorName = in .next();
        String[] doctor = {
            "Joe",
            "Helen",
            "Chandler",
            "John"
        };
        String[] days = {
            "Monday",
            "Wednesday",
            "Thursday",
            "Saturday"
        };
        boolean doctorFound = false;
        boolean dayFound = false;
        for (String s: doctor) {
            if (s.equals(doctorName)) {
                System.out.print("Found the doctor!\n");
                logger.info("Doctor found: " + doctorName);
                doctorFound = true;
                break;
            }
        }
        if (doctorFound) {
            System.out.println("When do you want to schedule your appointment?");
            String dayPicked = in .next();
            for (String p: days) {
                if (p.equals(dayPicked)) {
                    System.out.println("You are booked for " + dayPicked);
                    logger.info("Appointment booked on " + dayPicked);
                    dayFound = true;
                    System.exit(0);
                }
            }
            if (dayFound == false) {
                logger.error("Sorry, we are not available on that day.");
                logger.info("Exiting application.");
                System.exit(0);
            }
        } else {
            logger.error("Invalid doctor name");
        }
        logger.info("Exiting application.");
        System.exit(0);
    }
}

JavaLogging.java now has a simple user input flow that asks the following questions:

  • The doctor's name that they want to see.
  • Choose the day they wish to arrange their appointment from the available days in the days’ array.

In several of the input validation checks, we've also added some log statements. Let's run the program with the same parameters as before: Wednesday, Joe.

On the console, we should now see the following:

Which doctor do you prefer?
INPUT: Joe
Found the doctor!
2021-01-24 03:35:57, 006 INFO[main](JavaLogging.java: 28) - Doctor found: Joe
When do you want to schedule your appointment?
INPUT: Wednesday
You are booked for Wednesday
2021-01-24 03:36:45, 009 INFO[main](JavaLogging.java: 65) - Appointment booked on Wednesday

Our logger is functioning well, and we have log updates organized by severity, as well as the timestamp and line of code where the update was performed. We aim to make debugging and maintenance as simple as possible, therefore this is an important first step. If an error occurs, we will know what caused it and why it occurred.

Add Context to Your Logs

We're going to use a really basic example here. However, it does resemble a user interaction with a REST API, therefore logging user metadata like sessionID would be useful.

Mapped Diagnostic Context, or MDC, is a feature of Log4j that allows us to log information that isn't always in the scope of where the logging is taking place. It would be incredibly difficult to distinguish between two almost similar requests, or requests made by the same individual over a lengthy period of time if hundreds of users used our hypothetical service to make appointments and check availability. This is where MDC comes in to make things a lot easier.

Modify the following line in the log4j.properties file:

log4j.appender.stdout.layout.ConversionPattern = % d % 5 p[ % t]( % F: % L) - % m - % X {
    sessionId
} % n

We're going to use %X{sessionId} to attach the sessionId to every log instance.

Go to JavaLogging.java and import the MDC dependency:

import org.slf4j.MDC;

This should go right after public static void main(String[] args).

String sessionId = "123456";
MDC.put("sessionId", sessionId);

We wouldn't manually generate a sessionId in a real-world scenario. To test out a different console output, run the application again with the following inputs: Sunday, Joe.

Which doctor do you prefer?
INPUT: Joe
Found the doctor!
2021-01-24 03:57:57,100 INFO [main] (JavaLogging.java:31) - Dcotor found: Joe - 123456
When do you want to schedule your appointment?
INPUT: Sunday
2021-01-24 03:58:02,767 ERROR [main] (JavaLogging.java:75) - Sorry, we are not available on that day. - 123456
2021-01-24 03:58:02,767 INFO [main] (JavaLogging.java:76) - Exiting application. - 123456

We've successfully associated our sessionId to every log object. When it comes to debugging, this will come in handy. Instead of wasting time looking for that one failure that caused your entire application to crash, it's all right there in a human-readable format.

We've only logged into the console thus far. If you want to save the logged outputs to a file, the official documentation includes simple configuration files. To your log4j.properties file, make the following changes:

log4j.rootLogger = INFO, file
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File = log / logging.log
log4j.appender.file.MaxFileSize = 10 MB
log4j.appender.file.MaxBackupIndex = 10
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern = % d % 5 p[ % t]( % F: % L) - % m - % X {
    sessionId
} % n

If we run the program again with the same inputs, we should see a new folder called log in our root directory, which contains logging.log. The console's outputs should now be limited to the application's user input requests.

Making Queryable Logs

Let's add more value to our logs now that we have a log file to which we're delivering our log data. It's a good idea to make sure all of our logged outputs are in JSON format. This puts a premium on searching through log files, categorizing data, and ensuring readability. If you want to keep control over your codebase, you'll need to do all of these things.

We could usually utilize Logback to convert our logged outputs to JSON objects. There is a lot of documentation on how to do this with Logback, which you can find here.  

We can include a custom JSON formatter in our log4j.properties file in our application. Modify the file as follows:

log4j.rootLogger = INFO, file
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File = log / logging.log
log4j.appender.file.MaxFileSize = 10 MB
log4j.appender.file.MaxBackupIndex = 10
log4j.appender.file.layout = org.apache.log4j.PatternLayout
log4j.appender.file.encoding = UTF - 8
log4j.appender.file.layout.ConversionPattern = {
        "timestamp": "%d",
        "logUpdate": "%5p [%t] (%F:%L)",
        "status": "%m",
        "sessionID": "%X{sessionId}}%n"

Let's put our program to the test using the following parameters: Sunday, Joe

"{"timestamp":"2021-01-24 05:43:02,041","logUpdate": " INFO [main] (JavaLogging.java:31)","status":"Doctor found: Joe","sessionID", "123456}
"{"timestamp":"2021-01-24 05:43:05,578","logUpdate": "ERROR [main] (JavaLogging.java:75)","status":"Sorry, we are not available on that day.","sessionID" : "123456}
"{"timestamp":"2021-01-24 05:43:05,579","logUpdate": " INFO [main] (JavaLogging.java:76)","status":"Exiting application.","sessionID" :"123456"}

Our logged outputs are stored as JSON objects. This allows us to simply search our log file using keywords, as well as filter for certain numbers. We may now quickly export our JSON log to another location for a variety of reasons. These could include data analytics to determine when system failures are most likely to occur, data visualizations to indicate trends, or even dashboard-style log analysis.

Five Simple Tips for Java Logging

Overall, the ideal practice for Java logging is to log as much as possible while yet giving simple access to the important information included inside the logs. Of course, this is easier said than done, which is why you should follow a few best practices to achieve your broader objectives. The following five tips are included in these best practices:

  1. Severity Levels
    Create and configure log severity levels to make your Java logs easier to read and ensure you don't miss any important logs. Though it's beneficial to gather as much information as possible in your logs, it's also critical to avoid losing sight of the logs that can indicate application difficulties simply because they're not separated from less relevant logs.
  2. Optimize Log Data
    Write your logs with the expectation that they will be read by others, and make sure they are clear and easy to digest. This could entail printing two logs: one for computers and one for humans to read.
  3. Add Metadata
    Include metadata in your logs to help you find production issues faster. The more metadata you include in your log, the more useful it will be. When an issue occurs, metadata such as class, category, method, and thread ID can assist you to pinpoint the root cause.
  4. Log Size
    Write logs that aren't too long. By masking the data you need, you can lower the value of a log by incorporating unrelated or superfluous information. When writing your logs to disc, you can also cause performance or bandwidth issues. Although detailed, descriptive logs are useful, they should not be used to store unrelated data.
  5. Proper Log Exceptions
    Check to see if you're accurately logging exceptions and not reporting them several times. Allowing your log monitoring system to take care of reporting exceptions for you is the best way to go. An automated tool can even generate warnings based on specific categories of log data, ensuring that you get more precise insights into pressing issues.

Wrapping It!!!

The ability to successfully gather logs and extract relevant information through Java logging is critical to the success of any Java application. This information gives you information on the application's usability, stability, and performance so you can keep it running smoothly.

Gathering log data that provides insight into the application in question is the first step in successful troubleshooting. You can extract and analyze this data for more insight into a problem if an anomaly or performance issue develops. With this information, you can more quickly pinpoint the root of the problem and take steps to resolve it.

You can use tools like Atatus, which is built to store log data and provide analysis capabilities. By automatically processing and storing your log data, these tools make querying, sorting, and filtering log events much easier. They allow you to search and retrieve the logs you need with simple instructions, removing the stress and effort of having to parse and analyze log data on your own.


Monitor Your Java Applications with Atatus

Atatus is a Full Stack Observability Platform that tracks your Java application to give you a complete picture of your clients' end-user experience. You can determine the source of delayed load times, route changes, and other issues by identifying frontend performance bottlenecks for each page request.

To make bug fixing easier, every Java error is captured with a full stack trace and the specific line of source code marked. To assist you in resolving the Java error, look at the user activities, console logs, and all Java events that occurred at the moment. Error and exception alerts can be sent by email, Slack, PagerDuty, or webhooks.

Try Atatus’s entire features free for 14 days.

Janani
Janani works for Atatus as a Content Writer. She's devoted to assisting customers in getting the most out of application performance management (APM) tools.
India

Monitor your entire software stack

Gain end-to-end visibility of every business transaction and see how each layer of your software stack affects your customer experience.