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.FileHandlerjava.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 your data with CrowdStrike Falcon Next-Gen SIEM
Elevate your cybersecurity with the CrowdStrike Falcon® platform, the premier AI-native platform for SIEM and log management. Experience security logging at a petabyte scale, choosing between cloud-native or self-hosted deployment options. Log your data with a powerful, index-free architecture, without bottlenecks, allowing threat hunting with over 1 PB of data ingestion per day. Ensure real-time search capabilities to outpace adversaries, achieving sub-second latency for complex queries. Benefit from 360-degree visibility, consolidating data to break down silos and enabling security, IT, and DevOps teams to hunt threats, monitor performance, and ensure compliance seamlessly across 3 billion events in less than 1 second.