Logging

Logging enables information about the current state and execution of a program to be outputted for debugging purposes. We already did use some limited form of logging utilizing System.out to write information to standard output. While this already is useful, this method has some drawbacks especifically in the context of larger, more complex programs:

  1. There is no mechansim for conveniently deciding what information should be logged. For example, during development we want to be able to see all possible log output while in production we would like to limit log output to the bare essentials to reduce the overhead of logging.
  2. All log output is printed to the console. This is often appropriate for debugging of a single-node application, but when building a distributed system or in production we may prefer writing log output to a file, possibly even rotating among a set of log files of fixed size, or send it to a server, or use multiple targets at once.

The purpose of logging frameworks is to allow this type of flexible logging. Java has a build-in logging framework in package java.logging. The framework consists of the following major compontents:

Loggers

Loggers create log messages. In java.util.logging loggers have names and are organized into a hierarchy based on their names. This hierarchy has a root called the root logger (its name is ""). The strucure of the hierarchy is determined based on logger names. A Logger's name is split into parts separated by .. For example, a logger named "A.B.Hello.C" would placed in this hierarchy as follows:

root -> A -> B -> Hello -> C

The hierarchy comes into play when configuring Loggers were decendents will per default inherit certain propoerties like formatting settings from their ancestors.

Formatters

Formatters take log messages created by Loggers and transform them into text. The default formatter logs the log message and additional information such as the level (see below) and the class and method from where the log message was created and a timestamp.

Handlers

Handlers are responsible for processing the log messages created by Loggers. For example, the build-in default handler for the root logger is a ConsoleHandler which writes to standard error and has its log level set to INFO (see below). To control how log messages are transformed into strings, you can set the formatter to be used with the handler with setFormatter. Handlers are associated with Logger by calling addHandler. A logger can have multiple handlers (which all would process messages from the logger). Per default log messages of a logger are also processed by handlers associated with ancestors of the logger.

Log Levels

Log levels allow log messages to be classified by their severity. For each logger you can set a log level and only messages with this log level or a more severe log level will be shown. These are the log levels supported by java.util.logging in decreasing order of severity:

  • SEVERE (highest value)
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST (lowest value)

Configuration Files

In addition to programtic configuration, the logging framework can also be configured from a java Properties object. A Properties object stores a set of key-value pairs which are Strings. The class provides methods to read properties from a text file where each key-value pair is given as a line key = value or an XML file. The LogManager classes documentation lists keys that are supported: https://docs.oracle.com/javase/6/docs/api/java/util/logging/LogManager.html.

In [3]:
package lecture;

import java.util.logging.*;

public class ClassWithLogging {

    private static final Logger log = Logger.getLogger(ClassWithLogging.class.getName());
    
    public static void someMethod () {
        log.finer("Some low priority message");
        log.info("a info level message");
        log.warning("a warning");
    }
    
    public static void main (String[] args) {
        Logger root = Logger.getLogger(""); // the root logger's name is ""
        Handler[] rootHandlers = root.getHandlers();
        for(Handler h: rootHandlers) { // remove the default handler from the root logger
            root.removeHandler(h);
        }
        
        // set log levels for loggers
        log.setLevel(Level.ALL); // show log messages of all level for our logger
        root.setLevel(Level.SEVERE); // show only SEVERE messages for the root logger
        
        // create our own handler
        ConsoleHandler handler = new ConsoleHandler(); // create a new console handler for this logger since the default one has log level INFO
        handler.setLevel(Level.ALL); // this handler does show all log messages passed from loggers
        root.addHandler(handler); // set this handler for the root logger, per default all other loggers inherit the handlers of their ancestors 

        // cause some logging
        someMethod();
        root.severe("this will be shown");
        root.info("but this will not!");
    }
}
Out[3]:
lecture.ClassWithLogging
In [2]:
import lecture.ClassWithLogging;

ClassWithLogging.main(null);
Feb 20, 2019 8:40:10 PM lecture.ClassWithLogging someMethod
FINER: Some low priority message
Feb 20, 2019 8:40:10 PM lecture.ClassWithLogging someMethod
INFO: a info level message
Feb 20, 2019 8:40:10 PM lecture.ClassWithLogging someMethod
WARNING: a warning
Feb 20, 2019 8:40:10 PM lecture.ClassWithLogging main
SEVERE: this will be shown
Out[2]:
null