Logging in Java Applications (Using java.util.logging Package)

Having a logging component within a library or service (especially in a microservices architecture) is useful to the downstream applications that use it. When properly set up, it allows them to choose their own logging implementation, and level of verbosity, without having to recompile the code.

While there are many different logging implementations (e.g. log4j, ACL, SLF4J), the Java SDK does come with its own logging package – java.util.logging. There has been plenty of debate on the flexibility, performance, and compatibility of the various logging implementations, but the concept is still the same.

In this post, I will explain how to get started with Java’s own logging package, java.util.logging, and illustrate how downstream applications are able to modify the verbosity of logs without recompiling the entire package/service.

Pre-Requisite

  • Java SDK 1.4 or later (I am using Java 9)
  • Familiarity with writing simple Java programs
  • Familiarity with compiling Java programs from CLI

Step 1: Setting the Context

Let’s say we are creating a calculator app in Java, and that I will be implementing a SquareRoot library that returns the square root of a given number. Here are the requirements:

  • Library Name: SquareRoot.jar
  • Logging Requirements
    • Log the outcome of the square root method
    • Log when an invalid result is returned
    • Log when a negative number is provided

Step 2: Understanding Logging Levels

Based on the logging requirements above, we can see that logs can (and should) have different levels of severity. Providing a negative number to the square root function is rather severe (and not be allowed); while logging the outcome of the square root method may be good for downstream debugging.

If we set all the logs to have the same level of severity, then we choose to either 1) be overwhelmed by the sheer amount of logs both during development and deployment of the application, or 2) have no logs at all that makes development and maintenance of the application tricky.

The java.util.logging.Level class defines several standard logging levels that are be useful to us. It is important to note that “enabling logging at a given level also enables logging at all higher levels.”

That is to say, if logging level is set to INFO, then

  • Messages with SEVERE, WARNING, and INFO level will be logged
  • Messages with CONFIG, FINE, FINER, FINEST will be ignored

It is important to note that there are two additional levels, ALL and OFF, which are rather self-descriptive.

Step 3: Directory Structure

Because we are creating Java libraries, the folder structure is rather important. I have set up my project root as follows:

  • ProjectRoot
    • source/
      • calculatorApp/
        • util/
          • SquareRootImpl.java (see step 4)
    • class/
    • jarLibs/

Step 4: Developing the Square Root Library

Below is a simple implementation of the Square Root Library:

package calculatorApp.util;

import java.lang.Math.sqrt;
import java.util.logging.*;

public class SquareRootImpl{ 

  private static final Logger classLogger = Logger.getLogger(SquareRootImpl.class.getName());

  public static int getSquareRoot(int input){
    int result = -1;
    if (input < 0){
      classLogger.log(Level.SEVERE, "Negative Number Provided!");
    } else {
      int rawResult = (int)sqrt(input);
      if (input != rawResult*rawResult){ 
        classLogger.log(Level.INFO, "No integer square root found");
        classLogger.log(Level.WARNING, "An invalid result is being returned");
      } else {
        result = rawResult;
        classLogger.log(Level.INFO, "Square root of " + input + " is +/-" + result);
      }
    }
    return result;
  }
}

Note:

  • I have placed the file within the source directory, within the corresponding package folder hierarchy (see step 3)
  • The default log output will be in the following format:
    date time className methodName
    your log message

    For example:

    May 19, 2018 1:11:48 PM SquareRootImpl getSquareRoot
    INFO: No integer square root found

Step 5: Creating the Java Library

Step 5.1: Compile Class With Package Folder Structure

Creating a Java Library is simply creating the .jar file with the appropriate folder hierarchy with respect to the package definition.

First, I compile the SquareRootImpl.java source code file. I use the -d flag to create the class file with the corresponding package folder structure.

ProjectRoot$ javac -d ./class ./source/calculatorApp/util/SquareRootImpl.java

My project hierarchy now looks like this:

  • ProjectRoot/
    • source/
      • calculatorApp/
        • util/
          • SquareRootImpl.java
    • class/
      • calculatorApp/
        • util/
          • SquareRootImpl.class
    • jarLibs/

Step 5.2: Create the Square Root Java Library

Then I use the jar command to create a .jar library. Note: you will need to run the jar command from the root of the class folder, so as to maintain the package folder hierarchy.

ProjectRoot$ cd class
ProjectRoot/class$ jar cf ../jarLibs/SquareRootLib.jar calculatorApp

My project hierarchy now looks like this:

  • ProjectRoot/
    • source/
      • calculatorApp/
        • util/
          • SquareRootImpl.java
    • class/
      • calculatorApp/
        • util/
          • SquareRootImpl.class
    • jarLibs/
      • SquareRootLib.jar

Step 6: Create the CalculatorApp Driver

In a separate project: Create a similar folder structure, and copy the SquareRootLib.jar. You should have something like the following:

  • NEWProjectRoot/
    • source/
    • class/
    • jarLibs/
      • SquareRootLib.jar

Next, create a simple CalculatorApp Driver (mine is called CalculatorImpl) in the source folder that imports the SquareRootLib library, and uses the getSquareRoot method, as defined earlier, with a command line argument.

