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