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  // Contributors: Dan MacDonald <dan@redknee.com>
19  
20  package org.apache.log4j.net;
21  
22  import java.io.IOException;
23  import java.io.ObjectOutputStream;
24  import java.io.InterruptedIOException;
25  import java.net.InetAddress;
26  import java.net.Socket;
27  
28  import org.apache.log4j.AppenderSkeleton;
29  import org.apache.log4j.helpers.LogLog;
30  import org.apache.log4j.spi.ErrorCode;
31  import org.apache.log4j.spi.LoggingEvent;
32  
33  /**
34      Sends {@link LoggingEvent} objects to a remote a log server,
35      usually a {@link SocketNode}.
36  
37      <p>The SocketAppender has the following properties:
38  
39      <ul>
40  
41        <p><li>If sent to a {@link SocketNode}, remote logging is
42        non-intrusive as far as the log event is concerned. In other
43        words, the event will be logged with the same time stamp, {@link
44        org.apache.log4j.NDC}, location info as if it were logged locally by
45        the client.
46  
47        <p><li>SocketAppenders do not use a layout. They ship a
48        serialized {@link LoggingEvent} object to the server side.
49  
50        <p><li>Remote logging uses the TCP protocol. Consequently, if
51        the server is reachable, then log events will eventually arrive
52        at the server.
53  
54        <p><li>If the remote server is down, the logging requests are
55        simply dropped. However, if and when the server comes back up,
56        then event transmission is resumed transparently. This
57        transparent reconneciton is performed by a <em>connector</em>
58        thread which periodically attempts to connect to the server.
59  
60        <p><li>Logging events are automatically <em>buffered</em> by the
61        native TCP implementation. This means that if the link to server
62        is slow but still faster than the rate of (log) event production
63        by the client, the client will not be affected by the slow
64        network connection. However, if the network connection is slower
65        then the rate of event production, then the client can only
66        progress at the network rate. In particular, if the network link
67        to the the server is down, the client will be blocked.
68  
69        <p>On the other hand, if the network link is up, but the server
70        is down, the client will not be blocked when making log requests
71        but the log events will be lost due to server unavailability.
72  
73        <p><li>Even if a <code>SocketAppender</code> is no longer
74        attached to any category, it will not be garbage collected in
75        the presence of a connector thread. A connector thread exists
76        only if the connection to the server is down. To avoid this
77        garbage collection problem, you should {@link #close} the the
78        <code>SocketAppender</code> explicitly. See also next item.
79  
80        <p>Long lived applications which create/destroy many
81        <code>SocketAppender</code> instances should be aware of this
82        garbage collection problem. Most other applications can safely
83        ignore it.
84  
85        <p><li>If the JVM hosting the <code>SocketAppender</code> exits
86        before the <code>SocketAppender</code> is closed either
87        explicitly or subsequent to garbage collection, then there might
88        be untransmitted data in the pipe which might be lost. This is a
89        common problem on Windows based systems.
90  
91        <p>To avoid lost data, it is usually sufficient to {@link
92        #close} the <code>SocketAppender</code> either explicitly or by
93        calling the {@link org.apache.log4j.LogManager#shutdown} method
94        before exiting the application.
95  
96  
97       </ul>
98  
99      @author  Ceki G&uuml;lc&uuml;
100     @since 0.8.4 */
101 
102 public class SocketAppender extends AppenderSkeleton {
103 
104   /**
105      The default port number of remote logging server (4560).
106      @since 1.2.15
107   */
108   static public final int DEFAULT_PORT                 = 4560;
109 
110   /**
111      The default reconnection delay (30000 milliseconds or 30 seconds).
112   */
113   static final int DEFAULT_RECONNECTION_DELAY   = 30000;
114 
115   /**
116      We remember host name as String in addition to the resolved
117      InetAddress so that it can be returned via getOption().
118   */
119   String remoteHost;
120 
121   /**
122    * The MulticastDNS zone advertised by a SocketAppender
123    */
124   public static final String ZONE = "_log4j_obj_tcpconnect_appender.local.";
125 
126   InetAddress address;
127   int port = DEFAULT_PORT;
128   ObjectOutputStream oos;
129   int reconnectionDelay = DEFAULT_RECONNECTION_DELAY;
130   boolean locationInfo = false;
131   private String application;
132 
133   private Connector connector;
134 
135   int counter = 0;
136 
137   // reset the ObjectOutputStream every 70 calls
138   //private static final int RESET_FREQUENCY = 70;
139   private static final int RESET_FREQUENCY = 1;
140   private boolean advertiseViaMulticastDNS;
141   private ZeroConfSupport zeroConf;
142 
143   public SocketAppender() {
144   }
145 
146   /**
147      Connects to remote server at <code>address</code> and <code>port</code>.
148   */
149   public SocketAppender(InetAddress address, int port) {
150     this.address = address;
151     this.remoteHost = address.getHostName();
152     this.port = port;
153     connect(address, port);
154   }
155 
156   /**
157      Connects to remote server at <code>host</code> and <code>port</code>.
158   */
159   public SocketAppender(String host, int port) {
160     this.port = port;
161     this.address = getAddressByName(host);
162     this.remoteHost = host;
163     connect(address, port);
164   }
165 
166   /**
167      Connect to the specified <b>RemoteHost</b> and <b>Port</b>.
168   */
169   public void activateOptions() {
170     if (advertiseViaMulticastDNS) {
171       zeroConf = new ZeroConfSupport(ZONE, port, getName());
172       zeroConf.advertise();
173     }
174     connect(address, port);
175   }
176 
177   /**
178    * Close this appender.  
179    *
180    * <p>This will mark the appender as closed and call then {@link
181    * #cleanUp} method.
182    * */
183   synchronized public void close() {
184     if(closed)
185       return;
186 
187     this.closed = true;
188     if (advertiseViaMulticastDNS) {
189       zeroConf.unadvertise();
190     }
191 
192     cleanUp();
193   }
194 
195   /**
196    * Drop the connection to the remote host and release the underlying
197    * connector thread if it has been created 
198    * */
199   public void cleanUp() {
200     if(oos != null) {
201       try {
202 	oos.close();
203       } catch(IOException e) {
204           if (e instanceof InterruptedIOException) {
205               Thread.currentThread().interrupt();
206           }
207 	      LogLog.error("Could not close oos.", e);
208       }
209       oos = null;
210     }
211     if(connector != null) {
212       //LogLog.debug("Interrupting the connector.");
213       connector.interrupted = true;
214       connector = null;  // allow gc
215     }
216   }
217 
218   void connect(InetAddress address, int port) {
219     if(this.address == null)
220       return;
221     try {
222       // First, close the previous connection if any.
223       cleanUp();
224       oos = new ObjectOutputStream(new Socket(address, port).getOutputStream());
225     } catch(IOException e) {
226       if (e instanceof InterruptedIOException) {
227           Thread.currentThread().interrupt();
228       }
229       String msg = "Could not connect to remote log4j server at ["
230 	+address.getHostName()+"].";
231       if(reconnectionDelay > 0) {
232         msg += " We will try again later.";
233 	fireConnector(); // fire the connector thread
234       } else {
235           msg += " We are not retrying.";
236           errorHandler.error(msg, e, ErrorCode.GENERIC_FAILURE);
237       } 
238       LogLog.error(msg);
239     }
240   }
241 
242 
243   public void append(LoggingEvent event) {
244     if(event == null)
245       return;
246 
247     if(address==null) {
248       errorHandler.error("No remote host is set for SocketAppender named \""+
249 			this.name+"\".");
250       return;
251     }
252 
253     if(oos != null) {
254       try {
255     	 
256 	if(locationInfo) {
257 	   event.getLocationInformation();
258 	}
259     if (application != null) {
260         event.setProperty("application", application);
261     }
262     event.getNDC();
263     event.getThreadName();
264     event.getMDCCopy();
265     event.getRenderedMessage();
266     event.getThrowableStrRep();
267     
268 	oos.writeObject(event);
269 	//LogLog.debug("=========Flushing.");
270 	oos.flush();
271 	if(++counter >= RESET_FREQUENCY) {
272 	  counter = 0;
273 	  // Failing to reset the object output stream every now and
274 	  // then creates a serious memory leak.
275 	  //System.err.println("Doing oos.reset()");
276 	  oos.reset();
277 	}
278       } catch(IOException e) {
279           if (e instanceof InterruptedIOException) {
280               Thread.currentThread().interrupt();
281           }
282 	      oos = null;
283 	      LogLog.warn("Detected problem with connection: "+e);
284 	      if(reconnectionDelay > 0) {
285 	         fireConnector();
286 	      } else {
287 	         errorHandler.error("Detected problem with connection, not reconnecting.", e,
288 	               ErrorCode.GENERIC_FAILURE);
289 	      }
290       }
291     }
292   }
293 
294   public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
295     this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
296   }
297 
298   public boolean isAdvertiseViaMulticastDNS() {
299     return advertiseViaMulticastDNS;
300   }
301 
302   void fireConnector() {
303     if(connector == null) {
304       LogLog.debug("Starting a new connector thread.");
305       connector = new Connector();
306       connector.setDaemon(true);
307       connector.setPriority(Thread.MIN_PRIORITY);
308       connector.start();
309     }
310   }
311 
312   static
313   InetAddress getAddressByName(String host) {
314     try {
315       return InetAddress.getByName(host);
316     } catch(Exception e) {
317       if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
318           Thread.currentThread().interrupt();
319       }
320       LogLog.error("Could not find address of ["+host+"].", e);
321       return null;
322     }
323   }
324 
325   /**
326    * The SocketAppender does not use a layout. Hence, this method
327    * returns <code>false</code>.  
328    * */
329   public boolean requiresLayout() {
330     return false;
331   }
332 
333   /**
334    * The <b>RemoteHost</b> option takes a string value which should be
335    * the host name of the server where a {@link SocketNode} is
336    * running.
337    * */
338   public void setRemoteHost(String host) {
339     address = getAddressByName(host);
340     remoteHost = host;
341   }
342 
343   /**
344      Returns value of the <b>RemoteHost</b> option.
345    */
346   public String getRemoteHost() {
347     return remoteHost;
348   }
349 
350   /**
351      The <b>Port</b> option takes a positive integer representing
352      the port where the server is waiting for connections.
353    */
354   public void setPort(int port) {
355     this.port = port;
356   }
357 
358   /**
359      Returns value of the <b>Port</b> option.
360    */
361   public int getPort() {
362     return port;
363   }
364 
365   /**
366      The <b>LocationInfo</b> option takes a boolean value. If true,
367      the information sent to the remote host will include location
368      information. By default no location information is sent to the server.
369    */
370   public void setLocationInfo(boolean locationInfo) {
371     this.locationInfo = locationInfo;
372   }
373 
374   /**
375      Returns value of the <b>LocationInfo</b> option.
376    */
377   public boolean getLocationInfo() {
378     return locationInfo;
379   }
380 
381   /**
382    * The <b>App</b> option takes a string value which should be the name of the 
383    * application getting logged.
384    * If property was already set (via system property), don't set here.
385    * @since 1.2.15
386    */
387   public void setApplication(String lapp) {
388     this.application = lapp;
389   }
390 
391   /**
392    *  Returns value of the <b>Application</b> option.
393    * @since 1.2.15
394    */
395   public String getApplication() {
396     return application;
397   }
398 
399   /**
400      The <b>ReconnectionDelay</b> option takes a positive integer
401      representing the number of milliseconds to wait between each
402      failed connection attempt to the server. The default value of
403      this option is 30000 which corresponds to 30 seconds.
404 
405      <p>Setting this option to zero turns off reconnection
406      capability.
407    */
408   public void setReconnectionDelay(int delay) {
409     this.reconnectionDelay = delay;
410   }
411 
412   /**
413      Returns value of the <b>ReconnectionDelay</b> option.
414    */
415   public int getReconnectionDelay() {
416     return reconnectionDelay;
417   }
418 
419   /**
420      The Connector will reconnect when the server becomes available
421      again.  It does this by attempting to open a new connection every
422      <code>reconnectionDelay</code> milliseconds.
423 
424      <p>It stops trying whenever a connection is established. It will
425      restart to try reconnect to the server when previously open
426      connection is droppped.
427 
428      @author  Ceki G&uuml;lc&uuml;
429      @since 0.8.4
430   */
431   class Connector extends Thread {
432 
433     boolean interrupted = false;
434 
435     public
436     void run() {
437       Socket socket;
438       while(!interrupted) {
439 	try {
440 	  sleep(reconnectionDelay);
441 	  LogLog.debug("Attempting connection to "+address.getHostName());
442 	  socket = new Socket(address, port);
443 	  synchronized(this) {
444 	    oos = new ObjectOutputStream(socket.getOutputStream());
445 	    connector = null;
446 	    LogLog.debug("Connection established. Exiting connector thread.");
447 	    break;
448 	  }
449 	} catch(InterruptedException e) {
450 	  LogLog.debug("Connector interrupted. Leaving loop.");
451 	  return;
452 	} catch(java.net.ConnectException e) {
453 	  LogLog.debug("Remote host "+address.getHostName()
454 		       +" refused connection.");
455 	} catch(IOException e) {
456         if (e instanceof InterruptedIOException) {
457             Thread.currentThread().interrupt();
458         }
459 	    LogLog.debug("Could not connect to " + address.getHostName()+
460 		       ". Exception is " + e);
461 	}
462       }
463       //LogLog.debug("Exiting Connector.run() method.");
464     }
465 
466     /**
467        public
468        void finalize() {
469        LogLog.debug("Connector finalize() has been called.");
470        }
471     */
472   }
473 
474 }