View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    * 
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   * 
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.log4j.net;
19  
20  import java.io.IOException;
21  import java.io.InterruptedIOException;
22  import java.io.ObjectOutputStream;
23  import java.net.InetAddress;
24  import java.net.ServerSocket;
25  import java.net.Socket;
26  import java.net.SocketException;
27  import java.util.Vector;
28  
29  import org.apache.log4j.AppenderSkeleton;
30  import org.apache.log4j.helpers.CyclicBuffer;
31  import org.apache.log4j.helpers.LogLog;
32  import org.apache.log4j.spi.LoggingEvent;
33  
34  /**
35    Sends {@link LoggingEvent} objects to a set of remote log servers,
36    usually a {@link SocketNode SocketNodes}.
37      
38    <p>Acts just like {@link SocketAppender} except that instead of
39    connecting to a given remote log server,
40    <code>SocketHubAppender</code> accepts connections from the remote
41    log servers as clients.  It can accept more than one connection.
42    When a log event is received, the event is sent to the set of
43    currently connected remote log servers. Implemented this way it does
44    not require any update to the configuration file to send data to
45    another remote log server. The remote log server simply connects to
46    the host and port the <code>SocketHubAppender</code> is running on.
47    
48    <p>The <code>SocketHubAppender</code> does not store events such
49    that the remote side will events that arrived after the
50    establishment of its connection. Once connected, events arrive in
51    order as guaranteed by the TCP protocol.
52  
53    <p>This implementation borrows heavily from the {@link
54    SocketAppender}.
55  
56    <p>The SocketHubAppender has the following characteristics:
57    
58    <ul>
59    
60    <p><li>If sent to a {@link SocketNode}, logging is non-intrusive as
61    far as the log event is concerned. In other words, the event will be
62    logged with the same time stamp, {@link org.apache.log4j.NDC},
63    location info as if it were logged locally.
64    
65    <p><li><code>SocketHubAppender</code> does not use a layout. It
66    ships a serialized {@link LoggingEvent} object to the remote side.
67    
68    <p><li><code>SocketHubAppender</code> relies on the TCP
69    protocol. Consequently, if the remote side is reachable, then log
70    events will eventually arrive at remote client.
71    
72    <p><li>If no remote clients are attached, the logging requests are
73    simply dropped.
74    
75    <p><li>Logging events are automatically <em>buffered</em> by the
76    native TCP implementation. This means that if the link to remote
77    client is slow but still faster than the rate of (log) event
78    production, the application will not be affected by the slow network
79    connection. However, if the network connection is slower then the
80    rate of event production, then the local application can only
81    progress at the network rate. In particular, if the network link to
82    the the remote client is down, the application will be blocked.
83    
84    <p>On the other hand, if the network link is up, but the remote
85    client is down, the client will not be blocked when making log
86    requests but the log events will be lost due to client
87    unavailability. 
88  
89    <p>The single remote client case extends to multiple clients
90    connections. The rate of logging will be determined by the slowest
91    link.
92      
93    <p><li>If the JVM hosting the <code>SocketHubAppender</code> exits
94    before the <code>SocketHubAppender</code> is closed either
95    explicitly or subsequent to garbage collection, then there might
96    be untransmitted data in the pipe which might be lost. This is a
97    common problem on Windows based systems.
98    
99    <p>To avoid lost data, it is usually sufficient to {@link #close}
100   the <code>SocketHubAppender</code> either explicitly or by calling
101   the {@link org.apache.log4j.LogManager#shutdown} method before
102   exiting the application.
103   
104   </ul>
105      
106   @author Mark Womack */
107 
108 public class SocketHubAppender extends AppenderSkeleton {
109 
110   /**
111      The default port number of the ServerSocket will be created on. */
112   static final int DEFAULT_PORT = 4560;
113   
114   private int port = DEFAULT_PORT;
115   private Vector oosList = new Vector();
116   private ServerMonitor serverMonitor = null;
117   private boolean locationInfo = false;
118   private CyclicBuffer buffer = null;
119   private String application;
120   private boolean advertiseViaMulticastDNS;
121   private ZeroConfSupport zeroConf;
122 
123   /**
124    * The MulticastDNS zone advertised by a SocketHubAppender
125    */
126   public static final String ZONE = "_log4j_obj_tcpaccept_appender.local.";
127   private ServerSocket serverSocket;
128 
129 
130     public SocketHubAppender() { }
131 
132   /**
133      Connects to remote server at <code>address</code> and <code>port</code>. */
134   public
135   SocketHubAppender(int _port) {
136     port = _port;
137     startServer();
138   }
139 
140   /**
141      Set up the socket server on the specified port.  */
142   public
143   void activateOptions() {
144     if (advertiseViaMulticastDNS) {
145       zeroConf = new ZeroConfSupport(ZONE, port, getName());
146       zeroConf.advertise();
147     }
148     startServer();
149   }
150 
151   /**
152      Close this appender. 
153      <p>This will mark the appender as closed and
154      call then {@link #cleanUp} method. */
155   synchronized
156   public
157   void close() {
158     if(closed)
159       return;
160 
161 	LogLog.debug("closing SocketHubAppender " + getName());
162     this.closed = true;
163     if (advertiseViaMulticastDNS) {
164       zeroConf.unadvertise();
165     }
166     cleanUp();
167 
168 	LogLog.debug("SocketHubAppender " + getName() + " closed");
169   }
170 
171   /**
172      Release the underlying ServerMonitor thread, and drop the connections
173      to all connected remote servers. */
174   public 
175   void cleanUp() {
176     // stop the monitor thread
177 	LogLog.debug("stopping ServerSocket");
178     serverMonitor.stopMonitor();
179     serverMonitor = null;
180 
181     // close all of the connections
182 	LogLog.debug("closing client connections");
183     while (oosList.size() != 0) {
184       ObjectOutputStream oos = (ObjectOutputStream)oosList.elementAt(0);
185       if(oos != null) {
186         try {
187         	oos.close();
188         } catch(InterruptedIOException e) {
189             Thread.currentThread().interrupt();
190             LogLog.error("could not close oos.", e);
191         } catch(IOException e) {
192             LogLog.error("could not close oos.", e);
193         }
194         
195         oosList.removeElementAt(0);     
196       }
197     }
198   }
199 
200   /**
201     Append an event to all of current connections. */
202   public
203   void append(LoggingEvent event) {
204     if (event != null) {
205       // set up location info if requested
206       if (locationInfo) {
207         event.getLocationInformation();
208       }
209       if (application != null) {
210           event.setProperty("application", application);
211         } 
212         event.getNDC();
213         event.getThreadName();
214         event.getMDCCopy();
215         event.getRenderedMessage();
216         event.getThrowableStrRep();
217         
218       if (buffer != null) {
219         buffer.add(event);
220       }
221     }
222 
223     // if no event or no open connections, exit now
224     if ((event == null) || (oosList.size() == 0)) {
225       return;
226     }
227 
228 	// loop through the current set of open connections, appending the event to each
229     for (int streamCount = 0; streamCount < oosList.size(); streamCount++) {    	
230 
231       ObjectOutputStream oos = null;
232       try {
233         oos = (ObjectOutputStream)oosList.elementAt(streamCount);
234       }
235       catch (ArrayIndexOutOfBoundsException e) {
236         // catch this, but just don't assign a value
237         // this should not really occur as this method is
238         // the only one that can remove oos's (besides cleanUp).
239       }
240       
241       // list size changed unexpectedly? Just exit the append.
242       if (oos == null)
243         break;
244         
245       try {
246       	oos.writeObject(event);
247       	oos.flush();
248     	// Failing to reset the object output stream every now and
249     	// then creates a serious memory leak.
250     	// right now we always reset. TODO - set up frequency counter per oos?
251     	oos.reset();
252       }
253       catch(IOException e) {
254         if (e instanceof InterruptedIOException) {
255             Thread.currentThread().interrupt();
256         }
257           // there was an io exception so just drop the connection
258       	oosList.removeElementAt(streamCount);
259       	LogLog.debug("dropped connection");
260       	
261       	// decrement to keep the counter in place (for loop always increments)
262       	streamCount--;
263       }
264     }
265   }
266   
267   /**
268      The SocketHubAppender does not use a layout. Hence, this method returns
269      <code>false</code>. */
270   public
271   boolean requiresLayout() {
272     return false;
273   }
274   
275   /**
276      The <b>Port</b> option takes a positive integer representing
277      the port where the server is waiting for connections. */
278   public
279   void setPort(int _port) {
280     port = _port;
281 	}
282 
283   /**
284    * The <b>App</b> option takes a string value which should be the name of the application getting logged. If property was already set (via system
285    * property), don't set here.
286    */
287   public 
288   void setApplication(String lapp) {
289     this.application = lapp;
290   }
291 
292   /**
293    * Returns value of the <b>Application</b> option.
294    */
295   public 
296   String getApplication() {
297     return application;
298   }
299   
300   /**
301      Returns value of the <b>Port</b> option. */
302   public
303   int getPort() {
304     return port;
305   }
306 
307   /**
308    * The <b>BufferSize</b> option takes a positive integer representing the number of events this appender will buffer and send to newly connected
309    * clients.
310    */
311   public 
312   void setBufferSize(int _bufferSize) {
313     buffer = new CyclicBuffer(_bufferSize);
314   }
315 
316   /**
317    * Returns value of the <b>bufferSize</b> option.
318    */
319   public 
320   int getBufferSize() {
321     if (buffer == null) {
322       return 0;
323     } else {
324       return buffer.getMaxSize();
325     }
326   }
327   
328   /**
329      The <b>LocationInfo</b> option takes a boolean value. If true,
330      the information sent to the remote host will include location
331      information. By default no location information is sent to the server. */
332   public
333   void setLocationInfo(boolean _locationInfo) {
334     locationInfo = _locationInfo;
335   }
336   
337   /**
338      Returns value of the <b>LocationInfo</b> option. */
339   public
340   boolean getLocationInfo() {
341     return locationInfo;
342   }
343 
344   public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
345     this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
346   }
347 
348   public boolean isAdvertiseViaMulticastDNS() {
349     return advertiseViaMulticastDNS;
350   }
351 
352   /**
353     Start the ServerMonitor thread. */
354   private
355   void startServer() {
356     serverMonitor = new ServerMonitor(port, oosList);
357   }
358   
359   /**
360    * Creates a server socket to accept connections.
361    * @param socketPort port on which the socket should listen, may be zero.
362    * @return new socket.
363    * @throws IOException IO error when opening the socket. 
364    */
365   protected ServerSocket createServerSocket(final int socketPort) throws IOException {
366       return new ServerSocket(socketPort);
367   }
368 
369   /**
370     This class is used internally to monitor a ServerSocket
371     and register new connections in a vector passed in the
372     constructor. */
373   private class ServerMonitor implements Runnable {
374     private int port;
375     private Vector oosList;
376     private boolean keepRunning;
377     private Thread monitorThread;
378     
379     /**
380       Create a thread and start the monitor. */
381     public
382     ServerMonitor(int _port, Vector _oosList) {
383       port = _port;
384       oosList = _oosList;
385       keepRunning = true;
386       monitorThread = new Thread(this);
387       monitorThread.setDaemon(true);
388       monitorThread.setName("SocketHubAppender-Monitor-" + port);
389       monitorThread.start();
390     }
391     
392     /**
393       Stops the monitor. This method will not return until
394       the thread has finished executing. */
395     public synchronized void stopMonitor() {
396       if (keepRunning) {
397     	LogLog.debug("server monitor thread shutting down");
398         keepRunning = false;
399         try {
400             if (serverSocket != null) {
401                 serverSocket.close();
402                 serverSocket = null;
403             }
404         } catch (IOException ioe) {}
405 
406         try {
407           monitorThread.join();
408         }
409         catch (InterruptedException e) {
410             Thread.currentThread().interrupt();
411           // do nothing?
412         }
413         
414         // release the thread
415         monitorThread = null;
416     	LogLog.debug("server monitor thread shut down");
417       }
418     }
419     
420     private 
421     void sendCachedEvents(ObjectOutputStream stream) throws IOException {
422       if (buffer != null) {
423         for (int i = 0; i < buffer.length(); i++) {
424           stream.writeObject(buffer.get(i));
425         }
426         stream.flush();
427         stream.reset();
428       }
429     }
430 
431     /**
432       Method that runs, monitoring the ServerSocket and adding connections as
433       they connect to the socket. */
434     public
435     void run() {
436       serverSocket = null;
437       try {
438         serverSocket = createServerSocket(port);
439         serverSocket.setSoTimeout(1000);
440       }
441       catch (Exception e) {
442         if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
443             Thread.currentThread().interrupt();
444         }
445         LogLog.error("exception setting timeout, shutting down server socket.", e);
446         keepRunning = false;
447         return;
448       }
449 
450       try {
451     	try {
452         	serverSocket.setSoTimeout(1000);
453     	}
454     	catch (SocketException e) {
455           LogLog.error("exception setting timeout, shutting down server socket.", e);
456           return;
457     	}
458       
459     	while (keepRunning) {
460           Socket socket = null;
461           try {
462             socket = serverSocket.accept();
463           }
464           catch (InterruptedIOException e) {
465             // timeout occurred, so just loop
466           }
467           catch (SocketException e) {
468             LogLog.error("exception accepting socket, shutting down server socket.", e);
469             keepRunning = false;
470           }
471           catch (IOException e) {
472             LogLog.error("exception accepting socket.", e);
473           }
474 	        
475           // if there was a socket accepted
476           if (socket != null) {
477             try {
478               InetAddress remoteAddress = socket.getInetAddress();
479               LogLog.debug("accepting connection from " + remoteAddress.getHostName() 
480 			   + " (" + remoteAddress.getHostAddress() + ")");
481 	        	
482               // create an ObjectOutputStream
483               ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
484               if (buffer != null && buffer.length() > 0) {
485                 sendCachedEvents(oos);
486               }
487 	            
488               // add it to the oosList.  OK since Vector is synchronized.
489               oosList.addElement(oos);
490             } catch (IOException e) {
491               if (e instanceof InterruptedIOException) {
492                     Thread.currentThread().interrupt();
493               }
494               LogLog.error("exception creating output stream on socket.", e);
495             }
496           }
497         }
498       }
499       finally {
500     	// close the socket
501     	try {
502     		serverSocket.close();
503     	} catch(InterruptedIOException e) {
504             Thread.currentThread().interrupt();  
505         } catch (IOException e) {
506     		// do nothing with it?
507     	}
508       }
509     }
510   }
511 }
512