74d7186ea0711eb8ab66b31f2fb3954cf4fc0d7e
[jalview.git] / srcjar / org / apache / log4j / net / SocketHubAppender.java
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
162         LogLog.debug("closing SocketHubAppender " + getName());
163     this.closed = true;
164     if (advertiseViaMulticastDNS) {
165       zeroConf.unadvertise();
166     }
167     cleanUp();
168
169         LogLog.debug("SocketHubAppender " + getName() + " closed");
170   }
171
172   /**
173      Release the underlying ServerMonitor thread, and drop the connections
174      to all connected remote servers. */
175   public 
176   void cleanUp() {
177     // stop the monitor thread
178         LogLog.debug("stopping ServerSocket");
179     serverMonitor.stopMonitor();
180     serverMonitor = null;
181
182     // close all of the connections
183         LogLog.debug("closing client connections");
184     while (oosList.size() != 0) {
185       ObjectOutputStream oos = (ObjectOutputStream)oosList.elementAt(0);
186       if(oos != null) {
187         try {
188                 oos.close();
189         } catch(InterruptedIOException e) {
190             Thread.currentThread().interrupt();
191             LogLog.error("could not close oos.", e);
192         } catch(IOException e) {
193             LogLog.error("could not close oos.", e);
194         }
195         
196         oosList.removeElementAt(0);     
197       }
198     }
199   }
200
201   /**
202     Append an event to all of current connections. */
203   public
204   void append(LoggingEvent event) {
205     if (event != null) {
206       // set up location info if requested
207       if (locationInfo) {
208         event.getLocationInformation();
209       }
210       if (application != null) {
211           event.setProperty("application", application);
212         } 
213         event.getNDC();
214         event.getThreadName();
215         event.getMDCCopy();
216         event.getRenderedMessage();
217         event.getThrowableStrRep();
218         
219       if (buffer != null) {
220         buffer.add(event);
221       }
222     }
223
224     // if no event or no open connections, exit now
225     if ((event == null) || (oosList.size() == 0)) {
226       return;
227     }
228
229         // loop through the current set of open connections, appending the event to each
230     for (int streamCount = 0; streamCount < oosList.size(); streamCount++) {            
231
232       ObjectOutputStream oos = null;
233       try {
234         oos = (ObjectOutputStream)oosList.elementAt(streamCount);
235       }
236       catch (ArrayIndexOutOfBoundsException e) {
237         // catch this, but just don't assign a value
238         // this should not really occur as this method is
239         // the only one that can remove oos's (besides cleanUp).
240       }
241       
242       // list size changed unexpectedly? Just exit the append.
243       if (oos == null) {
244         break;
245     }
246         
247       try {
248         oos.writeObject(event);
249         oos.flush();
250         // Failing to reset the object output stream every now and
251         // then creates a serious memory leak.
252         // right now we always reset. TODO - set up frequency counter per oos?
253         oos.reset();
254       }
255       catch(IOException e) {
256         if (e instanceof InterruptedIOException) {
257             Thread.currentThread().interrupt();
258         }
259           // there was an io exception so just drop the connection
260         oosList.removeElementAt(streamCount);
261         LogLog.debug("dropped connection");
262         
263         // decrement to keep the counter in place (for loop always increments)
264         streamCount--;
265       }
266     }
267   }
268   
269   /**
270      The SocketHubAppender does not use a layout. Hence, this method returns
271      <code>false</code>. */
272   public
273   boolean requiresLayout() {
274     return false;
275   }
276   
277   /**
278      The <b>Port</b> option takes a positive integer representing
279      the port where the server is waiting for connections. */
280   public
281   void setPort(int _port) {
282     port = _port;
283         }
284
285   /**
286    * 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
287    * property), don't set here.
288    */
289   public 
290   void setApplication(String lapp) {
291     this.application = lapp;
292   }
293
294   /**
295    * Returns value of the <b>Application</b> option.
296    */
297   public 
298   String getApplication() {
299     return application;
300   }
301   
302   /**
303      Returns value of the <b>Port</b> option. */
304   public
305   int getPort() {
306     return port;
307   }
308
309   /**
310    * The <b>BufferSize</b> option takes a positive integer representing the number of events this appender will buffer and send to newly connected
311    * clients.
312    */
313   public 
314   void setBufferSize(int _bufferSize) {
315     buffer = new CyclicBuffer(_bufferSize);
316   }
317
318   /**
319    * Returns value of the <b>bufferSize</b> option.
320    */
321   public 
322   int getBufferSize() {
323     if (buffer == null) {
324       return 0;
325     } else {
326       return buffer.getMaxSize();
327     }
328   }
329   
330   /**
331      The <b>LocationInfo</b> option takes a boolean value. If true,
332      the information sent to the remote host will include location
333      information. By default no location information is sent to the server. */
334   public
335   void setLocationInfo(boolean _locationInfo) {
336     locationInfo = _locationInfo;
337   }
338   
339   /**
340      Returns value of the <b>LocationInfo</b> option. */
341   public
342   boolean getLocationInfo() {
343     return locationInfo;
344   }
345
346   public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
347     this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
348   }
349
350   public boolean isAdvertiseViaMulticastDNS() {
351     return advertiseViaMulticastDNS;
352   }
353
354   /**
355     Start the ServerMonitor thread. */
356   private
357   void startServer() {
358     serverMonitor = new ServerMonitor(port, oosList);
359   }
360   
361   /**
362    * Creates a server socket to accept connections.
363    * @param socketPort port on which the socket should listen, may be zero.
364    * @return new socket.
365    * @throws IOException IO error when opening the socket. 
366    */
367   protected ServerSocket createServerSocket(final int socketPort) throws IOException {
368       return new ServerSocket(socketPort);
369   }
370
371   /**
372     This class is used internally to monitor a ServerSocket
373     and register new connections in a vector passed in the
374     constructor. */
375   private class ServerMonitor implements Runnable {
376     private int port;
377     private Vector oosList;
378     private boolean keepRunning;
379     private Thread monitorThread;
380     
381     /**
382       Create a thread and start the monitor. */
383     public
384     ServerMonitor(int _port, Vector _oosList) {
385       port = _port;
386       oosList = _oosList;
387       keepRunning = true;
388       monitorThread = new Thread(this);
389       monitorThread.setDaemon(true);
390       monitorThread.setName("SocketHubAppender-Monitor-" + port);
391       monitorThread.start();
392     }
393     
394     /**
395       Stops the monitor. This method will not return until
396       the thread has finished executing. */
397     public synchronized void stopMonitor() {
398       if (keepRunning) {
399         LogLog.debug("server monitor thread shutting down");
400         keepRunning = false;
401         try {
402             if (serverSocket != null) {
403                 serverSocket.close();
404                 serverSocket = null;
405             }
406         } catch (IOException ioe) {}
407
408         try {
409           monitorThread.join();
410         }
411         catch (InterruptedException e) {
412             Thread.currentThread().interrupt();
413           // do nothing?
414         }
415         
416         // release the thread
417         monitorThread = null;
418         LogLog.debug("server monitor thread shut down");
419       }
420     }
421     
422     private 
423     void sendCachedEvents(ObjectOutputStream stream) throws IOException {
424       if (buffer != null) {
425         for (int i = 0; i < buffer.length(); i++) {
426           stream.writeObject(buffer.get(i));
427         }
428         stream.flush();
429         stream.reset();
430       }
431     }
432
433     /**
434       Method that runs, monitoring the ServerSocket and adding connections as
435       they connect to the socket. */
436     public
437     void run() {
438       serverSocket = null;
439       try {
440         serverSocket = createServerSocket(port);
441         serverSocket.setSoTimeout(1000);
442       }
443       catch (Exception e) {
444         if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
445             Thread.currentThread().interrupt();
446         }
447         LogLog.error("exception setting timeout, shutting down server socket.", e);
448         keepRunning = false;
449         return;
450       }
451
452       try {
453         try {
454                 serverSocket.setSoTimeout(1000);
455         }
456         catch (SocketException e) {
457           LogLog.error("exception setting timeout, shutting down server socket.", e);
458           return;
459         }
460       
461         while (keepRunning) {
462           Socket socket = null;
463           try {
464             socket = serverSocket.accept();
465           }
466           catch (InterruptedIOException e) {
467             // timeout occurred, so just loop
468           }
469           catch (SocketException e) {
470             LogLog.error("exception accepting socket, shutting down server socket.", e);
471             keepRunning = false;
472           }
473           catch (IOException e) {
474             LogLog.error("exception accepting socket.", e);
475           }
476                 
477           // if there was a socket accepted
478           if (socket != null) {
479             try {
480               InetAddress remoteAddress = socket.getInetAddress();
481               LogLog.debug("accepting connection from " + remoteAddress.getHostName() 
482                            + " (" + remoteAddress.getHostAddress() + ")");
483                         
484               // create an ObjectOutputStream
485               ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
486               if (buffer != null && buffer.length() > 0) {
487                 sendCachedEvents(oos);
488               }
489                     
490               // add it to the oosList.  OK since Vector is synchronized.
491               oosList.addElement(oos);
492             } catch (IOException e) {
493               if (e instanceof InterruptedIOException) {
494                     Thread.currentThread().interrupt();
495               }
496               LogLog.error("exception creating output stream on socket.", e);
497             }
498           }
499         }
500       }
501       finally {
502         // close the socket
503         try {
504                 serverSocket.close();
505         } catch(InterruptedIOException e) {
506             Thread.currentThread().interrupt();  
507         } catch (IOException e) {
508                 // do nothing with it?
509         }
510       }
511     }
512   }
513 }
514