Start - Publikationen - Wissen - TOGAF - Impressum -

Grundlagen: Logging in Java SE


Zunächst einmal rufen wir uns ins Gedächtnis, wie in Java SE Logging Frameworks arbeiten:

  • Loggerinstanzen werden von einer Registry (einer LoggerFactory) geholt und als statische Felder vorgehalten. Dabei werden sie in einer Namens-Hierarchie organisiert und gecached. Gewöhnlich wird der Name der Klasse in der sie zum Einsatz kommen als Namens-ID benutzt, das ist aber kein Zwang und lediglich eine nützliche Konvention.
  • In der Registry werden explizit LogLevel für jede Hierarchieebene konfiguriert, oder der LogLevel wird aus der Namens-Hierarchie ermittelt.
  • LogHandler (oder LogAppender) werden für jede gewünschte Logging-Senke initialisiert. Das sind zum Beispiel FileIO, Sysout/Syserr-Streams oder Datenbank-Connections.
  • Loggerinstanzen werden mit einem oder mehreren LogHandlern assoziiert oder delegieren Logaufrufe an den übergeordneten Logger.
+----------+   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:

  1. Der synchronisierte Code des Logging-Frameworks kann mit der Threadverwaltung des EJB Containers kollidieren. Dabei kann es zu ungewöhnlich langen Abarbeitungszeiten kommen, schlimmstenfalls werden Deadlocks provoziert. Aus diesem Grunde verstößt der Einsatz von synchronisiertem Code gegen die EJB Programmierkonventionen. In der Praxis scheinen aber EJB Container problemlos den synchronisierten Code der gängigen Logging Frameworks zu verkraften. Im Webcontainer ist Synchronisation kein Problem, da im Kontext eines Servlets/JSP Nebenläufigkeit vom Webcontainer erwartet wird.
  2. Die Logging-Senken werden im Server hochgradig nebenläufig angesprochen. Man stelle sich zum Beispiel vor, dass die Anfragen von 20 Clients gleichzeitig verarbeitet werden und 20 mal 100 Logstatements pro Sekunde auf einen FileIO-Handler losgelassen werden. Wenn diese Last kritische Werte erreicht, kann die Logging-Senke der begrenzende Performanzfaktor der gesamten Anwendung werden. Dieses Problem ist dann schon weniger akademisch und wird in der betrieblichen Praxis beobachtet.

    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.

  3. Viele Frameworks cachen Referenzen der Logger-Hierarchie in statischen Feldern. Im Kontext einer EJB kann das problematisch sein, da nun potenziell viele Bean-Instanzen über ihre Logger auf ein und dieselbe Ressource zugreifen. Im Kontext einer EJB sollten Logging-Frameworks deshalb ihre Ressourcen aus dem JNDI Kontext des Java EE Servers (und nicht der Bean) holen. Anders ausgedrückt: der JNDI Pfad darf NICHT mit java:comp/env beginnen.

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:

  1. Das Filesystem ist nicht für den hochgradig nebenläufigen Einsatz konzipiert. Es ist aus diesem Grunde auch keine Ressource, die dem Container zur Verfügung steht. Inwieweit das Filesystem den Anforderungen beim produktiven Einsatz genügt, hängt von vielen, schwer zu beurteilenden Faktoren ab (Belastung durch andere Anwendungen, durch den Container selbst, Lastspitzen durch Batchverarbeitung etc.).
  2. FileIO in Java EE Umgebungen ist eine Sicherheitslücke.
Unter allen Umständen sollte FileIO basiertes Logging auf Massenspeichern erfolgen, die physikalisch oder virtuell vom Serverbetrieb entkoppelt sind. Darüber hinaus bedingt das lokale und damit dezentrale Logging erschwertes Echtzeit-Anwendungsmonitoring. Im Allgemeinen müssen die lokalen Dateien zunächst zusammengeführt und aufbereitet werden, bevor sie aussagekräftig werden. Die zusätzlichen Aufwände dieses "Mergings" dürfen keinesfalls unberücksichtigt bleiben. Zentralisiertes FileIO-Logging zu Netzwerkfilesystemen ist im Produktivbetrieb gewöhnlich nicht möglich, denn Netzwerkfilesystem-Services sind dafür nicht ausgelegt.

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.

Logging im EJB Tier


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.

copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved