Wie lässt sich ein einfaches Logging realisieren?

Jenseits von Zusatzbibliotheken wie z.B. Log4J besitzt Java mit dem Package java.util.logging ab der Version 1.4 selbst eine für viele Fälle ausreichende API zum Protokollieren von Programmabläufen.

Unter Logging versteht man das Protokollieren eines Softwareablaufes. Ein solches Protokoll entsteht nicht automatisch, sondern muss vom Entwickler eingerichtet werden. Üblicherweise werden dabei Zwischenzustände eines Programmablaufes notiert. Für die Ausgabe des Logprotokolls können dabei unterschiedliche Ziele (Konsole, Datei, etc.) verwendet werden.

Die Java-Logging-API stellt für viele Fälle ausreichend flexible Möglichkeiten bereit, um Log-Ausgaben zu formatieren und zu konfigurieren. An dieser Stelle soll zunächst die Möglichkeit einer bewusst einfachen Ausgabe auf die Konsole nach System.err vorgestellt werden.

import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class LoggerBsp {

    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
        logger.setLevel(Level.ALL);

        logger.severe("Schwerwiegender Fehler");
        logger.warning("Warnung");
        logger.info("Information");
        logger.config("Konfigurationshinweis");
        logger.fine("Fein");
        logger.finer("Feiner");
        logger.finest("Am feinsten");
    }
}

Die obige Beispielklasse enthält lediglich eine main()-Methode, die beim Ausführen des Programms einige Loggingausgaben auf die Konsole generiert.
Es wird zunächst ein Logger-Objekt durch die statische Methode getLogger() gebildet. Sie benötigt einen String-Parameter, der einen Bezeichner definiert, mit dem das Logger-Objekt angesprochen werden kann. Der Einfachheit halber wird hier zunächst der globale Loggername eingetragen, der durch die Konstante GLOBAL_LOGGER_NAME gesetzt wird. Im Produktionsumfeld wird hier üblicherweise ein voll qualifizierter Programm- oder Klassenname verwendet, um Eindeutigkeit innerhalb einer Logging-Hierarchie zu wahren. Im einfachsten Fall kann jedoch auch ein anonymer Logger verwendet werden. Er wird durch Logger.getAnonymousLogger() erzeugt.

Logging-Ebenen

Um Unterschiede in der Bedeutungsschwere einer Lognachricht zu erfassen, kann ein Logging auf verschiedenen Ebenen gleichzeitig stattfinden. Bei der Ausgabe des Loggings kann dann die gewünschte Ebene gewählt werden.
Java definiert hierzu die folgenden Grade als statische Felder (Konstanten) der Klasse java.util.logging.Level in absteigender Gewichtung:

Ergänzend existieren noch Level.ALL und Level.OFF, um alle Ebenen anzusprechen, bzw. das Logging vollständig abzuschalten.

Durch setLevel(Level.ALL) wird dem Logger-Objekt somit mitgeteilt, dass alle Logebenen angesprochen werden können. Das Setzen der Logebene an dieser Stelle bezieht sich jedoch nur auf das Erzeugen der Lognachrichten selbst, nicht auf deren Ausgabe. Diese ist Sache eines Handler-Objektes.

Wird ein Handler wie hier nicht explizit gesetzt, so wird der übergeordnete ParentHandler zur Ausgabesteuerung verwendet. Seine Standardeinstellung bewirkt eine Ausgabe aller Warnungen bis zu Level.INFO. Alle feineren Warnungen werden unterdrückt.

Erzeugung der Log-Nachricht

Die Erzeugung der Log-Nachricht selbst wird durch Methoden erzeugt, deren Bezeichner denjenigen der Loglevel entsprechen. Wie erwähnt werden auf diese Weise unterschiedliche Gewichtungen von Logausgaben generiert. Unter einer "unterschiedlichen Gewichtung" ist in diesem Fall die Bedeutungsschwere eines Ereignisses zu verstehen, das zu einem Logeintrag führen soll. Es ist Aufgabe des Entwicklers zu entscheiden, ob und an welchen Stellen des Programmablaufs ein Logging erfolgen und mit welcher Gewichtung dies stattfinden soll.

Jeder Loggingebene, mit Ausnahme von ALL und OFF, entsprechen zwei überladene, gleichnamige Methoden der Klasse Logger, die zur Erzeugung einer dem jeweiligen Grad entsprechenden Protokollnachricht dienen und diese an einen Handler weiterreichen. Die Nachricht selbst ist ein frei formulierbarer Text, der der Methode als String-Parameter übergeben wird. Die Methoden zur Ausgabe des Logtextes werden im Beispielprogramm auf dem erzeugten Logger-Objekt für jede Ebene ein Mal aufgerufen.

