Schreiben Sie benutzerdefinierte Appender für log4j

Die Protokollierung ist der einfache Vorgang zum Drucken von Nachrichten verschiedener Art an bekannte Orte. Das Protokollieren von Nachrichten kann an eine Konsole, an eine Datei, an einen Remote-Monitor oder an einen anderen Ort gesendet werden, den Sie für zweckmäßig halten. Stellen Sie sich die Protokollierung als ein ausgeklügeltes Geschwister vor:

if( debug ) System.out.println("Debugging diagnostic"); 

Die Protokollierung hat gegenüber der einfachen mehrere Vorteile

println()

Aussagen jedoch. Das Protokollierungssystem kann der Nachricht automatisch Kontextinformationen hinzufügen, z. B. Dateiname, Zeilennummer und Datum. Sie können die Nachrichten an verschiedene Ziele umleiten oder die Formatierung ändern, ohne Ihr Programm neu zu kompilieren. (In log4j ändern Sie einfach eine einfache Eigenschaftendatei.) Sie können Kategorien von Nachrichten einfach ein- und ausschalten, sodass Sie Debug-Nachrichten beim Debuggen sehen können, aber sie können sie auch einfach deaktivieren, wenn Sie dies nicht sind.

Die Protokollierung ist für alle meine Programme von zentraler Bedeutung. Ich verwende es, um den Fortschritt meines Programms zu überwachen, während es funktioniert. Ich verwende es, um Fehlermeldungen von Bibliotheksmethoden zu protokollieren, die möglicherweise in einem serverseitigen Kontext verwendet werden (wo es keine Konsole gibt, auf der ein Stack-Trace gedruckt werden kann). Am wichtigsten ist, dass die Protokollierung eines meiner wichtigsten Debugging-Tools ist. Obwohl visuelle Debugger gelegentlich praktisch sind, habe ich festgestellt, dass ich mehr Fehler schneller finden kann, wenn ich das sorgfältige Lesen des Codes mit einigen gut platzierten Protokollierungsnachrichten kombiniere. (Ich bin mir nicht ganz sicher, warum das Lesen / Protokollieren effektiver erscheint als das visuelle Debuggen, aber meine derzeitige Theorie besagt, dass ein visueller Debugger Ihren Fokus auf einen einzelnen Kontrollfaden durch das Programm beschränkt, sodass Sie Fehler übersehen, die es nicht sind auf diesem Thread.)

Die Protokollierung ist beim serverseitigen Debuggen unerlässlich, bei dem normalerweise keine Konsole vorhanden ist, und System.outerweist sich daher als nutzlos. Beispielsweise sendet Tomcat System.outan eine eigene Protokolldatei, sodass Sie keine Nachrichten sehen, die dort gesendet werden, es sei denn, Sie haben Zugriff auf diese Protokolldatei. Genauer gesagt möchten Sie wahrscheinlich die Leistung eines Servers von einem anderen Ort als dem Server selbst überwachen. Das Überprüfen von Serverprotokollen ist nett, aber ich würde die Protokolle lieber auf meiner Workstation sehen.

Eines der besseren Protokollierungssysteme ist das log4j-Projekt der Apache Software Foundation. Es ist flexibler und benutzerfreundlicher als die in Java integrierten APIs. Es ist auch eine triviale Installation - Sie legen einfach eine JAR-Datei und eine einfache Konfigurationsdatei auf Ihrem CLASSPATH ab. (Ressourcen enthalten einen guten Einführungsartikel zu log4j.) Log4j ist ein kostenloser Download. Die abgespeckte, aber für den Endbenutzer angemessene Dokumentation ist ebenfalls kostenlos. Aber Sie müssen 0 für die vollständige Dokumentation bezahlen, die ich empfehle.

In diesem Artikel wird erläutert, wie Sie log4j erweitern, indem Sie einen neuen Appender hinzufügen - den Teil des Systems, der für das tatsächliche Senden der Protokollnachrichten an einen bestimmten Ort verantwortlich ist. Der Appender, den ich diskutiere, ist eine kompakte Version des Socket-basierten Appenders, der mit log4j geliefert wird. Sie können jedoch problemlos eigene Appender hinzufügen, um Protokollnachrichten in eine Datenbank oder ein LDAP-Verzeichnis (Lightweight Directory Access Protocol) zu stellen und in proprietäre Protokolle zu verpacken. Leiten Sie sie in bestimmte Verzeichnisse usw. weiter.

Log4J verwenden

Listing 1 zeigt die Verwendung von log4j. Sie erstellen eine

Logger

Objekt, das der aktuellen Klasse zugeordnet ist. (Das String-Argument zu

getLogger()

ist eigentlich willkürlich, aber der Klassenname ist bei weitem der nützlichste Name für den Logger.)

Wenn Sie dann eine Nachricht protokollieren möchten, senden Sie sie einfach an den Logger. Protokollierte Nachrichten fallen normalerweise in eine von fünf Kategorien: Debug, Info, Warnung, Fehler oder schwerwiegend und benannte Methoden

