Java Logging Guide:
The Basics

October 31, 2022

Logging helps you understand how an application performs or what went wrong when something fails. This information can be critical for debugging and auditing purposes. Logs maintain a trail of every event during a program’s execution, making those records available for later analysis.

However, effective logging does not happen automatically. Application developers need to ensure an application is systematically logging important details in an easy-to-process format.

The most rudimentary approach to logging is the simple use of print statements to display desired application details. Of course, this isn’t an ideal approach for the following reasons:

  • The output of the print statement exists only for the lifetime of the console and is not available for later analysis.
  • Custom formatting for print statements is either unavailable or cumbersome to implement.
  • In enterprise environments with multiple CI/CD processes, print statements can oftentimes be a lengthy process yielding performance problems and increasing program execution time

For all these reasons, many programming languages, like Java, include dedicated logging APIs and frameworks. In this overview—the first part of a two-part series—we will introduce the basic logging concepts for Java applications. We will look at logging options and then consider several available logging frameworks and their supported configurations.

Understanding Java Logging Frameworks

Like everything else in its architecture, Java takes an extensible and customizable approach to logging. The java.util.logging framework is the default option for all logging-related functions. This framework provides all the basic features needed for logging while allowing third-party frameworks to extend those features.

This logging framework includes three primary modules:

  1. Loggers: Loggers are responsible for capturing log events and redirecting them to the configured handler. The events are captured as instances of the LogRecord class.
  2. Handlers: Handlers dictate the log destination. This destination can be the console, file, or even a database. Handlers are sometimes called “appenders” in third-party logging frameworks.
  3. Formatter: The Formatter defines the format of the log entries—in other words, what the log entry should look like.

Another concept worth noting here is the Filter. Filters allow the developer to intercept a LogRecord and decide which handler should be used. While unnecessary for basic logging, Filters can help meet complex logging requirements.

For Java’s logging framework, the default output location is the user’s home directory.

While Java’s logging framework provides all the basic features, third-party frameworks can do quite a bit by combining Loggers, Handlers, and Formatters. Log4j 2 and Logback are two popular logging frameworks, each with its own strengths.

Since multiple frameworks are available for Java and their feature sets are always evolving, developers often want to try something different. This is where logging abstractors come into play. Abstractors allow developers to quickly switch between logging frameworks by using a standard function specification. Simple Logging Facade for Java (SLF4J) is a popular logging abstractor that supports the default framework, Log4j 2, and Logback. Apache commons-logging is another such abstractor.

It’s important to note, however, that unpatched versions of Log4j 2 pose a critical security risk (CVE-2021-44228). Though less severe, considerable security risks also exist for Log4j 1.x (CVE-2021-4104) and Logback (CVE-2021-42550). Because the use of abstractors still depends on these underlying frameworks, it is imperative that these vulnerable libraries are properly updated and patched for secure usage.

Implementing Logging With The Java Logging API

<h2 “Set Up & Logging”>Setting Up and Logging Events

The default framework uses a configuration file to define the details of appenders, loggers, and layouts. By default, this file is present in the lib folder of the Java installation directory, but this is a global configuration file.

It’s a good practice to define an application-specific configuration file. You can do this by specifying the properties file name when starting the application, as shown below:

-Djava.util.logging.config.file=/path/to/app.properties

A simple configuration file will look like this:

handlers=java.util.logging.FileHandler
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

Here, the configuration will log application events to a file it will create in the user’s home directory. The value %h denotes the home directory, and %u denotes an arbitrary unique number java will set to distinguish between log files.

Now, to create a new logfile from a new class, you can use the code snippet below:

Logger logger = Logger.getLogger(YourClass.class.getName());

When and what to log is entirely up to the application developer, and it depends on the logging strategy adopted by the application team. Once the developer decides which events to log, logging simply requires a single line of code. Here is an example:

logger.log(Level.INFO, "This is some info!");

Here, the snippet uses the logger.log method to persist a log event. The method takes two inputs—the logger level configuration and the text to be logged.

You can use two other methods to persist log events. The logp method logs the name of the class and functions along with the text. The logrb method lets you specify the resource bundle for localization. This is important for multi-lingual applications. Both methods can be used instead of the log method.

Log Destinations

The logging framework supports five types of handlers (and, therefore, five types of destinations). These can be easily configured in the properties file or the code. Let’s explore these five handlers and see how they are supposed to be used.

ConsoleHandler: As the name suggests, this will output log events to the console. There is no mechanism for persisting the output here. All the log entries will be lost once the console terminates. You can specify the handler in the properties file:

handlers=java.util.logging.ConsoleHandler

FileHandler: This handler writes logs to a file. This handler allows you to rotate log files when they reach a specific size. The snippet below shows how to set this handler, along with its specific parameters in the properties file:

handlers=java.util.logging.FileHandler
java.util.logging.FileHandler.limit     = 34534
java.util.logging.FileHandler.append    = false
java.util.logging.FileHandler.pattern   = log.%u.txt

The limit parameter defines the maximum log file size in bytes, after which a new log file will be created. The append parameter stipulates whether append should be permitted if the filename exists. The pattern parameter defines the log filename. 

SocketHandler: This handler writes logs to an HTTP socket. This can be useful when you need to store the logs in another instance or service other than the one already running. The default format used here is the XMLFormatter.

StreamHandler: This handler writes the output to an OutputStream that can later be used to write to any destination. This handler is a base class acting as a foundation in case a custom handler based on a different destination needs to be defined. Defining it is as easy as setting the handler in the properties file.

MemoryHandler: This handler writes all log entries to an in-memory circular buffer. It writes to the memory buffer if an incoming record has a level higher than a predefined one or if it meets specific conditions. These conditions can be customized by implementing a class that overrides the log function. The new function can scan all the records and decide when to write. This handler is often used because, in terms of performance, buffering is cheap while formatting is costly. By using this handler, formatting costs are deferred until writing is needed. 

Handlers are one area where the third-party logging frameworks do better than the default logging API. For example, Log4j 2 provides many handlers commonly used in an enterprise setup. For example, the SysLogAppender writes entries to the operating system log (such as syslog) or a log aggregating server.

Understanding Log Levels

Log Levels help developers classify logs according to their importance. This is very useful when migrating code through different environments—like development, staging, or production. In the development environment, developers may want to get all the information they can for debugging or testing. Once the code is deployed to production, the operations team may want to be notified about SEVERE log events only. The Java logging framework supports seven logging levels. Typically, applications use the following ones in increasing order of severity:

  • INFO
  • WARNING
  • SEVERE

Setting a log level when the Logger class is initialized helps developers filter out all the logs below that level. For example, when the log level is set to INFO, only the events tagged as INFO, WARNING, or SEVERE will be output. You can set the log level with a simple command:

logger.setLevel(Level.INFO);

The log function allows defining the level for persistent log entries. 

logger.log(Level.INFO, "This is some info!");

Conclusion

In this first part of our overview, we have covered the basics of Java’s default logging framework. While the framework provides all the basic features needed for application-level logging, advanced features require developers to write custom handlers. Third-party logging frameworks like Log4j 2 or Logback expand the feature set and remove the need for a developer to do custom development.

In Part Two of this series, we will learn about advanced concepts in Java logging, including exception handling, formatters, and log aggregation.