--- /dev/null
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Contributors: Dan MacDonald <dan@redknee.com>
+
+package org.apache.log4j.net;
+
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.InterruptedIOException;
+import java.net.InetAddress;
+import java.net.Socket;
+
+import org.apache.log4j.AppenderSkeleton;
+import org.apache.log4j.helpers.LogLog;
+import org.apache.log4j.spi.ErrorCode;
+import org.apache.log4j.spi.LoggingEvent;
+
+/**
+ Sends {@link LoggingEvent} objects to a remote a log server,
+ usually a {@link SocketNode}.
+
+ <p>The SocketAppender has the following properties:
+
+ <ul>
+
+ <p><li>If sent to a {@link SocketNode}, remote logging is
+ non-intrusive as far as the log event is concerned. In other
+ words, the event will be logged with the same time stamp, {@link
+ org.apache.log4j.NDC}, location info as if it were logged locally by
+ the client.
+
+ <p><li>SocketAppenders do not use a layout. They ship a
+ serialized {@link LoggingEvent} object to the server side.
+
+ <p><li>Remote logging uses the TCP protocol. Consequently, if
+ the server is reachable, then log events will eventually arrive
+ at the server.
+
+ <p><li>If the remote server is down, the logging requests are
+ simply dropped. However, if and when the server comes back up,
+ then event transmission is resumed transparently. This
+ transparent reconneciton is performed by a <em>connector</em>
+ thread which periodically attempts to connect to the server.
+
+ <p><li>Logging events are automatically <em>buffered</em> by the
+ native TCP implementation. This means that if the link to server
+ is slow but still faster than the rate of (log) event production
+ by the client, the client will not be affected by the slow
+ network connection. However, if the network connection is slower
+ then the rate of event production, then the client can only
+ progress at the network rate. In particular, if the network link
+ to the the server is down, the client will be blocked.
+
+ <p>On the other hand, if the network link is up, but the server
+ is down, the client will not be blocked when making log requests
+ but the log events will be lost due to server unavailability.
+
+ <p><li>Even if a <code>SocketAppender</code> is no longer
+ attached to any category, it will not be garbage collected in
+ the presence of a connector thread. A connector thread exists
+ only if the connection to the server is down. To avoid this
+ garbage collection problem, you should {@link #close} the the
+ <code>SocketAppender</code> explicitly. See also next item.
+
+ <p>Long lived applications which create/destroy many
+ <code>SocketAppender</code> instances should be aware of this
+ garbage collection problem. Most other applications can safely
+ ignore it.
+
+ <p><li>If the JVM hosting the <code>SocketAppender</code> exits
+ before the <code>SocketAppender</code> is closed either
+ explicitly or subsequent to garbage collection, then there might
+ be untransmitted data in the pipe which might be lost. This is a
+ common problem on Windows based systems.
+
+ <p>To avoid lost data, it is usually sufficient to {@link
+ #close} the <code>SocketAppender</code> either explicitly or by
+ calling the {@link org.apache.log4j.LogManager#shutdown} method
+ before exiting the application.
+
+
+ </ul>
+
+ @author Ceki Gülcü
+ @since 0.8.4 */
+
+public class SocketAppender extends AppenderSkeleton {
+
+ /**
+ The default port number of remote logging server (4560).
+ @since 1.2.15
+ */
+ static public final int DEFAULT_PORT = 4560;
+
+ /**
+ The default reconnection delay (30000 milliseconds or 30 seconds).
+ */
+ static final int DEFAULT_RECONNECTION_DELAY = 30000;
+
+ /**
+ We remember host name as String in addition to the resolved
+ InetAddress so that it can be returned via getOption().
+ */
+ String remoteHost;
+
+ /**
+ * The MulticastDNS zone advertised by a SocketAppender
+ */
+ public static final String ZONE = "_log4j_obj_tcpconnect_appender.local.";
+
+ InetAddress address;
+ int port = DEFAULT_PORT;
+ ObjectOutputStream oos;
+ int reconnectionDelay = DEFAULT_RECONNECTION_DELAY;
+ boolean locationInfo = false;
+ private String application;
+
+ private Connector connector;
+
+ int counter = 0;
+
+ // reset the ObjectOutputStream every 70 calls
+ //private static final int RESET_FREQUENCY = 70;
+ private static final int RESET_FREQUENCY = 1;
+ private boolean advertiseViaMulticastDNS;
+ private ZeroConfSupport zeroConf;
+
+ public SocketAppender() {
+ }
+
+ /**
+ Connects to remote server at <code>address</code> and <code>port</code>.
+ */
+ public SocketAppender(InetAddress address, int port) {
+ this.address = address;
+ this.remoteHost = address.getHostName();
+ this.port = port;
+ connect(address, port);
+ }
+
+ /**
+ Connects to remote server at <code>host</code> and <code>port</code>.
+ */
+ public SocketAppender(String host, int port) {
+ this.port = port;
+ this.address = getAddressByName(host);
+ this.remoteHost = host;
+ connect(address, port);
+ }
+
+ /**
+ Connect to the specified <b>RemoteHost</b> and <b>Port</b>.
+ */
+ public void activateOptions() {
+ if (advertiseViaMulticastDNS) {
+ zeroConf = new ZeroConfSupport(ZONE, port, getName());
+ zeroConf.advertise();
+ }
+ connect(address, port);
+ }
+
+ /**
+ * Close this appender.
+ *
+ * <p>This will mark the appender as closed and call then {@link
+ * #cleanUp} method.
+ * */
+ synchronized public void close() {
+ if(closed) {
+ return;
+ }
+
+ this.closed = true;
+ if (advertiseViaMulticastDNS) {
+ zeroConf.unadvertise();
+ }
+
+ cleanUp();
+ }
+
+ /**
+ * Drop the connection to the remote host and release the underlying
+ * connector thread if it has been created
+ * */
+ public void cleanUp() {
+ if(oos != null) {
+ try {
+ oos.close();
+ } catch(IOException e) {
+ if (e instanceof InterruptedIOException) {
+ Thread.currentThread().interrupt();
+ }
+ LogLog.error("Could not close oos.", e);
+ }
+ oos = null;
+ }
+ if(connector != null) {
+ //LogLog.debug("Interrupting the connector.");
+ connector.interrupted = true;
+ connector = null; // allow gc
+ }
+ }
+
+ void connect(InetAddress address, int port) {
+ if(this.address == null) {
+ return;
+ }
+ try {
+ // First, close the previous connection if any.
+ cleanUp();
+ oos = new ObjectOutputStream(new Socket(address, port).getOutputStream());
+ } catch(IOException e) {
+ if (e instanceof InterruptedIOException) {
+ Thread.currentThread().interrupt();
+ }
+ String msg = "Could not connect to remote log4j server at ["
+ +address.getHostName()+"].";
+ if(reconnectionDelay > 0) {
+ msg += " We will try again later.";
+ fireConnector(); // fire the connector thread
+ } else {
+ msg += " We are not retrying.";
+ errorHandler.error(msg, e, ErrorCode.GENERIC_FAILURE);
+ }
+ LogLog.error(msg);
+ }
+ }
+
+
+ public void append(LoggingEvent event) {
+ if(event == null) {
+ return;
+ }
+
+ if(address==null) {
+ errorHandler.error("No remote host is set for SocketAppender named \""+
+ this.name+"\".");
+ return;
+ }
+
+ if(oos != null) {
+ try {
+
+ if(locationInfo) {
+ event.getLocationInformation();
+ }
+ if (application != null) {
+ event.setProperty("application", application);
+ }
+ event.getNDC();
+ event.getThreadName();
+ event.getMDCCopy();
+ event.getRenderedMessage();
+ event.getThrowableStrRep();
+
+ oos.writeObject(event);
+ //LogLog.debug("=========Flushing.");
+ oos.flush();
+ if(++counter >= RESET_FREQUENCY) {
+ counter = 0;
+ // Failing to reset the object output stream every now and
+ // then creates a serious memory leak.
+ //System.err.println("Doing oos.reset()");
+ oos.reset();
+ }
+ } catch(IOException e) {
+ if (e instanceof InterruptedIOException) {
+ Thread.currentThread().interrupt();
+ }
+ oos = null;
+ LogLog.warn("Detected problem with connection: "+e);
+ if(reconnectionDelay > 0) {
+ fireConnector();
+ } else {
+ errorHandler.error("Detected problem with connection, not reconnecting.", e,
+ ErrorCode.GENERIC_FAILURE);
+ }
+ }
+ }
+ }
+
+ public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
+ this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
+ }
+
+ public boolean isAdvertiseViaMulticastDNS() {
+ return advertiseViaMulticastDNS;
+ }
+
+ void fireConnector() {
+ if(connector == null) {
+ LogLog.debug("Starting a new connector thread.");
+ connector = new Connector();
+ connector.setDaemon(true);
+ connector.setPriority(Thread.MIN_PRIORITY);
+ connector.start();
+ }
+ }
+
+ static
+ InetAddress getAddressByName(String host) {
+ try {
+ return InetAddress.getByName(host);
+ } catch(Exception e) {
+ if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
+ Thread.currentThread().interrupt();
+ }
+ LogLog.error("Could not find address of ["+host+"].", e);
+ return null;
+ }
+ }
+
+ /**
+ * The SocketAppender does not use a layout. Hence, this method
+ * returns <code>false</code>.
+ * */
+ public boolean requiresLayout() {
+ return false;
+ }
+
+ /**
+ * The <b>RemoteHost</b> option takes a string value which should be
+ * the host name of the server where a {@link SocketNode} is
+ * running.
+ * */
+ public void setRemoteHost(String host) {
+ address = getAddressByName(host);
+ remoteHost = host;
+ }
+
+ /**
+ Returns value of the <b>RemoteHost</b> option.
+ */
+ public String getRemoteHost() {
+ return remoteHost;
+ }
+
+ /**
+ The <b>Port</b> option takes a positive integer representing
+ the port where the server is waiting for connections.
+ */
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ /**
+ Returns value of the <b>Port</b> option.
+ */
+ public int getPort() {
+ return port;
+ }
+
+ /**
+ The <b>LocationInfo</b> option takes a boolean value. If true,
+ the information sent to the remote host will include location
+ information. By default no location information is sent to the server.
+ */
+ public void setLocationInfo(boolean locationInfo) {
+ this.locationInfo = locationInfo;
+ }
+
+ /**
+ Returns value of the <b>LocationInfo</b> option.
+ */
+ public boolean getLocationInfo() {
+ return locationInfo;
+ }
+
+ /**
+ * The <b>App</b> option takes a string value which should be the name of the
+ * application getting logged.
+ * If property was already set (via system property), don't set here.
+ * @since 1.2.15
+ */
+ public void setApplication(String lapp) {
+ this.application = lapp;
+ }
+
+ /**
+ * Returns value of the <b>Application</b> option.
+ * @since 1.2.15
+ */
+ public String getApplication() {
+ return application;
+ }
+
+ /**
+ The <b>ReconnectionDelay</b> option takes a positive integer
+ representing the number of milliseconds to wait between each
+ failed connection attempt to the server. The default value of
+ this option is 30000 which corresponds to 30 seconds.
+
+ <p>Setting this option to zero turns off reconnection
+ capability.
+ */
+ public void setReconnectionDelay(int delay) {
+ this.reconnectionDelay = delay;
+ }
+
+ /**
+ Returns value of the <b>ReconnectionDelay</b> option.
+ */
+ public int getReconnectionDelay() {
+ return reconnectionDelay;
+ }
+
+ /**
+ The Connector will reconnect when the server becomes available
+ again. It does this by attempting to open a new connection every
+ <code>reconnectionDelay</code> milliseconds.
+
+ <p>It stops trying whenever a connection is established. It will
+ restart to try reconnect to the server when previously open
+ connection is droppped.
+
+ @author Ceki Gülcü
+ @since 0.8.4
+ */
+ class Connector extends Thread {
+
+ boolean interrupted = false;
+
+ public
+ void run() {
+ Socket socket;
+ while(!interrupted) {
+ try {
+ sleep(reconnectionDelay);
+ LogLog.debug("Attempting connection to "+address.getHostName());
+ socket = new Socket(address, port);
+ synchronized(this) {
+ oos = new ObjectOutputStream(socket.getOutputStream());
+ connector = null;
+ LogLog.debug("Connection established. Exiting connector thread.");
+ break;
+ }
+ } catch(InterruptedException e) {
+ LogLog.debug("Connector interrupted. Leaving loop.");
+ return;
+ } catch(java.net.ConnectException e) {
+ LogLog.debug("Remote host "+address.getHostName()
+ +" refused connection.");
+ } catch(IOException e) {
+ if (e instanceof InterruptedIOException) {
+ Thread.currentThread().interrupt();
+ }
+ LogLog.debug("Could not connect to " + address.getHostName()+
+ ". Exception is " + e);
+ }
+ }
+ //LogLog.debug("Exiting Connector.run() method.");
+ }
+
+ /**
+ public
+ void finalize() {
+ LogLog.debug("Connector finalize() has been called.");
+ }
+ */
+ }
+
+}