debug()

,

info()

und so weiter, behandeln Sie jede dieser. Wenn Sie mit der Protokollierung fertig sind, können Sie das Protokollierungssubsystem mit einem Aufruf von herunterfahren

shutdown()

(am Ende von

main()

). Dieser Aufruf ist besonders wichtig für das Beispiel, das ich behandeln werde, weil

shutdown()

Indirekt werden Socket-Verbindungen zu Remote-Clients ordnungsgemäß heruntergefahren.

Listing 1. Test.java: Verwenden der log4j-Klassen

 1 import org.apache.log4j.Logger; 2 import org.apache.log4j.LogManager; 3 4 public class Test 5 { 6 private static final Logger log = Logger.getLogger( "com.holub.log4j.Test"); 7 8 public static void main(String[] args) throws Exception 9 { 10 // For testing, give the client that will display the 11 // logged messages a moment to connect. 12 // (It's in a 50-ms wait loop, so pausing for 13 // 100 ms should do it). 14 Thread.currentThread().sleep( 100 ); 15 16 log.debug("Debug Message"); 17 log.warn ("Warning Message"); 18 log.error("Error Message"); 19 20 Thread.currentThread().sleep( 100 ); 21 LogManager.shutdown(); 22 } 23 } 

Das einzige andere Puzzleteil ist eine einfache Konfigurationsdatei, die (zum Glück) nicht im XML-Format vorliegt. Es ist eine einfache Eigenschaftendatei wie die in Listing 2.

Um die Datei zu verstehen, müssen Sie ein wenig über die Logger-Architektur wissen. Logger bilden eine Laufzeithierarchie von Objekten, die nach Namen organisiert sind. Der "Root" -Logger befindet sich am Stamm der Hierarchie, und die von Ihnen erstellten Logger befinden sich abhängig von ihren Namen unter dem Stamm (und untereinander). Ein Logger mit dem Namen ab befindet sich beispielsweise unter dem Logger mit dem Namen a , der sich unter der Wurzel befindet.

Logger schreiben Zeichenfolgen mit zwei Haupthilfeklassen, die als Appender und Layouts bezeichnet werden. Ein Appender-Objekt schreibt tatsächlich, und ein Layout-Objekt formatiert die Nachricht. Appender werden zur Laufzeit mithilfe der Informationen in der Konfigurationsdatei an einen Logger gebunden. Auf diese Weise können Sie sie ändern, ohne sie neu zu kompilieren. Ein bestimmter Logger kann mehrere Appender verwenden. In diesem Fall sendet jeder Appender die Nachricht irgendwohin, wodurch Nachrichten an mehreren Stellen dupliziert werden. Log4j wird mit mehreren Appendern geliefert, die beispielsweise Konsolen- und Dateiausgaben ausführen und Protokollierungsnachrichten per E-Mail oder JMS (Java Message Service) senden. Log4j enthält auch einen Socket-basierten Appender, der dem in diesem Artikel dargestellten ähnelt.

Layoutobjekte, die die Nachrichtenformatierung steuern, werden zur Laufzeit ähnlich wie Logger und Appender an Appender gebunden. Log4J enthält mehrere Layoutklassen, die in XML, HTML und mithilfe einer printfähnlichen Formatzeichenfolge formatiert werden. Ich habe festgestellt, dass diese für die meisten meiner Bedürfnisse angemessen sind.

Schließlich haben Logger auch Filterung . Die Idee ist, alle Kategorien von Nachrichten unterhalb einer bestimmten Priorität herauszufiltern oder zu verwerfen. Die zuvor erwähnten Kategorien (Debug, Info, Warnung, Fehler oder schwerwiegend) sind in der Prioritätsreihenfolge. (Debug ist das niedrigste und schwerwiegendste, das höchste.) Sie können alle Nachrichten auf oder unter einer bestimmten Ebene filtern, indem Sie den Logger dazu auffordern - entweder in Ihrem Code oder in der Konfigurationsdatei.

In Listing 2 gibt die erste Zeile die Filterstufe an (

DEBUG

) und die Appender (

FILE

,

CONSOLE

, und

REMOTE

) attached to the root logger. All loggers beneath the root in the runtime hierarchy inherit this filter level and these appenders, so this line effectively controls logging for the whole program (unless you use a more complex configuration file to specify something different).

The remainder of the configuration file specifies properties for the appenders. For example, Listing 2's second line says that the file appender named

FILE

is an instance of the

com.apache.log4j.FileAppender

class. Subsequent lines initialize this appender object when it's created—in this case, passing it the name of the file in which it will put the log messages, the layout object to use, and a format string for that layout object.

The rest of the configuration file does the same for the other appenders. The

CONSOLE

appender sends messages to the console, and the

REMOTE

appender sends messages down a socket. (We'll look at the source code for the

REMOTE

appender shortly.)

At runtime, log4j creates all the required classes for you, hooks them up as necessary, and passes the arguments you specify in the configuration file to the newly created objects using JavaBean-style "setter" methods.

Listing 2. log4j.properties: A log4j configuration file

log4j.rootLogger=DEBUG, FILE, CONSOLE, REMOTE log4j.appender.FILE=org.apache.log4j.FileAppender log4j.appender.FILE.file=/tmp/logs/log.txt log4j.appender.FILE.layout=org.apache.log4j.PatternLayout log4j.appender.FILE.layout.ConversionPattern=[%d{MMM dd HH:mm:ss}] %-5p (%F:%L) - %m%n log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout log4j.appender.CONSOLE.layout.ConversionPattern=[%d{MMM dd HH:mm:ss}] %-5p (%F:%L) - %m%n log4j.appender.REMOTE=com.holub.log4j.RemoteAppender log4j.appender.REMOTE.Port=1234 log4j.appender.REMOTE.layout=org.apache.log4j.PatternLayout log4j.appender.REMOTE.layout.ConversionPattern=[%d{MMM dd HH:mm:ss}] %-5p (%F:%L) - %m%n 

Using a remote appender

One of log4j's major strengths is that the tool is easy to extend. My

RemoteAppender

extension provides a way to log messages across the network to a simple socket-based client application. Log4J actually comes with a means of doing remote logging (an appender called

SocketAppender

), but this default mechanism is too heavyweight for my needs. It requires you to have the log4j packages on the remote client, for example.

Log4j also comes with an elaborate standalone GUI called Chainsaw that you can use to view messages from a

SocketAppender

. But Chainsaw is also way more than I need and really badly documented to boot. (I've never have had the time or patience to figure out how to use Chainsaw.) In any event, I just wanted to watch debugging diagnostics scroll by on a console window as I tested. Chainsaw was way too much for this simple need.

Listing 3 shows a simple viewer application for my

RemoteAppender

. It's just a simple socket-based client application that waits in a loop until it can open a socket to the server application that logs the messages. (See

Resources

for a discussion of sockets and Java's socket APIs). The port number, which is hard-coded into this simple example (as

1234

) is passed to the server via the configuration file in Listing 2. Here's the relevant line:

log4j.appender.REMOTE.Port=1234 

The client application waits in a loop until it can connect to the server, and then it just reads messages from the server and prints them to the console. Nothing earth shattering. The client knows nothing about log4j—it just reads strings and prints them—so the coupling to the log4j systems is nonexistent. Launch the client with

java Client

and terminate it with a Ctrl-C.

Listing 3. Client.java: A client for viewing logging messages

 1 import java.net.*; 2 import java.io.*; 3 4 public class Client 5 { 6 public static void main(String[] args) throws Exception 7 { 8 Socket s; 9 while( true ) 10 { try 11 { 12 s = new Socket( "localhost", 1234 ); 13 break; 14 } 15 catch( java.net.ConnectException e ) 16 { // Assume that the host isn't available yet, wait 17 // a moment, then try again. 18 Thread.currentThread().sleep(50); 19 } 20 } 21 22 BufferedReader in = new BufferedReader( 23 new InputStreamReader( s.getInputStream() ) ); 24 25 String line; 26 while( (line = in.readLine()) != null ) 27 System.err.println( line ); 28 } 29 } 

Note, by the way, that the client in Listing 3 is a great example of when not to use Java's NIO (new input/output) classes. There's no need for asynchronous reading here, and NIO would complicate the application considerably.

The remote appender

All that's left is the appender itself, which manages the server-side socket and writes the output to the clients that connect to it. (Several clients can receive logging messages from the same appender simultaneously.) The code is in Listing 4.

Starting with the basic structure, the

RemoteAppender

extends log4j's

AppenderSkeleton

class, which does all of the boilerplate work of creating an appender for you. You must do two things to make an appender: First, if your appender needs to be passed arguments from the configuration file (like the port number), you need to provide a getter/setter function with the names

getXxx()

and

setXxx()

for a property named

Xxx

. I've done that for the

Port

property on line 41 of Listing 4.

Note that both the getter and setter methods are

private

. They're provided strictly for use by the log4j system when it creates and initializes this appender, and no other object in my program has any business accessing them. Making

getPort()

and

setPort() private

guarantees that normal code can't access the methods. Since log4j accesses these methods via the introspection APIs, it can ignore the

private

attribute. Unfortunately, I've noticed that private getters and setters work only in some systems. I have to redefine these fields as public to get the appender to work correctly under Linux, for example.

The second order of business is to override a few methods from the AppenderSkeleton superclass.

After log4j has parsed the configuration file and called any associated setters, the

activateOptions()

method (Listing 4, line 49) is called. You can use

activeOptions()

um Eigenschaftswerte zu validieren, aber hier verwende ich es, um tatsächlich einen serverseitigen Socket an der angegebenen Portnummer zu öffnen.

activateOptions()