Grundlagen: Logging in Java SE
Zunächst einmal rufen wir uns ins Gedächtnis, wie in Java SE Logging Frameworks arbeiten:
+----------+ 1+-------------------+ | Registry |----| Logger-Hierarchie | (Singletons) +----------+1 | mit LogLevel | 1| +-------------------+ | getLogger *| +--------+ | Logger | (einer pro Klasse) +--------+ | | doLog | +------------+ | LogHandler | (einer pro Logging-Senke) +------------+
Auf diese Weise kann sehr feingranular und bis auf Klassenebene bestimmt werden, in welcher Tiefe und zu welchen Senken geloggt werden soll. LogLevel können zur Laufzeit geändert werden. Solche Änderungen werden in der Registry an einer zentralen Stelle wirksam. Die Synchronisation beim Aufbau der Logger-Hierarchie, beim Fesstellen des LogLevels und beim Aufrufen der LogHandler ist in den schwach nebenläufigen SE Umgebungen niemals ein Problem. Es wundert deshalb nicht, dass das beschriebene Vorgehen in den meisten Logging Frameworks so oder mit leichten Abweichungen zu finden ist.
Klassische Logging Frameworks in Java EE Umgebungen
Klassische Logging-Frameworks in Java EE Umgebungen sind aus mehreren Gründen problematisch. Sorgfältig durchgeführte Lasttests können diese Probleme rechtzeitig sichtbar machen:
Logging-Senken in Java EE Umgebungen werden deshalb über skalierende Connection-Pools bereitgestellt. LogHandler holen sich die Referenz auf die Logging-Senke über einen JNDI Lookup. Einige Logging Frameworks bringen solche Implementierungen schon fertig mit, so gibt es beispielsweise für log4j eine Appender-Implementierung auf der Basis einer im JNDI hinterlegten Datasource.
Logging-Senken in Java EE Umgebungen
Was kommt als Logging-Senke dann alles in Frage:
Datenbanken sind normalerweise hochverfügbar und gut skalierende transaktionale Ressourcen, die für den nebenläufigen Einsatz bei hohem Durchsatz designed sind. Sie sind deshalb ideale Kandidaten für Logging-Senken und in Java EE Umgebungen leicht als Ressourcen zu konfigurieren. Datenbanken werden dabei grundsätzlich über gepoolte Connections angesprochen. Als Datenbankschema ist dabei möglichst eine einfache Tabelle ohne Fremdschlüsselbeziehungen vorzusehen. Ein niedriges Isolationslevel ist unproblematisch und viele Datenbanken bieten die Möglichkeit, ein Schema als Archiv zu konfigurieren - dann sind nur Inserts und Selects zugunsten einer deutlich besseren Performanz erlaubt. Datenbanken vereinfachen die Auswertung von Logeinträgen signifikant und machen Echtzeit-Anwendungsmonitoring möglich. Darüber hinaus zentralisieren sie Logdaten und bieten eine gute Grundsicherheit.
Allerdings sind auch Nachteile zu berücksichtigen. Gewöhnlich sind die operativen Daten von den Logging-Daten getrennt zu halten. Für das Logging wird damit eine eigene Datenbank-Instanz, oder zumindest ein eigenes Schema benötigt. Das verursacht zusätzliche betriebliche Aufwände und erhöht die Komplexität der betrieblichen Umgebung der Anwendung. Wenn die Logging-Datenbank nicht mit dem Server zusammen betrieben werden kann (collocation) weil beispielsweise die Java EE Anwendung in einem Cluster betrieben wird, müssen Logeinträge über das Netzwerk versendet werden.
Message Queues sind ebenfalls ausgezeichnet skalierende Logging-Senken und zentralisieren die Erfassung von Logeinträgen. Speziel JMS konforme Implementierungen sind als Standard-Ressourcen in Java EE Umgebungen verfügbar und werden wie Datenbanken über gepoolte Connections angesprochen. Das gute Skalierverhalten resultiert aus der Tatsache, dass Message Queues Nachrichten lediglich empfangen, eine potentiell aufwändige Erfassung und Auswertung der Nachrichten erfolgt in nachgelagerten, von der operativen Umgebung entkoppelten Systemen.
Aus betrieblicher Sicht sind allerdings Message Queues noch aufwändiger als Datenbanken, denn neben den Queues müssen auch die Message Consumer als eigene Instanzen entwickelt, gewartet und betrieben werden. So gesehen sind die Message Queues garnicht die eigentlichen Logging-Senken, diese müssen nachgelagert bereitstehen und imstande sein, im statistischen Mittel Lognachrichten schneller abzuholen, als sie produziert werden. Wird die Java EE Anwendung in einem Cluster betrieben, müssen Lognachrichten über das Netzwerk versendet werden.
Als betrieblich leichtgewichtige Alternative zu Datenbanken und Message Queues kann FileIO zum Einsatz kommen. Außerdem ist FileIO hochverfügbar in dem Sinne, dass ein laufender Server ohne Filesystem nicht denkbar ist. Backupszenarien lassen sich auf der Basis einfacher Fileschnittstellen umsetzen. FileIO wird gewöhnlich auf dem lokalen Filesystem umgesetzt, auch im Clusterbetrieb erfolgt dann durch Logeinträge keine Netzwerkbelastung.
Problematisch an FileIO ist zunächst, dass es nicht Java EE konform ist. Dafür werden im Wesentlichen zwei Gründe genannt, die als Risiken entsprechend zu berücksichtigen sind:
JCA Outbound Resource Adapter abstrahieren eine Logging-Senke als externe Ressource. Sie repräsentieren eine Factory für Connections zu solchen Diensten. Der Container verwaltet solche Connections in Pools, damit kann eine Belastungsgrenze der externen Ressource festgelegt werden, das Gesamtsystem wird stabilisiert. Damit sind JCA Outbound Resource Adapter das Mittel der Wahl, wenn ein vorhandener externer Service als Logging-Senke angebunden werden soll. Beispiele sind ein HTTP/SOAP WebService und ein ERP System mit einer proprietären Schnittstelle.
Wie erwähnt ist Synchronisation in Servlets/JSP erlaubt und kollidiert nicht mit dem Thread-Handling des Webcontainers. Aber was geschieht, wenn im EJB Tier die ersten Deadlocks auftauchen und bewiesen werden kann, dass es im Zusammenhang mit dem eingesetzten Logging-Framework steht? Wie wird denn nun Logging im EJB Container richtig implementiert?
Der Paradigmenwechsel ist hier, dass von der Logger-per-Klasse Semantik hin zu einer Logger-per-EJBKomponente gewechselt werden muss. Die Logger-Instanz wird also im Kontext einer EJB bereitgestellt und mit einer Logging-Senke assoziiert, die als EJB Ressource konfiguriert ist. Logger-Instanzen werden pro Bean-Instanz nur einmal verwendet und insbesondere nicht in statischen Feldern der EJB abgelegt oder in einer Registry gecached.
Da EJBs vom Container garantiert single-threaded angesprochen werden, muss der Code in Logger-Implementierungen nicht synchronisiert werden (genauer: er darf es auch nicht). Die Loggerinstanz wird als Argument an die Metoden der Helperklassen der EJB weitergereicht. Das ist nicht elegant, aber die einzige Möglichkeit Helperklassen der Bean mit der Loggerinstanz zu versorgen. Leider gibt es kein den Request repräsentierendes Objekt (ähnlich HTTPRequest) an das man den Logger binden könnte (mit EJB3.1 steht mit InvocationContext genau ein solches Objekt möglicherweise bald zur Verfügung). Eine feingranulare Loggerhierarchie erreicht man, indem zu jedem Logaufruf ein Loghierarchie-Pfad mitgegeben wird. Typische Methoden des Loggers sind dann
Logger.log(level, logpath, message) Logger.log(level, logpath, throwable)
Logging-Senken werden hier über den JNDI Kontext der Bean angebunden (der JNDI Pfad beginnt mit java:comp/). Die Loglevel werden über die Initialisierungsparameter der Bean festgelegt und sind dann bei jedem Neustart der Anwendung änderbar.
Sollen zur Laufzeit Logger-Konfigurationen geändert werden (eine aus betrieblicher Sicht überaus attraktives Funktionalität), so geschieht das notgedrungen wieder über eine Registry. Synchronisierter Code ist dann wieder unvermeidlich.