·미리보기 | 소스복사·
- 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
·미리보기 | 소스복사·
- import java.net.*;
- import java.io.*;
-
- public class Client
- {
- public static void main(String[] args) throws Exception
- {
- Socket s;
- while( true )
- { try
- {
- s = new Socket( "localhost", 1234 );
- break;
- }
- catch( java.net.ConnectException e )
- {
-
- Thread.currentThread().sleep(50);
- }
- }
-
- BufferedReader in = new BufferedReader(
- new InputStreamReader( s.getInputStream() ) );
-
- String line;
- while( (line = in.readLine()) != null )
- System.err.println( line );
- }
- }
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()
to validate property values, but here I'm using it to actually open up a server-side socket at the specified port number.
activateOptions()
creates a thread that manages the server socket. The thread sits in an endless loop waiting for the client to connect. The accept()
call on line 56 blocks, or suspends, the thread until a connection is established; accept()
returns a socket connected to the client application. The thread is terminated in close()
, which we'll look at shortly, by closing the listenerSocket
object. Closing the socket causes a SocketException
to be thrown.
Once the connection is established, the thread wraps the output stream for the client socket in a Writer
and adds that Writer
to a Collection
called clients
. There's one Writer
in this Collection
for each client connected to the current appender.
The synchronization is worth mentioning. I put objects into clients
in the socket-management thread, but the collection is also used by whatever thread actually doing the logging.
The code that does the actual appending is in the append()
method (Listing 4, line 93). The appender first delegates message formatting to the layout object specified in the configuration file (on line 107) and then writes the message to the various client sockets on line 120. The code formats the message only once, but then iterates through the Writer
s for each client and sends the message to each of them.
That iteration is tricky. I could have wrapped the clients Collection
in a synchronized wrapper by calling Collections.synchronizedCollection()
and not explicitly synchronized. Had I done so, however, the add()
method back in the socket-management thread would have thrown an exception if an iteration was in progress when it tried to add a new client.
Since clients aren't added often, I've solved the problem simply by locking the clients Collection
manually while iterations are in progress. This way, the socket-management thread blocks until the iteration completes. The only downside to this solution is that a client might have to wait a while before the server accepts the connection. I haven't found this wait to be problematic.
The only appender method that remains is an override of close()
(Listing 4, line 143), which closes the server socket and cleans up (and closes) the client connections. As I mentioned earlier, closing the listenerSocket
terminates the thread that contains the accept()
loop.
Listing 4. RemoteAppender.java: A custom log4j appender
Conclusion
That's all there is to building an appender. You override a few methods of AppenderSkeleton
and add getter/setter methods for the parameters. Most of the nastiness here is socket-and-thread related. The actual log4j code was simplicity itself (extend a class and overwrite a few methods). Writing your own appenders for things like logging to a database, for example, is equally simple.