1. Introduction
In this tutorial, we’ll explore how and why to use the Log4j framework for logging.
Logging is the art of producing messages during the execution of a program to help developers and operators interact with it. Note that the log messages aren’t designed to be read by the application’s users but by its maintainers and those responsible for keeping it working.
All software developers, sooner or later, need to debug code. When it’s not practical to use a fancy IDE to explore step-by-step what their code does, the more mundane but always effective option of printing messages to the screen is always available. But it’s not problem-free. Let’s explore the problems of this approach and how logging helps us avoid them.
2. The Problems of println
Using println pollutes the code. The more information we need at runtime, the more printing statements we need to add, and the code soon becomes ugly and difficult to read.
Information presented this way isn’t granular. There is simply no way to know which messages are critical, which are important, and which are just informative. There’s no way to know if the messages are useful for debugging or for operations.
Additionally, println impacts the performance of the program. Printing a message to the screen doesn’t take a long time, that’s true. But it takes some, and it’s a blocking operation. If that operation happens often, the net effect on the performance will become noticeable.
Also, there’s no way to control the display of messages. Once set up, they’re always there unless we wrap every message inside an if statement (a horrible thing to do).
3. Logging Frameworks to the Rescue
A way to address the above-mentioned problem is to use a logging framework. These still pollute the code, but since all the messages take the same general form, they do so in a less ugly way.
On the other hand, logging frameworks are very granular. They give the developers the concept of levels, so messages can be deemed debugging, informational, warnings, or errors in their nature.
Logging frameworks have a much lesser impact on the program because they’re not blocking and because the evaluation of the expression to print only happens if they’re of the appropriate level of granularity.
As if that were not enough, logging frameworks allow for an extended configuration of the logs. For instance, we can:
- Append messages to the screen, to files, or to both.
- Use different formats for the timestamps and for the messages themselves.
- Include tags (marks) in the messages for further filtering and grouping.
- Rotate the logging files to impede them grow indefinitely.
- Send them to a remote, central logging facility.
4. Log4j
4.1. Setup
Log4j is one of the most renowned, widely used, and influential logging frameworks for Java. It has a modular, plug-in structure. To use it in Scala, we need to declare two dependencies:
- The core log4j library — Since we’re not going to interact with this directly, we can declare it as a run-time dependency.
- The Scala-specific bindings — This one is a regular compile-time dependency.
Let’s see what an example setup might look like in our build.sbt file:
libraryDependencies ++= Seq(
"org.apache.logging.log4j" % "log4j-api-scala" % "13.0.0",
"org.apache.logging.log4j" % "log4j-core" % "2.19.0" % Runtime
)
4.2. Configuration
Log4j is quite versatile regarding configuration. We can configure it using configuration files (properties files, JSON, YAML, or XML), or we can configure it programmatically. Most of the time, static configuration with files is enough, as it’s both flexible and maintainable.
What’s important to remember is that the configuration contains properties, appenders, and loggers. We can think of appenders as the output of the logging system: We can declare the console, files, remote central logging nodes, database, and other destinations. And we can think of the loggers as the contact point with our code: Using them, we can filter our messages, send our messages to several appenders, define the logging level for specific classes, and more.
For our examples, let’s stick with XML. A very simple approach declares a file called 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>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
The configuration file can be anywhere as long as it appears in the program’s classpath. It’s conventional to place it in the src/main/resources directory of our project.
4.3. Basic Use
By extending the Logging trait, a class inherits a value called logger, of type org.apache.logging.log4j.scala.Logger. All our code needs to do is call its methods:
object LoggingApp extends App with Logging {
logger.info("Writing an informative message to the log")
logger.debug("Writing a debug message to the log")
Try(1 / 0) match {
case Success(value) => logger.warn("Math has changed")
case Failure(exception) => logger.catching(exception)
}
}
When we run this program, we’ll see the output on our screen:
/usr/lib/jvm/java-11-openjdk/bin/java ...
18:26:48.390 [main] INFO com.baeldung.scala.log4j.LoggingApp$ - Writing an informative message to the log
18:26:48.398 [main] ERROR com.baeldung.scala.log4j.LoggingApp$ - Catching
java.lang.ArithmeticException: / by zero
at com.baeldung.scala.log4j.LoggingApp$.$anonfun$new$1(LoggingApp.scala:10) ~[classes/:?]
at scala.runtime.java8.JFunction0$mcI$sp.apply(JFunction0$mcI$sp.java:23) ~[scala-library-2.12.15.jar:?]
at scala.util.Try$.apply(Try.scala:213) ~[scala-library-2.12.15.jar:?]
at com.baeldung.scala.log4j.LoggingApp$.delayedEndpoint$com$baeldung$scala$log4j$LoggingApp$1(LoggingApp.scala:10) ~[classes/:?]
at com.baeldung.scala.log4j.LoggingApp$delayedInit$body.apply(LoggingApp.scala:7) ~[classes/:?]
at scala.Function0.apply$mcV$sp(Function0.scala:39) ~[scala-library-2.12.15.jar:?]
at scala.Function0.apply$mcV$sp$(Function0.scala:39) ~[scala-library-2.12.15.jar:?]
at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17) ~[scala-library-2.12.15.jar:?]
at scala.App.$anonfun$main$1$adapted(App.scala:80) ~[scala-library-2.12.15.jar:?]
at scala.collection.immutable.List.foreach(List.scala:431) [scala-library-2.12.15.jar:?]
at scala.App.main(App.scala:80) [scala-library-2.12.15.jar:?]
at scala.App.main$(App.scala:78) [scala-library-2.12.15.jar:?]
at com.baeldung.scala.log4j.LoggingApp$.main(LoggingApp.scala:7) [classes/:?]
at com.baeldung.scala.log4j.LoggingApp.main(LoggingApp.scala) [classes/:?]
Process finished with exit code 0
The first message, produced with logger.info, is there. No surprises here. But the second message is not there! If we recall the configuration, we’ll see that the root logger is of level info. So, all messages of lower priority than that will be silently ignored. During coding time, it’s useful to have a low level – debug, for instance – and then raise it to info or warn in production. That can be achieved automatically using arbiters, but that lies outside the scope of this short tutorial.
The third message is something that not all logging frameworks offer: a special treatment for exceptional conditions (throwables).
5. Alternatives
There are several alternatives to Log4j for Scala developers. Very popular ones are the Java Logging API and Logback.
Around a logging framework, an even higher abstraction can be added: a logging wrapper. They allow the developers to deal with a single interface, while the actual logging is done by a pluggable logging framework. Possibly the most popular wrapper is SLF4J, and possibly its most popular back-end is Logback. Those constitute the default logging solution for Akka projects and related technologies.
However, since these projects were started by the same person, Ceki Gülcü, they share much of the philosophy of Log4j and can be considered improvements upon it.
6. Conclusion
Writing log messages is a skill that all developers should have. And although we haven’t touched on it in this tutorial, it’s not only a matter of how to produce the messages but also what they should contain. What we should avoid at all costs is the use of println in our code, except in very small, illustrative programs.
Log4j is a very flexible, mature, and battle-tested option for logging. The fact that the logger object is accessible from the trait makes it very simple to use as well.
As usual, the code for this article is available over on GitHub.