Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

Sponsored Links

Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs

Write custom appenders for log4j

Extend log4j to support lightweight over-the-network logging

  • Print
  • Feedback

Page 5 of 6

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 Writers 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

  1  package com.holub.log;
   2  
   3  import org.apache.log4j.AppenderSkeleton;
   4  import org.apache.log4j.spi.LoggingEvent;
   5  import org.apache.log4j.spi.ErrorCode;
   6  import org.apache.log4j.Layout;
   7  import org.apache.log4j.helpers.LogLog;
   8  
   9  import java.util.*;
  10  import java.io.*;
  11  import java.net.*;
  12  
  13  /** This appender works much like log4j's Socket Appender.
  14   *  The main difference is that it sends strings to
  15   *  remote clients rather than sending serialized
  16   *  LoggingEvent objects. This approach has the
  17   *  advantages of being considerably faster (serialization
  18   *  is not cheap) and of not requiring the client
  19   *  application to be coupled to log4j at all.
  20   *
  21   *  <p>This appender takes only one "parameter," which specifies
  22   *     the port number (defaults to 9999). Set it with:
  23   *  <PRE>
  24   *  log4j.appender.R=com.holub.log4j.RemoteAppender;
  25   *  ...
  26   *  log4j.appender.R.Port=1234
  27   *  </PRE>
  28   *
  29   */
  30  
  31  public class RemoteAppender extends AppenderSkeleton
  32  {
  33      // The iterator across the "clients" Collection must
  34      // support a "remove()" method.
  35  
  36      private Collection   clients = new LinkedList();
  37      private int          port    = 9999;
  38      private ServerSocket listenerSocket;
  39      private Thread       listenerThread;
  40  
  41      private void setPort(int port)  { this.port = port; }
  42      private int  getPort()          { return this.port; }
  43  
  44      public boolean requiresLayout(){ return true; }
  45  
  46      /** Called once all the options have been set. Starts
  47       *  listening for clients on the specified socket.
  48       */
  49      public void activateOptions()
  50      {   try
  51          {   listenerSocket  = new ServerSocket( port );
  52              listenerThread  = new Thread()
  53              {   public void run()
  54                  {   try
  55                      {   Socket clientSocket;    
  56                          while( (clientSocket = listenerSocket.accept()) != null )
  57                          {   // Create a (deliberately) unbuffered writer
  58                              // to talk to the client and add it to the
  59                              // collection of listeners.
  60  
  61                              synchronized( clients )
  62                              {   clients.add(
  63                                      new OutputStreamWriter(
  64                                          clientSocket.getOutputStream()) );
  65                              }
  66                          }
  67                      }
  68                      catch( SocketException e )
  69                      {   // Normal close operation. Doing nothing
  70                          // terminates the thread gracefully.
  71                      }
  72                      catch( IOException e )
  73                      {   // Other IO errors also kill the thread, but with
  74                          // a logged message.
  75                          errorHandler.error("I/O Exception in accept loop" + e );
  76                      }
  77                  }
  78              };
  79              listenerThread.setDaemon( true );
  80              listenerThread.start();
  81          }
  82          catch( IOException e )
  83          {   errorHandler.error("Can't open server socket: " + e );
  84          }
  85      }
  86  
  87      /** Actually do the logging. The AppenderSkeleton's 
  88       *  doAppend() method calls append() to do the
  89       *  actual logging after it takes care of required
  90       *  housekeeping operations.
  91       */
  92  
  93      public synchronized void append( LoggingEvent event )
  94      {   
  95          // If this Appender has been closed or if there are no
  96          // clients to service, just return.
  97  
  98          if( listenerSocket== null || clients.size() <= 0 )
  99              return;
 100  
 101          if( this.layout == null )
 102          {   errorHandler.error("No layout for appender " + name ,
 103                                  null, ErrorCode.MISSING_LAYOUT );
 104              return;
 105          }
 106  
 107          String message = this.layout.format(event);
 108  
 109          // Normally, an exception is thrown by the synchronized collection
 110          // when somebody (i.e., the listenerThread) tries to modify it
 111          // while iterations are in progress. The following synchronized
 112          // statement causes the listenerThread to block in this case,
 113          // but note that connections that can't be serviced quickly
 114          // enough might be refused.
 115  
 116          synchronized( clients )
 117          {   for( Iterator i = clients.iterator(); i.hasNext(); )
 118              {   Writer out = (Writer)( i.next() );
 119                  try
 120                  {   out.write( message, 0, message.length() );
 121                      out.flush();
 122                      // Boilerplate code: handle exceptions if not
 123                      // handled by layout object:
 124                      //
 125                      if( layout.ignoresThrowable() )
 126                      {   String[] messages = event.getThrowableStrRep();
 127                          if( messages != null )
 128                          {   for( int j = 0; j < messages.length; ++j )
 129                              {   out.write( messages[j], 0, messages[j].length() );
 130                                  out.write( '\n' );
 131                                  out.flush();
 132                              }
 133                          }
 134                      }
 135                  }
 136                  catch( IOException e )  // Assume that the write failed
 137                  {   i.remove();         // because the connection is closed.    
 138                  }
 139              }
 140          }
 141      }
 142  
 143      public synchronized void close()
 144      {   try
 145          {   
 146              if( listenerSocket == null ) // Already closed.
 147                  return;
 148              listenerSocket.close();     // Also kills listenerThread.
 149  
 150              // Now close all the client connections.
 151              for( Iterator i = clients.iterator(); i.hasNext(); )
 152              {   ((Writer) i.next()).close();
 153                  i.remove();
 154              }
 155  
 156              listenerThread.join();      // Wait for thread to die.
 157              listenerThread  = null;     // Allow everything to be
 158              listenerSocket  = null;     // garbage collected.
 159              clients         = null;
 160          }
 161          catch( Exception e )
 162          {   errorHandler.error("Exception while closing: " + e);
 163          }
 164      }
 165  }



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.

  • Print
  • Feedback

Resources