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 }