org.apache.log4j.net.SocketAppender Maven / Gradle / Ivy
/*
* 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
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}.
*
*
* The SocketAppender has the following properties:
*
*
*
*
*
- 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.
*
*
*
- SocketAppenders do not use a layout. They ship a serialized
* {@link LoggingEvent} object to the server side.
*
*
*
- Remote logging uses the TCP protocol. Consequently, if the server is
* reachable, then log events will eventually arrive at the server.
*
*
*
- 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
* connector thread which periodically attempts to connect to the
* server.
*
*
*
- Logging events are automatically buffered 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.
*
*
* 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.
*
*
*
- Even if a
SocketAppender
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 SocketAppender
explicitly. See also next item.
*
*
* Long lived applications which create/destroy many SocketAppender
* instances should be aware of this garbage collection problem. Most other
* applications can safely ignore it.
*
*
*
- If the JVM hosting the
SocketAppender
exits before the
* SocketAppender
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.
*
*
* To avoid lost data, it is usually sufficient to {@link #close} the
* SocketAppender
either explicitly or by calling the
* {@link org.apache.log4j.LogManager#shutdown} method before exiting the
* application.
*
*
*
*
* @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 address
and port
.
*/
public SocketAppender(InetAddress address, int port) {
this.address = address;
this.remoteHost = address.getHostName();
this.port = port;
connect(address, port);
}
/**
* Connects to remote server at host
and port
.
*/
public SocketAppender(String host, int port) {
this.port = port;
this.address = getAddressByName(host);
this.remoteHost = host;
connect(address, port);
}
/**
* Connect to the specified RemoteHost and Port.
*/
public void activateOptions() {
if (advertiseViaMulticastDNS) {
zeroConf = new ZeroConfSupport(ZONE, port, getName());
zeroConf.advertise();
}
connect(address, port);
}
/**
* Close this appender.
*
*
* 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
* false
.
*/
public boolean requiresLayout() {
return false;
}
/**
* The RemoteHost 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 RemoteHost option.
*/
public String getRemoteHost() {
return remoteHost;
}
/**
* The Port 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 Port option.
*/
public int getPort() {
return port;
}
/**
* The LocationInfo 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 LocationInfo option.
*/
public boolean getLocationInfo() {
return locationInfo;
}
/**
* The App 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 Application option.
*
* @since 1.2.15
*/
public String getApplication() {
return application;
}
/**
* The ReconnectionDelay 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.
*
*
* Setting this option to zero turns off reconnection capability.
*/
public void setReconnectionDelay(int delay) {
this.reconnectionDelay = delay;
}
/**
* Returns value of the ReconnectionDelay 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
* reconnectionDelay
milliseconds.
*
*
* 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."); }
*/
}
}