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