Tulis pelengkap khusus untuk log4j

Logging adalah proses sederhana untuk mencetak pesan dari berbagai jenis ke tempat yang diketahui. Pesan pencatatan dapat pergi ke konsol, ke file, ke monitor jarak jauh, atau di mana pun yang Anda rasa nyaman. Anggap logging sebagai saudara yang canggih dari:

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

Logging memiliki beberapa keunggulan dibandingkan yang sederhana

println()

pernyataan, bagaimanapun. Sistem pencatatan dapat menambahkan informasi kontekstual — nama file, nomor baris, dan tanggal, misalnya — ke pesan secara otomatis. Anda dapat mengarahkan pesan ke tujuan yang berbeda, atau mengubah formatnya, tanpa mengkompilasi ulang program Anda. (Di log4j, Anda cukup memodifikasi file properti sederhana.) Anda dapat dengan mudah mengaktifkan dan menonaktifkan kategori pesan sehingga Anda dapat melihat pesan debug saat Anda men-debug, tetapi dengan mudah menonaktifkannya jika tidak, misalnya.

Logging adalah pusat dari semua program saya. Saya menggunakannya untuk memantau kemajuan program saya saat bekerja. Saya menggunakannya untuk mencatat pesan kesalahan dari metode perpustakaan yang mungkin digunakan dalam konteks sisi server (di mana tidak ada konsol untuk mencetak jejak tumpukan). Yang terpenting, logging adalah salah satu alat debugging utama saya. Meskipun debugger visual kadang-kadang berguna, saya perhatikan bahwa saya dapat menemukan lebih banyak bug lebih cepat dengan menggabungkan pembacaan kode yang cermat dengan beberapa pesan logging yang ditempatkan dengan baik. (Saya tidak begitu yakin mengapa membaca / logging tampaknya lebih efektif daripada debugging visual, tetapi teori saya saat ini adalah bahwa debugger visual mempersempit fokus Anda ke satu utas kontrol melalui program, jadi Anda cenderung melewatkan bug yang tidak di utas itu.)

Logging sangat penting dalam proses debug sisi server, di mana, biasanya, tidak ada konsol, jadi System.outterbukti tidak berguna. Misalnya, Tomcat mengirim System.outke file lognya sendiri, jadi Anda tidak akan pernah melihat pesan yang dikirim ke sana kecuali Anda memiliki akses ke file log tersebut. Lebih tepatnya, Anda mungkin ingin memantau kinerja server dari tempat lain selain dari server itu sendiri. Memeriksa log server itu bagus, tetapi saya lebih suka melihat log di workstation saya.

Salah satu sistem logging yang lebih baik adalah proyek log4j Apache Software Foundation. Ini lebih fleksibel dan lebih mudah digunakan daripada API bawaan Java. Ini juga merupakan instalasi yang sepele — Anda cukup meletakkan file jar dan file konfigurasi sederhana di CLASSPATH Anda. (Sumber daya termasuk artikel pengantar yang bagus untuk log4j.) Log4j dapat diunduh gratis. Dokumentasi yang dipreteli tetapi memadai untuk pengguna akhir juga gratis. Tetapi Anda harus membayar 0 untuk dokumentasi lengkap, yang saya rekomendasikan.

Artikel ini akan membahas cara memperluas log4j dengan menambahkan appender baru — bagian dari sistem yang bertanggung jawab untuk mengirimkan pesan log ke suatu tempat. Appender yang saya diskusikan adalah versi ringan dari appender berbasis soket yang disertakan dengan log4j, tetapi Anda dapat dengan mudah menambahkan appender Anda sendiri untuk memasukkan pesan log ke dalam database atau direktori LDAP (protokol akses direktori ringan), membungkusnya dalam protokol berpemilik, mengarahkan mereka ke direktori tertentu, dan sebagainya.

Menggunakan log4J

Kode 1 menunjukkan bagaimana menggunakan log4j. Anda membuat

Logger

objek yang terkait dengan kelas saat ini. (Argumen string ke

getLogger()

sebenarnya sewenang-wenang, tetapi nama kelas sejauh ini merupakan nama yang paling berguna untuk logger.)

Kemudian, saat Anda ingin memasukkan pesan, Anda tinggal mengirimkannya ke logger. Pesan yang dicatat biasanya termasuk dalam salah satu dari lima kategori: debug, info, warn, error, atau fatal, dan metode bernama

debug()

,

info()

, dan seterusnya, tangani masing-masing. Setelah Anda selesai melakukan logging, ada baiknya untuk menutup subsistem logging dengan panggilan ke

shutdown()

(di dasar

main()

). Panggilan ini sangat penting untuk contoh yang akan saya bahas karena

shutdown()

secara tidak langsung menyebabkan koneksi soket ke klien jarak jauh untuk dimatikan secara tertib.

Kode 1. Test.java: Menggunakan kelas log4j

 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 } 

Satu-satunya bagian teka-teki lainnya adalah file konfigurasi sederhana, yang (untungnya) tidak dalam format XML. Ini adalah file properti sederhana, seperti yang ada di Listing 2.

Untuk memahami file tersebut, Anda perlu mengetahui sedikit tentang arsitektur logger. Logger membentuk hierarki runtime objek, diatur berdasarkan nama. Logger "root" berada di root hierarki, dan logger yang Anda buat berada di bawah root (dan satu sama lain), bergantung pada namanya. Misalnya, logger bernama ab berada di bawah logger bernama a , yang berada di bawah root.

Logger menulis string menggunakan dua kelas pembantu utama yang disebut appenders dan layout. Objek appender melakukan penulisan sebenarnya, dan objek tata letak memformat pesan. Appenders terikat ke logger pada waktu proses menggunakan informasi dalam file konfigurasi — dengan cara ini, Anda dapat mengubahnya tanpa kompilasi ulang. Logger tertentu dapat menggunakan beberapa appender, dalam hal ini, setiap appender mengirim pesan ke suatu tempat, sehingga menduplikasi pesan di beberapa tempat. Log4j hadir dengan beberapa appender yang melakukan hal-hal seperti konsol dan output file serta mengirim pesan logging menggunakan email atau JMS (Java Message Service). Log4j juga menyertakan appender berbasis soket yang mirip dengan yang saya ilustrasikan di artikel ini.

Objek tata letak, yang mengontrol pemformatan pesan, terikat ke appender pada waktu proses dengan cara yang mirip dengan logger dan appenders. Log4J hadir dengan beberapa kelas layout, yang diformat dalam XML, HTML, dan dengan printfformat seperti string. Saya telah menemukan ini cukup untuk sebagian besar kebutuhan saya.

Terakhir, penebang juga memiliki pemfilteran . Idenya adalah untuk menyaring, atau membuang, semua kategori pesan di bawah prioritas tertentu. Kategori yang saya sebutkan sebelumnya (debug, info, warn, error, atau fatal) berada dalam urutan prioritas. (Debug adalah yang terendah dan fatal, yang tertinggi.) Anda dapat memfilter semua pesan pada atau di bawah tingkat yang ditentukan cukup dengan memberi tahu logger untuk melakukannya — baik dalam kode Anda atau dalam file konfigurasi.

Beralih ke Daftar 2, baris pertama menentukan tingkat filter (

DEBUG

) dan pelengkap (

FILE

,

CONSOLE

, dan

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()

untuk memvalidasi nilai properti, tetapi di sini saya menggunakannya untuk benar-benar membuka soket sisi server pada nomor port yang ditentukan.

activateOptions()