Die Ausführung des Programms erzeugt die nachstehende Ausgabe:

Okt 17, 2017 8:00:50 PM allgemeines.logging.LoggerBsp main
SCHWERWIEGEND: Schwerwiegender Fehler
Okt 17, 2017 8:00:50 PM allgemeines.logging.LoggerBsp main
WARNUNG: Warnung
Okt 17, 2017 8:00:50 PM allgemeines.logging.LoggerBsp main
INFORMATION: Information

Obwohl im Programm Ausgaben durch die erwähnten Methoden auf allen Leveln erzeugt werden, erfolgt offensichtlich nur eine Ausgabe der drei obersten Ebenen. Dies so zu erklären, dass der Logger zwar zur Ausgabe aller Meldungen konfiguriert wurde und diese auch erzeugt, der Handler jedoch alle Ausgaben ab Level.CONFIG einschließlich unterdrückt.

Erweitert man das Programm durch folgende Zeile

logger.getParent().getHandlers()[0].setLevel(Level.ALL);

so werden alle Warnungsebenen ausgegeben, da auch der ParentHandler nun alle an ihn übergebenen Nachrichten publiziert.
Umgekehrt kann durch

logger.setUseParentHandlers(false);

die Ausgabe des ParentHandlers vollständig unterdrückt werden. Ist kein Handler explizit gesetzt worden, so findet eine Ausgabe dann gar nicht mehr statt.

Der Handler

Ein Handler ist ein Objekt, das Art und Bedingungen der Ausgabe einer Lognachricht steuert. Die Klasse besitzt zwei Unterklassen, MemoryHandler und StreamHandler, von denen die zweite wiederum durch ConsoleHandler, FileHandler und SocketHandler erweitert wird. Wie die Bezeichner nahelegen, werden hierdurch die Besonderheiten einer Ausgabe auf das jeweilige Medium gesteuert.

Um die Funktionsweise zu verdeutlichen, die Ausgaberoutinen jedoch nicht unnötig zu verkomplizieren, wird im folgenden Beispiel nun ein ConsoleHandler erzeugt, der dem Logger-Objekt 'manuell' hinzugefügt wird. Die Ausgabe-Logebene wird hier ebenfalls zunächst auf Level.ALL gesetzt, um alle Ausgaben zu erhalten.
Hier der vollständige Quelltext nebst Ausgabe

public class LoggerBsp {

    public static void main(String[] args) {
        Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
        logger.setLevel(Level.ALL);
        Handler handler = new ConsoleHandler();
        handler.setLevel(Level.ALL);
        logger.addHandler(handler);

        logger.severe("Schwerwiegender Fehler");
        logger.warning("Warnung");
        logger.info("Information");
        logger.config("Konfigurationshinweis");
        logger.fine("Fein");
        logger.finer("Feiner");
        logger.finest("Am feinsten");
    }
}

// Ausgabe:            
            
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
SCHWERWIEGEND: Schwerwiegender Fehler
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
SCHWERWIEGEND: Schwerwiegender Fehler
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
WARNUNG: Warnung
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
WARNUNG: Warnung
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
INFORMATION: Information
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
INFORMATION: Information
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
KONFIGURATION: Konfigurationshinweis
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
FEIN: Fein
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
FEINER: Feiner
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
AM FEINSTEN: Am feinsten

Es ist zu erkennen, dass die vier der Ebene Level.INFO nachgeordneten Gewichtungen CONFIG, FINE, FINER und FINEST ein Mal, die Ebenen SEVERE, WARNING und INFO jedoch doppelt ausgegeben werden. Vor dem obigen Hintergrund ist dies leicht zu erklären: Die Ausgaben erfolgen ein Mal durch den explizit gesetzten ConsoleHandler, die zusätzlichen Ausgaben entstammen dem ParentHandler.
Unterdrückt man diesen wieder durch

logger.setUseParentHandlers(false);

so findet jede Ausgabe nur ein Mal durch den gesetzten Handler statt.
Um das Verfahren zusätzlich zu verdeutlichen, kann die Zuweisung der Logebene an den Handler, an den Logger oder an beide gleichzeitig durch setLevel() z.B. auf Leven.FINE gesetzt werden. Die Ausgaben sind dann in allen drei Fällen identisch:

Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
SCHWERWIEGEND: Schwerwiegender Fehler
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
WARNUNG: Warnung
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
INFORMATION: Information
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
KONFIGURATION: Konfigurationshinweis
Okt 17, 2017 7:57:36 PM allgemeines.logging.LoggerBsp main
FEIN: Fein

Die Begrenzung des Ausgabelevels wird dann entweder durch den Handler, den Logger oder durch beide vorgenommen.