package calculatorApp;
import calculatorApp.util.SquareRootImpl;
public class CalculatorImpl{
  public static void main(String args[]){
    int sqrtResult = SquareRootImpl.getSquareRoot(Integer.parseInt(args[0]));
  }
}

Compile the CalculatorImpl.java file, with the SquareRootImpl.jar on the classpath.

NEWProjectRoot$ javac -d ./class -cp "./jarLibs/SquareRootLib.jar" ./source/calculatorApp/CalculatorImpl.java

My project hierarchy now looks like this:

  • NEWProjectRoot/
    • source/
      • calculatorApp/
        • CalculatorImpl.java
    • class/
      • calculatorApp/
        • CalculatorImpl.class
    • jarLibs/
      • SquareRootLib.jar

Step 7: Run the CalculatorApp Driver (Optional)

We might want to test the CalculatorApp Driver before we continue, so that if there are errors/bugs, we can fix them before we introduce additional complexity.

Run the following commands to test the CalculatorApp Driver – note the errors messages that are printed on the console.

NEWProjectRoot$ java -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl 10
May 19, 2018 9:57:00 PM calculatorApp.util.SquareRootImpl getSquareRoot
INFO: No integer square root found
May 19, 2018 9:57:00 PM calculatorApp.util.SquareRootImpl getSquareRoot
WARNING: An invalid result is being returned

NEWProjectRoot$ java -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl -4
May 19, 2018 9:57:13 PM calculatorApp.util.SquareRootImpl getSquareRoot
SEVERE: Negative Number Provided!

NEWProjectRoot$ java -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl 4
May 19, 2018 9:57:16 PM calculatorApp.util.SquareRootImpl getSquareRoot
INFO: Square root of 4 is +/-2

Note that:

  • I have to use the full qualified name of the class, and
  • All log messages are being returned, regardless of their severity level

Step 8: Create an External Log Properties File

If you are the developer of the calculator app, the high verbosity level would be beneficial to debug and trace method calls between libraries (log level: INFO). However, an integration tester may be overwhelmed by all the log messages, especially when testing the full functionality with multiple libraries – she may only be interested in critical events (log level: WARNING/SEVERE).

Most logging components have the ability to read in logging properties through an external configuration file, and the inbuilt Java logging package is no different.

In the NEWProjectRoot, create an external log properties file (mine is called log.properties) with the basic log properties:

handlers= java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = INFO

This tutorial by Jenkov Aps contains a more comprehensive list of the various properties that can be defined: http://tutorials.jenkov.com/java-logging/configuration.html

Step 9: Change the Logging Severity Levels Without Recompiling

Run the calculator app driver with the external log properties file:

NEWProjectRoot$ java -Djava.util.logging.config.file=./log.properties -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl 10
May 19, 2018 9:57:00 PM calculatorApp.util.SquareRootImpl getSquareRoot
INFO: No integer square root found
May 19, 2018 9:57:00 PM calculatorApp.util.SquareRootImpl getSquareRoot
WARNING: An invalid result is being returned

NEWProjectRoot$ java -Djava.util.logging.config.file=./log.properties -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl -4
May 19, 2018 9:57:13 PM calculatorApp.util.SquareRootImpl getSquareRoot
SEVERE: Negative Number Provided!

NEWProjectRoot$ java -Djava.util.logging.config.file=./log.properties -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl 4
May 19, 2018 9:57:16 PM calculatorApp.util.SquareRootImpl getSquareRoot
INFO: Square root of 4 is +/-2

Note that there is no difference to the output as compared to Step 6, because the log message levels are higher than or equal to the log level we set (i.e. “INFO”).

Let’s change the level to be “WARNING”. Below is the new content of the log.properties file:

handlers= java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = WARNING

And rerun the calculator app driver:

NEWProjectRoot$ java -Djava.util.logging.config.file=./log.properties -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl 10
May 19, 2018 10:47:41 PM calculatorApp.util.SquareRootImpl getSquareRoot
WARNING: An invalid result is being returned

NEWProjectRoot$ java -Djava.util.logging.config.file=./log.properties -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl -4
May 19, 2018 10:47:45 PM calculatorApp.util.SquareRootImpl getSquareRoot
SEVERE: Negative Number Provided!

NEWProjectRoot$ java -Djava.util.logging.config.file=./log.properties -cp ./class;./jarLibs/SquareRootLib.jar calculatorApp.CalculatorImpl 4
(no output)

From the output above, we can see that the log messages with severity level “INFO” and below have been omitted.

By changing an external log properties file, I was able to change the logging levels/verbosity within the SquareRootLib.jar Java library without recompiling it.

Step 10: Conclusion

Logging events using a Logging component with different severity levels gives downstream applications the ability to customize their experience by filtering/enabling logs based on requirements, without recompiling any code.

Developers can deploy their application/services without worrying about log messages marring the user experience.

While Java comes with its own logging package, it is also possible to use other logging frameworks/libraries, depending on requirements (e.g. different severity levels, high logging volume).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s