Robust application logging is central to any quality strategy. Unfortunately, many quality strategies fall short, implementing logging in a less than stellar way. Java application logging is no different. And since we’re talking about one of the most popular programming languages, investments in improving the overall Java logging strategy could pay off many dividends in the future.
I wrote this to share what I have learned about logging in Java and how to do it better. I’ll walk you through a list of tips you can start applying today. Some of them will be specific to Java; others will also be applicable to other tech stacks. After reading, you should be ready to take your logging approach to the next level.
I know it is obvious, but I had to put this on my list. The reason is simple: many people still roll out their homegrown Java logging solutions, and most regret it down the road.
Even though logging might look like just a matter of recording some text to a destination, there’s much more to it than that. A robust logging approach includes:
When you adopt a logging library or framework, you get all of the above—and more—in a nicely packaged, ready-to-use unit, and most of the time it’s free.
One of the most popular solutions for the Java world is the Apache Log4j 2 framework. Maintained by the Apache Foundation, Log4j 2 is an improvement on the original Log4j, which was the most popular logging framework in Java for many years. Log4j 2 brings new features, fixes old problems, and offers an API detached from the implementation. That way, not only do they ensure the framework will always evolve in a compatible way, but they also allow use of other logging frameworks as long as they adhere to the contracts of the API.
Logging levels are labels you can attach to each log entry to indicate their severity. Adding adequate log levels to messages is essential if you want to be able to make sense of them.
Log4j 2, besides supporting custom log levels, offers the following standard ones:
|FATAL||100||The application is unusable. Action needs to be taken immediately.|
|ERROR||200||An error occurred in the application.|
|WARN||300||Something unexpected—though not necessarily an error—happened and needs to be watched.|
|INFO||400||A normal, expected, relevant event happened.|
|DEBUG||500||Used for debugging purposes|
|TRACE||600||Used for debugging purposes—includes the most detailed information|
The most obvious benefit is levels allow you to search and filter your log entries according to their severity. That way, you can prevent a “needle in a haystack” kind of situation. Imagine having to find the one error entry among thousands of normal events.
You can go a step further and configure your logging so log entries are written to different destinations according to their levels, which can make it even easier to find the messages you’re looking for.
Additionally, you can use levels to control the granularity of your logging. In the development environment, you could set the application’s level to the minimum level, so everything is logged. On the other hand, in the production environment, you typically wouldn’t want to record everything, so you’d set the level as INFO.
There’s nothing more frustrating than trying to troubleshoot unexpected application behavior, opening the logs… only to find they contain meaningless, contextless messages.
Don’t do this to your fellow developers. Create meaningful log messages, rich with context, so they have enough information to understand the event and resolve the issue. Instead of a generic “Operation failed” message, be more specific, such as “Data export to .csv format failed.” You should also include relevant metadata in your event messages. At a minimum, you should include:
Almost 10 years ago, Jeff Atwood said performance is a feature. He was right then, and he’s still right now.
Make no mistake: you should expect a performance hit due to logging. But there are steps you can take to minimize performance impacts. Consider the following piece of code:
logger.info(String.format("The result is %d.", superExpensiveMethod()));
As you can see, to construct the log message, we call an extensive method. Now suppose the current logging level set for the application is WARN. This means the message above won’t get logged. However, the call to superExpensiveMethod() will still be made. The advice here is simple: before doing expensive computations, check whether the application’s configured logging level will allow your message to be written. In the case of log4j 2, you can easily do it this way:
if (logger.isEnabled(Level.INFO)) logger.info(String.format("The result is %d.", superExpensiveMethod()));
By performing the check above, you can preventing an unnecessary call to a very expensive method.
Another thing you can do to mitigate the impact of logging on your app’s performance is to avoid logging in the hot path. “Hot path” here means a portion of the application critical for performance and executed frequently. If you can avoid logging in this area, you prevent a large number of performance-impacting logging calls. Another option is to leverage asynchronous logging, so the main thread of the application doesn’t get stuck because of expensive logging calls.
Many applications handle some sort of sensitive data. When it comes to logging, you have to be careful not to log the data. Besides the obvious security implications, if you fail to correctly handle sensitive data, you might find yourself with serious financial and legal consequences.
Here is a non-exhaustive list of data you should not include in logs:
If you do have a legitimate need to log sensitive information, don’t log them in plain text. Rather, use anonymization and/or pseudonymization techniques, so an eventual attacker can’t read the true data.
The advice here is short: When logging exceptions, include the whole stack trace. This will make it easier for other developers or IT pros to troubleshoot problems. They’ll be able to track the problem to its root cause and fix it.
Last but not least, don’t forget to make your log entries machine parsable.
Many of the tips on this list could be summarized in one sentence: make your logs human readable. This makes sense since, most of the time, humans are the intended target audience for logs.
But it’s becoming increasingly common to use tools to perform analysis on log files, extracting information from the logs to help with decision-making. Parsing plain text logs is doable, but it often requires complex regular expression to locate and extract the relevant pieces of data.
So, instead of logging in plain text, leverage structured logging—for instance, outputting your log events as JSON. This will allow you to use off-the-shelf tools to parse and extract the information you need.
Good logging is one of the most valuable investments you can make in your application if you want to achieve high-quality software. Unfortunately, it’s easy to get logging in Java—and in other languages—wrong. I wanted share what I learned, so you can avoid these common traps.
On a last note, if you are looking for a cloud-based logging tool that is easy to setup and use, check out SolarWinds® Papertrail™. Papertrail, aggregates all your logs, including Java, into a single location and lets you search and filter events in real time.
Now that you have the basics, the next step is to make your approach to Java logs more efficient and easier to manage.
I shared some pointers on how to approach logs with minimal impact on performance, most of them related to making the messages in the log files easily readable and understandable. To sum it up: be considerate of the devs who will be relying on your logs . One of them might even be you.
This post was written by Carlos Schults. Carlos is a .NET software developer with experience in both desktop and web development, and he’s now trying his hand at mobile. He has a passion for writing clean and concise code, and he’s interested in practices that help you improve app health, such as code review, automated testing, and continuous build.