Java Logging Guide:
Advanced Concepts

October 31, 2022

Java Logging Guide: Overview

Part 2: Advanced Concepts

A log file contains the records of all the events that occurred during an application’s execution. These files help troubleshoot application failures, debug software errors, and audits.

In Part One of this guide, we saw that logging using the print statement isn’t very useful, as the message is lost when the console session expires. This approach also lacks formatting and destination options and can cause performance issues.

Logging frameworks for a programming language can help, offering features and options for logging. In the case of Java, its default logging API is java.util.logging, which sets the foundation for Java application logging. We covered the basics of logging with the Java Logging API in Part One.

Here in Part Two, we will learn about advanced functionalities like exception handling, layouts, and aggregation. Finally, we’ll cover a brief introduction to LogScale, a modern log management solution you can use to capture, process, and analyze Java log messages.

Logging Exceptions

An exception occurs when an application’s flow encounters a situation the programmer didn’t anticipate or implement specific code to handle. Exception handlers address such situations. Exception handlers take a default action when they can’t resolve the specific error condition. This allows a graceful response to the error.

Java developers can wrap their code in try-catch blocks for exception handling. If the code in the try block fails for some reason, the control goes to the catch block. The code in this block tests the reason for the error and takes appropriate action. If the code in the catch block (the exception handler) fails to find the cause of the error, it takes a default action, such as logging a generic message. Java provides information about the sequence of function calls that resulted in that exception and the exception message. This is called a stack trace.

Logging exceptions is about systematically persisting the exception, the stack trace, and anything else required to investigate it.

Handling Caught Exceptions

Caught exceptions are the ones developers have already handled. For example, let’s say you are implementing a simple division function, and the user inputs the divisor as zero. In this case, Java will throw an ArithmeticException. The developer handles this scenario by writing code for catching the ArithmeticException and logging it appropriately, as in the example below:

try {
  int out = 11 / 0;
}
catch (ArithmeticException e) {
  logger.log(Level.SEVERE, "Exception occurred", e);
}

We see the logger.log method called inside the catch block. This method takes three arguments: a log level, an exception message, and the exception object. This will result in the logging of the complete stack trace.

Logging exceptions often result in large amounts of text being written to the log files. If you have a programmatic way to process logs, using a layout other than the SimpleFormatter is good practice. Of the default layouts available, XMLFormatter works well in this case.

Handling Uncaught Exceptions

Developers can use try-catch blocks in code only if they know a specific block of code might throw an error. Sometimes, this is difficult to anticipate. However, without any exception handler in place, the program will throw an unhandled exception and terminate.

It’s critical to log such exceptions because, once an unexpected event happens, logs are the only means available to the developer to find the root cause of the issue. The Java Logging API allows setting an UncaughtExceptionHandler at the class level to redirect logs from uncaught exceptions. An example of this usage is as follows:

Thread.setDefaultUncaughtExceptionHandler(
  new Thread.UncaughtExceptionHandler() {
    public void uncaughtException(Thread th, Throwable ex) {
      logger.log(Level.SEVERE, t + "Logging the uncaught exception", ex);
    };
  }
);

We see that the same logger.log method has been used, but there’s an UncaughtExceptionHandler class created, and the new class is assigned as the DefaultUncaughtExceptionHandler. Java will redirect all uncaught exceptions to this handler.

Understanding Formatters

A Formatter defines how log records will be written to a file. The default Logging API provides two formatters: SimpleFormatter, which deals with plain text files, and XMLFormatter. Frameworks like Log4j 2 and Logback come with additional formatters. Some popular supported formats include HTML, JSON, and PatternLayout.

Third-party frameworks sometimes denote formatters as Layouts. PatternLayout is one such layout. Layouts help developers implement custom formats by defining the exact entities to be logged.

You can use a configuration file—or even use the code when initiating the logger—to specify the properties of the formatter. A sample configuration file is shown below:

handlers=java.util.logging.FileHandler
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 20000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter

Code that uses this logger might look like this:

Handler consoleHandler = new ConsoleHandler();
consoleHandler.setFormatter(new XMLFormatter());
logger.addHandler(consoleHandler);

Sometimes, developers may find that the two default formatters included with the default Logging API are quite limited for structured logging needs. Java makes it easy for anyone to implement a custom formatter by providing a Formatter interface.

The following code shows the setup for a basic custom formatter:

class CustomerFormatter extends Formatter {
    
    public String format(LogRecord record) {
        StringBuilder builder = new StringBuilder(1000);
        builder.append("This is a custom formatted message");
        builder.append("[").append(record.getSourceClassName()).append(".");
        builder.append(record.getSourceMethodName()).append("] - ");
        builder.append("[").append(record.getLevel()).append("] - ");
        builder.append(record.toString());
        builder.append("\n");
        return builder.toString();
    }

    public String getHead(Handler h) {
        return super.getHead(h);
    }

    public String getTail(Handler h) {
        return super.getTail(h);
    }
}

With a bit more programming, the above example can be extended to write records in any required format like HTML, JSON, or others. You can also use a third-party framework with built-in formatters.

Using Log Aggregators

Log aggregators help collect logs from multiple applications in one place and visualize them.  Log aggregators allow you to retain a historical record of everything that has happened in your application landscape. This is useful when your application comprises a large number of microservices.

Log Everything, Answer Anything – For Free

Falcon LogScale Community Edition (previously Humio) offers a free modern log management platform for the cloud. Leverage streaming data ingestion to achieve instant visibility across distributed systems and prevent and resolve incidents.

Falcon LogScale Community Edition, available instantly at no cost, includes the following:

  • Ingest up to 16GB per day
  • 7-day retention
  • No credit card required
  • Ongoing access with no trial period
  • Index-free logging, real-time alerts and live dashboards
  • Access our marketplace and packages, including guides to build new packages
  • Learn and collaborate with an active community

Get Started Free