Side note: Ship your structured Java logs to Better Stack
Once your Java app is emitting structured logs with Log4j, Better Stack makes it easy to explore them in real time, run powerful searches, and build dashboards that keep an eye on production.
Logging is an integral and indispensable aspect of the software development process. It empowers developers to effectively monitor and troubleshoot applications by gathering and analyzing pertinent data. By identifying potential issues and bugs, logging plays a pivotal role in enhancing code quality and optimizing performance.
In this comprehensive tutorial, we will delve into the realm of best practices for creating a robust logging system specifically tailored for Java applications. By implementing these practices, you will be able to craft logs that are consistent, informative, and immensely valuable for critical tasks such as debugging, maintenance, and other essential purposes.
Before proceeding to the rest of this article, make sure you have JDK 20 installed on your computer, and we assume you are using Apache Maven as the build system. You should also understand the basics of logging in Java, including log levels, log rotation, and so on.
Once your Java app is emitting structured logs with Log4j, Better Stack makes it easy to explore them in real time, run powerful searches, and build dashboards that keep an eye on production.
Like many other programming languages, Java has built-in logging functionality
provided by the java.util.logging package. However, even though it is
effortless to set up and use, it only offers basic logging features, making it
unsuitable for production-ready applications.
If you are building a large-scale application, you'll likely need a more robust logging solution, which should have the following features:
You get all of these features out of the box by using a third-party logging framework. And in the Java community, the two most popular options are Log4j and Logback.
Log4j is a powerful and flexible logging framework for Java applications. It provides a wide range of features, including log levels, various logging appenders, advanced logging configurations, and support for asynchronous logging. The framework has a modular architecture that allows developers to easily extend and customize its functionality, and it provides an intuitive API that makes it easy to integrate with other tools and frameworks.
Logback is a project created based on the old Log4j 1, as a result, they offer similar functionality. On top of that, Logback offers many improvements. It is known for its simplicity and ease of use, with a consistent API and configuration process that make it easy to integrate with other tools and frameworks. Additionally, Logback is highly performant, with a focus on efficiency and minimal resource usage. The framework is actively maintained and has a large user community, making it a reliable choice for logging in Java applications.
Overall, If you are looking for something simple and easy to use, you should go with Logback, and you if need a more feature-rich and extensible framework, then you should consider Log4j instead. In this tutorial, we are going to use Log4j as an example, as it is the more popular option.
Many other tutorials refer to SLF4J as a logging framework, but that is not an accurate definition. It is not like Log4j or Logback, instead, it provides an interface that work on top of other logging frameworks, allowing developers to switch the underlying logging frameworks without having to change any code.
For example, you could set up Log4j to work with SLF4J. First, make sure you
have the following dependencies in your pom.xml:
log4j-api and log4j-core are the main components of Log4j, and
log4j-slf4j2-impl is Log4j's SLF4J binding, which allows SLF4J's API to use
Log4j as its backend.
And then you could configure this logging system as you would with a standard Log4j setup.
Lastly, use SLF4J's API to make a logging call instead of Log4j.
The log level is a fundamental concept in logging, no matter which logging framework you use. It allows you to tag log records according to their severity or importance. SLF4J offers the following log levels by default:
TRACE: typically used to provide detailed diagnostic information that can be
used for troubleshooting and debugging. Compare to DEBUG messages, TRACE
messages are more fine-grained and verbose.DEBUG: used to provide information that can be used to diagnose issues
especially those related to program state.INFO: used to record events that indicate that program is functioning
normally.WARN: used to record potential issues in your application. They may not be
critical but should be investigated.ERROR: records unexpected errors that occur during the operation of your
application. In most cases, the error should be addressed as soon as possible
to prevent further problems or outages.You should always ensure you log the each messages with the appropriate log level. For example:
You need to specify the most appropriate a minimum log level for the logger, so
that you can limit the number of logs recorded in a particular environment. For
example, you can default to INFO in production and DEBUG in development. To
specify log levels in Log4j, open the configuration file (log4j2.xml) and
configure it as shown below:
This configuration defines two different loggers, org.apache.test and the
default logger (Root), both using the Out appender. The logger
org.apache.test has minimum log level trace, which means it will log all
messages higher than or equal to trace. The Root logger has minimum log
level error, meaning it will exclude messages with levels trace, debug,
and info.
Plain text log message are easy for humans to read but not so much for machines . However, when running an application with a considerable number of logs, you'll definitely need to rely on machines to process them to allow for more efficient log processing and analysis workflows. Logging in a structured format, such as JSON, makes it easier for machines to process and analyze the log records.
Log4j offers a convenient feature that allows you to log in different formats, called layouts. It enables you to format the log records into CSV, JSON, XML, YAML, etc. The most commonly used format is JSON.
To log in JSON format using Log4j, make sure you include the jackson-databind
dependency in your pom.xml file, and then create a new JsonLayout in the
configuration:
Now your log messages will be transformed into JSON format:
You can also use the JsonTemplateLayout instead, which allows you to specify a
template that the JSON output should follow. Ensure you have the
log4j-layout-template-json dependency in your pom.xml, then edit the
configuration file:
This example uses the Elastic Common Schema (ECS) specification, defined by the
EcsLayout.json file:
And it should give you the following output:
Another issue with our previous examples is that the message only contains
simple text such as A user just signed in, which does not provide enough
information about the user who signed in. In practice, you should always provide
more contextual information to describe the event logged.
For example, with SLF4J, you could include the username using MDC (Mapped
Diagnostic Context):
Notice that the username has been added to the end of the log entry. Note that the same context is added to all the logging calls in the current scope:
If you want to clear the context, you must use the MDC.clear() method before
the call to a logging method.
You may have noticed something called appender in both Log4j. Appenders allow you to push the logs to different destinations, such as the console, local files, databases, and so on. You can set up a logger with multiple appenders to forward the logs to different destinations. Here is an example:
You may configure this system however you wish, but in most cases, you should configure the loggers to log to the standard output.
There are many benefits to forwarding the logs to the standard output. It is usually the most straightforward method of setting up your logging system. The standard output is also the most accessible option, as you can use the terminal to check the logs in real-time.
And most importantly, logging to the console is the standard behavior for most logging tools and frameworks. For example, you can use the log routers such as Vector, LogStash, or Fluentd to collect the logs from the standard output, and then deliver them to other destinations for long-term storage or analysis.
With Better Stack, you can turn your Log4j JSON logs into clear dashboards. Track errors over time, break down trends by level or exception type, and use MDC context to follow user activity, all in one place.
In a real-world scenario, different teams in your organization could use the logs for various purposes. For instance, the development team could use the logs to identify potential issues in the application, the operations team could use the logs to track the key performance indicators and monitor the health of your services, and the customer services team could use the logs to gain insights into user behavior and understand how users are interacting with the application.
As a result, you should always log as many events as possible when logging in your Java application and always log for more than debugging and troubleshooting purposes. Some examples include:
However, even though you should be as thorough as possible when logging, that doesn't mean you should log everything. Some sensitive information could lead to security issues, such as passwords, the user's real name, address, credit card numbers, and so on.
Using Log4j, you can programmatically mask sensitive information in your log messages with the following steps:
EcsLayout.json in our
example): This example will find the password field in your MDC, and then mask it with
the MaskingJsonMessage class.
MaskingJsonMessage class like this: This class overwrites the default getFormattedMessage() method, and masks
the password field by replacing its value with ********.
log4j2.xml.Logging as many activities as possible may also have a negative impact on the performance of your application and services. For example, it could slow down the application's response time and, as a result, affect the user experience. But luckily, there are ways to remedy that.
For instance, you could up asynchronous logging for your application, which allows frameworks to execute I/O operations in a separate thread so that the logging call will not affect the application's response time.
With synchronous logging, the application thread will generate the log message, and then wait to push the message to its destination before it can continue executing. This can slow down the application, particularly if the logging operation takes a long time to complete.
Asynchronous logging, on the other hand, will utilize a separate thread to push the log message, so that the application thread could continue executing without waiting. This can help reducing the impact of logging on the performance of your application, particularly in high-traffic environments where a large number of log messages are generated.
But of course, nothing is perfect. Asynchronous logging does have several downsides, such as increased memory usage, as the log messages will need to stay in memory before the second thread picks them up. Async logging could also lead to wrong log message ordering, which could make it more difficult to debug issues that span multiple log messages. And there is also the risk of data loss, if the system crashed before log messages are pushed to its destination for long-term storage, the messages that are still in memory could be lost.
To set up asynchronous logging with Log4j, you can convert all existing loggers to asynchronous loggers by setting either one of the following system properties:
Or if you only want selected loggers to be asynchronous, use the <asyncRoot>
or <asyncLogger> configuration elements:
Besides asynchronous logging, Log4j also comes with a feature called Garbage-free logging. Most logging frameworks allocate temporary objects during logging, which puts pressure on the garbage collector. By enabling the garbage-free mode, Log4j will reuse the objects and buffers and allocate as few temporary objects as possible.
The garbage-free logging can be enabled by setting the following system properties:
First, make sure objects are created in ThreadLocal and reused.
Ensure log events are converted to texts and then to bytes without creating objects.
Enable garbage-free mode for the ThreadContext map.
Exception handling is perhaps the most common use case for logging. When logging exceptions, you should always log the full stack trace, so that the log entry includes enough information to let you know where the exception happened. Here is an example of how to log exceptions using Log4j:
This example contains detailed information regarding the error, such as the type of the error, the error message, as well as the location of the error in your code.
When managing a smaller application with one or two servers, you can log on to each to check the logs. However, as your application scales, this becomes tedious. The solution is aggregating and centralizing your logs with a log management service.
Better Stack is a cloud-based log management tool that enables you to collect, monitor, and analyze log data from various sources in real time. It offers a centralized platform for managing log data with powerful search, filtering, alerting, and visualization capabilities.
With Better Stack, you can easily collect and analyze log data from servers, applications, containers, and cloud platforms. It offers integrations with popular services like AWS, Google Cloud, Docker, Kubernetes, and many others.
Centralizing your logs with Better Stack helps you streamline log management processes, improve troubleshooting and monitoring capabilities, and enhance your overall application performance and security.
To get started, register an account, create a new source, and follow the instructions to connect Better Stack to your Java project. You can use an appender to push logs directly to Better Stack, but the standard approach is to continue sending log entries to standard output, then use Vector to collect and redirect them to Better Stack.
Once logs start streaming, you'll see them on the Live tail page where you can watch logs in real-time and filter them using queries like log.level:ERROR or username:jack.
One major benefit of using Better Stack is the ability to visualize your logs with custom dashboards.
You can also easily set up monitoring and alerting. Create alerts based on log patterns, volumes, or specific conditions. For example, Better Stack can push an alert to your team if more than 10 ERROR logs are received in one minute, or when specific exceptions like ArrayIndexOutOfBoundsException occur repeatedly.
In this tutorial, we covered some best practice guidelines that you should follow to help you create a comprehensive and effective logging system for your Java application. We hope this article can help you create more robust and reliable applications.
To dig deeper into the subject of logging, please take a look at our articles on how to choose the right framework for your application, what is log aggregation, as well as a detailed tutorial on how to start logging with Log4j.
Thanks for reading, and happy logging!
We use cookies to authenticate users, improve the product user experience, and for personalized ads. Learn more.