1. Why Loggers?
While writing a program or developing an enterprise production application, using System.out.println seems to be the simplest and easiest option. There are no extra libraries to be added to the classpath and no additional configurations to be made.
But using System.out.println comes with several disadvantages that affect its usability in many situations. In this tutorial, we’ll discuss why and when we’d want to use a Logger over plain old System.out and System.err. We’ll also show some quick examples using the Log4J2 logging framework.
2. Setup
Before we begin, let’s look into the Maven dependencies and configurations required.
2.1. Maven Dependencies
Let’s start by adding the Log4J2 dependency to our pom.xml:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
We can find the latest versions of log4j-api and log4j-core on Maven Central.
2.2. Log4J2 Configuration
The use of System.out doesn’t require any additional configuration. However, to use Log4J2, we need a log4j.xml configuration file:
<Configuration status="debug" name="baeldung" packages="">
<Appenders>
<Console name="stdout" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
</Console>
</Appenders>
<Root level="error">
<AppenderRef ref="STDOUT"/>
</Root>
</Configuration>
Almost all logger frameworks will require some level of configuration, either programmatically or through an external configuration file, such as the XML file shown here.
3. Separating Log Output
3.1. System.out and System.err
When we deploy our application to a server like Tomcat, the server uses its own logger. If we use System.out, the logs end up in catalina.out. It’s much easier to debug our application if logs are put in a separate file. With Log4j2, we need to include a file appender in the configuration to save application logs in a separate file.
Also, with System.out.println, there’s no control or filtering of which logs are to be printed. The only possible way to separate the logs is to use System.out.println for information logs and System.err.println for error logs:
System.out.println("This is an informational message");
System.err.println("This is an error message");
3.2. Log4J2 Logging Levels
In debug or development environments, we want to see all the information the application is printing. But in a live enterprise application, more logs means an increase in latency. Logger frameworks like Log4J2 provide multiple log level controls:
- FATAL
- ERROR
- WARN
- INFO
- DEBUG
- TRACE
- ALL
Using these levels, we can easily filter when and where to print what information:
logger.trace("Trace log message");
logger.debug("Debug log message");
logger.info("Info log message");
logger.error("Error log message");
logger.warn("Warn log message");
logger.fatal("Fatal log message");
We may also configure the levels for each source code package individually. For more details on log level configuration, refer to our Java Logging article.
4. Writing Logs to Files
4.1. Rerouting System.out and System.err
It is possible to route System.out.println to a file using the System.setOut() method:
PrintStream outStream = new PrintStream(new File("outFile.txt"));
System.setOut(outStream);
System.out.println("This is a baeldung article");
And in case of System.err:
PrintStream errStream = new PrintStream(new File("errFile.txt"));
System.setErr(errStream);
System.err.println("This is a baeldung article error");
When redirecting the output to a file using System.out or System.err, we can’t control the file size, thus the file keeps growing for the duration of the run of the application.
As the file size grows, it might be difficult to open or analyze these bigger logs.
4.2. Logging to Files With Log4J2
Log4J2 provides a mechanism to systematically write logs in files and also roll the files based on certain policies. For example, we can configure the files to be rolled over based on a date/time pattern:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
<Appenders>
<File name="fout" fileName="log4j/target/baeldung-log4j2.log"
immediateFlush="false" append="false">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
</File>
<Loggers>
<AsyncRoot level="DEBUG">
<AppenderRef ref="stdout"/>
<AppenderRef ref="fout"/>
</AsyncRoot>
</Loggers>
</Configuration>
Or we can roll the files based on size once they reach a given threshold:
...
<RollingFile name="roll-by-size"
fileName="target/log4j2/roll-by-size/app.log" filePattern="target/log4j2/roll-by-size/app.%i.log.gz"
ignoreExceptions="false">
<PatternLayout>
<Pattern>%d{yyyy-MM-dd HH:mm:ss} %p %m%n</Pattern>
</PatternLayout>
<Policies>
<OnStartupTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="5 KB"/>
</Policies>
</RollingFile>
5. Logging to External Systems
As we’ve seen in the previous section, logger frameworks allow writing the logs to a file. Similarly, they also provide appenders to send logs to other systems and applications. This makes it possible to send logs to a Kafka Stream or an Elasticsearch database using Log4J appenders rather than using *System.out.println.
*
Please refer to our Log4j appender article for more details on how to use such appenders.
6. Customizing Log Output
With the use of Loggers, we can customize what information is to be printed along with the actual message. The information that we can print includes the package name, log level, line number, timestamp, method name, etc.
While this would be possible with System.out.println, it would require a lot of manual work, while logging frameworks provide this functionality out of the box. With loggers, we can simply define a pattern in the logger configuration:
<Console name="ConsoleAppender" target="SYSTEM_OUT">
<PatternLayout pattern="%style{%date{DEFAULT}}{yellow}
%highlight{%-5level}{FATAL=bg_red, ERROR=red, WARN=yellow, INFO=green} %message"/>
</Console>
If we consider Log4J2 for our logger framework, there are several patterns that we can choose from or customize. Refer to the official Log4J2 documentation to learn more about them.
7. Avoid printStackTrace() by Logging Exception Output Instead
When we handle exceptions in our code, we often need to learn what exceptions actually occurred at runtime. There are two common options for this: printStackTrace() or using a logger call.
It’s pretty common to see exception handling that uses printStackTrace() to print details about the exception:
try {
// some code
} catch (Exception e) {
e.printStackTrace();
}
The problem here is that printStackTrace()* prints its information to *System.err, and we’ve already said we want to avoid that.
Instead, we can log the exception using the logging framework, and then, we’ll be able to easily retrieve the logs:
try {
// some code
} catch (Exception e) {
logger.log("Context message", e);
}
8. Conclusion
This article explains various reasons why to use a logger framework and why not to rely only on System.out.println for our application logs. While it is justifiable to use System.out.println for small test programs, we’d prefer not to use it as our main source of logging for an enterprise production application.
As always, the code examples in the article are available over on GitHub.