Dieser Artikel ist auch auf Deutsch verfügbar
Especially when speaking with people who have only recently been developing with Java, I learn how confusing the jungle of libraries for logging in Java truly is. After all, all the libraries look identical at first.
As is so often the case in our profession, there is a history to this. Anyone who was not there when it happened or missed it for other reasons will have a difficult time understanding it. This is why we will use this article to address the shared concepts and differences between various logging libraries in Java.
But before we delve into the world of these libraries, let’s first look at why, in my opinion, writing messages to the standard output via ‘System.out.println’ is not sufficient for logging.
Logging with System.out.println
Nowadays, most applications write their log messages to the standard output. This is especially the preferred method when the application runs in a container. It is thus tempting to write log messages without a library and with ‘System.out.println’ at the right locations.
But a log message consists of more than just the message we want to send. We are also interested in the time at which it was written, and there are messages that we only aim to send during the development period, but which we do not want to fill our log with during production.
And so, we have to build at least a minimal abstraction to meet these two requirements. Then we could also have problems with our performance. Writing to the standard output could have an influence. We would then have to implement improvements, such as with an internal queue. If we now also need a structured format to issue log messages, such as JSON, and provide the message and metadata, we have to expand our abstraction further.
By this point, we have presumably implemented most of what logging libraries do for us. Because logging does not generally present the objectivity itself, but merely serves as an aid, we should consider using one of the available libraries.
But before we look at a specific library, we should first examine the commonalities between the logging libraries.
Concepts of logging libraries in Java
Even though we have a wide range of various logging libraries to choose from in Java, they look identical at first glance. This is mainly because they are based on shared concepts and only differ in their details.
The first thing we notice during use is the name of the logger and the use of categories or levels for log messages, as seen in listing 1.
The name of a logger can be selected arbitrarily. Because most logging libraries support hierarchical loggers, however, it has become common to create them exactly like Java packages. It is also common to use a logger for each class within our library or application and to give it the name of the class in which it is being used. This is why many libraries also make it possible to provide a Class instead of a String when creating the logger.
The chosen level for a log message is a hint by itself. But it can also be used in the configuration, such as to only issue certain messages. The common ones used in most libraries are DEBUG, INFO, WARN, and ERROR. For these, it is recommended that you form a joint understanding in the project for when, which of these levels should be used.
I tend to mainly use INFO for important or relevant events that occur during normal operation, such as at the beginning or end of a job. However, it may also make sense to log events like an order in an online shop on INFO.
I use WARN and ERROR for errors that occur and that should be or need to be examined. I mainly differentiate between these based on urgency. If an ERROR occurs, I know that someone should immediately check what happened, and then probably amend the situation. With WARN the problem should also be analyzed, but it’s not urgent and no one has to be woken up in the middle of the night to fix it.
I only use DEBUG in places that can help me track errors. The places for these messages usually come down to your gut feeling, and in case of doubt, the exact place that I need to be looking at is missing a message. Candidates for DEBUG log messages include the beginning of controller methods to see that an HTTP request has really reached the application and that the parameters of the request were read correctly. Other good candidates for such a log message include places where complicated decisions have to be made. Ideally, a complete business transaction can be processed in this way without being overwhelmed with log messages.
Appenders are the next characteristic shared by all logging libraries. Even though, as mentioned above, most applications write their log messages to the standard output, this was not always the case. When I was starting out in my career, it was more common to write log messages in one or more files. This file was then also often rolled overnight, too, so that there was one file per day. Appenders are responsible for writing log messages in a specific space. In addition to files and the standard output, there are appenders for databases, message queues, and emails as well. Within the logging configuration, a logger is assigned an appender in which he writes his log messages.
An appender knows where it is writing to, but not the specific format the log message is to be written in. The logging libraries also share the layout concept. This is specified in the configuration and assigned to an appender. Layouts range from a line-based format, in which we can specify the sequence of information, like log level, time, and message, to more complex layouts that can convert our log messages into structured formats like JSON.
Many of the logging libraries also support a mapped diagnostic concept, or MDC, for supplementing log messages with context information. For example, it is possible to insert the specific tenant in the MDC at the beginning of a business transaction in a multi-tenant capable system. This is then automatically included in every log message and can be issued without us having to manually insert it into each message. Because the MDC is usually internally based on a ThreadLocal, we have to remember to migrate the MDC into a new thread when working with multiple threads. We also have to remember to remove the values from the MDC at the end so, that, for the next execution, old values are not present accidentally (see listing 2).
Now that we have familiarized ourselves with some of the most important shared concepts of logging libraries, we want to look at how some of these actually work in practice.
One of the first, if not the first, logging library was Log4j. It was created in the late ‘90s as part of the EU SEMPER project and then became its own project within the Apache Software Foundation. Log4j quickly spread throughout the community and was the first to implement many of the aforementioned concepts, thereby establishing them as the standard. Aside from being a pioneer, Log4j was especially popular for its large number of appenders that essentially met every need.
In 2015, more than 15 years later, version 1 of Log4j was replaced by Log4j 2. Version 2 is an entirely new implementation that is no longer compatible with version 1, and it contains many innovations that have since been established by other libraries. Listing 3 shows use of Log4j 2 in our code.
The greatest innovation lay in Log4j 2 consisting of two parts, namely the programming interface and the implementation itself. This makes it easier to support various logging libraries within an application. Log4j 2 also contains a large number of finished appenders, utilizes the opportunity to use lambdas facilitated by Java 8, and expands its API so that we don’t only log strings anymore, but rather objects as well. Much emphasis was also placed on high throughput and optimal garbage collection.
At pretty much the same time Log4j appeared, JSR 47 appeared in the JDK, a logging library that became available in 2002 with JDK 1.4 and is now known as java.util.logging, or JUL for short.
Unlike Log4j, JUL does not support any MDC, and the log levels use different names, as can be seen in listing 4.
Although JUL has the advantage of being delivered with the JDK, and can thus be used without any additional dependency, I believe that it never really became established outside the JDK itself. There are many reasons for this, but in particular it was up against the more widespread and widely used Log4j.
The main result of this initiative was that various libraries now used different logging libraries, and applications, were thus facing the challenge of consolidating them all. The first solution was another logging library, Jakarta Commons Logging, which formed a bridge between the various libraries.
Apache Commons Logging
Jakarta Commons Logging, JCL for short, is now known as Apache Commons Logging. JCL itself is not a logging implementation, but rather offers a programming interface that we can use in our library, as can be seen in listing 5, to write log messages.
During startup of an application, JCL dynamically detects the specific logging library being used and delegates to it. The specific library could and should be used directly within the application because the goal of JCL was not to present an abstraction of logging libraries. However, because there was no harm in using JCL in the application code instead of a logging library, this is what was done. A specific logging library was then only needed for the runtime of the application.
At the time, however, there were problems with JCL and the class loader in many web applications deployed in Tomcat. This was partially due to the mechanism JCL used to recognize the specific logging library, and partially due to how class loaders function in Tomcat.
Because of these (at least perceived) issues, JCL is barely used now, even though it was widespread. This is why, for example, Spring uses JCL for its internal logging but would take a different approach now and at this moment even uses its own implementation of JCL.
SLF4J and Logback
Although there was a commonly used logging library with Log4j, and it was also possible with JCL to integrate libraries that use different logging libraries, one of the Log4j developers, Ceki Gülcü, opted to develop another library. This resulted in two libraries, specifically Logback and SLF4J.
Similar to JCL, SLF4J only provides one programming interface, which can be seen in listing 6. Unlike JCL, however, SLF4J does not try to dynamically determine which logging library is being used at runtime, but rather, the application itself must integrate a matching bridge. This bridge implements the SLF4J interface and delegates all calls to the actual logging implementation. SLF4J also offers bridges in the other direction. Log statements using another library, such as Log4j, are thus captured by SLF4J and processed there.
Logback, a direct SLF4J implementation that, at least according to its own statement, is faster than Log4j 1 and requires less memory, arose at the same time. Logback also provided an improved filter mechanism and better stack traces.
With JEP 264, JDK 9 received another logging implementation that is usually referred to as System Logger. This in turn is a bridge with a minimalist programming interface, see listing 7, that uses JUL as the default output. Of course, the aforementioned libraries like SLF4J or log4j 2 can also be used for the output.
The library’s main advantage is that it is already contained in the JDK and we thus don’t need any external dependency for logging. For very small libraries in particular, this library is an excellent alternative to SLF4J.