001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 * 
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018// Contributors: Dan MacDonald <dan@redknee.com>
019
020package org.apache.log4j.net;
021
022import java.io.IOException;
023import java.io.ObjectOutputStream;
024import java.io.InterruptedIOException;
025import java.net.InetAddress;
026import java.net.Socket;
027
028import org.apache.log4j.AppenderSkeleton;
029import org.apache.log4j.helpers.LogLog;
030import org.apache.log4j.spi.ErrorCode;
031import org.apache.log4j.spi.LoggingEvent;
032
033/**
034    Sends {@link LoggingEvent} objects to a remote a log server,
035    usually a {@link SocketNode}.
036
037    <p>The SocketAppender has the following properties:
038
039    <ul>
040
041      <p><li>If sent to a {@link SocketNode}, remote logging is
042      non-intrusive as far as the log event is concerned. In other
043      words, the event will be logged with the same time stamp, {@link
044      org.apache.log4j.NDC}, location info as if it were logged locally by
045      the client.
046
047      <p><li>SocketAppenders do not use a layout. They ship a
048      serialized {@link LoggingEvent} object to the server side.
049
050      <p><li>Remote logging uses the TCP protocol. Consequently, if
051      the server is reachable, then log events will eventually arrive
052      at the server.
053
054      <p><li>If the remote server is down, the logging requests are
055      simply dropped. However, if and when the server comes back up,
056      then event transmission is resumed transparently. This
057      transparent reconneciton is performed by a <em>connector</em>
058      thread which periodically attempts to connect to the server.
059
060      <p><li>Logging events are automatically <em>buffered</em> by the
061      native TCP implementation. This means that if the link to server
062      is slow but still faster than the rate of (log) event production
063      by the client, the client will not be affected by the slow
064      network connection. However, if the network connection is slower
065      then the rate of event production, then the client can only
066      progress at the network rate. In particular, if the network link
067      to the the server is down, the client will be blocked.
068
069      <p>On the other hand, if the network link is up, but the server
070      is down, the client will not be blocked when making log requests
071      but the log events will be lost due to server unavailability.
072
073      <p><li>Even if a <code>SocketAppender</code> is no longer
074      attached to any category, it will not be garbage collected in
075      the presence of a connector thread. A connector thread exists
076      only if the connection to the server is down. To avoid this
077      garbage collection problem, you should {@link #close} the the
078      <code>SocketAppender</code> explicitly. See also next item.
079
080      <p>Long lived applications which create/destroy many
081      <code>SocketAppender</code> instances should be aware of this
082      garbage collection problem. Most other applications can safely
083      ignore it.
084
085      <p><li>If the JVM hosting the <code>SocketAppender</code> exits
086      before the <code>SocketAppender</code> is closed either
087      explicitly or subsequent to garbage collection, then there might
088      be untransmitted data in the pipe which might be lost. This is a
089      common problem on Windows based systems.
090
091      <p>To avoid lost data, it is usually sufficient to {@link
092      #close} the <code>SocketAppender</code> either explicitly or by
093      calling the {@link org.apache.log4j.LogManager#shutdown} method
094      before exiting the application.
095
096
097     </ul>
098
099    @author  Ceki G&uuml;lc&uuml;
100    @since 0.8.4 */
101
102public 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}