org.apache.log4j.net.SocketHubAppender 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.
*/
package org.apache.log4j.net;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.util.Vector;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.helpers.CyclicBuffer;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.spi.LoggingEvent;
/**
* Sends {@link LoggingEvent} objects to a set of remote log servers, usually a
* {@link SocketNode SocketNodes}.
*
*
* Acts just like {@link SocketAppender} except that instead of connecting to a
* given remote log server, SocketHubAppender
accepts connections
* from the remote log servers as clients. It can accept more than one
* connection. When a log event is received, the event is sent to the set of
* currently connected remote log servers. Implemented this way it does not
* require any update to the configuration file to send data to another remote
* log server. The remote log server simply connects to the host and port the
* SocketHubAppender
is running on.
*
*
* The SocketHubAppender
does not store events such that the remote
* side will events that arrived after the establishment of its connection. Once
* connected, events arrive in order as guaranteed by the TCP protocol.
*
*
* This implementation borrows heavily from the {@link SocketAppender}.
*
*
* The SocketHubAppender has the following characteristics:
*
*
*
*
*
- If sent to a {@link SocketNode}, 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.
*
*
*
SocketHubAppender
does not use a layout. It ships a
* serialized {@link LoggingEvent} object to the remote side.
*
*
*
SocketHubAppender
relies on the TCP protocol. Consequently,
* if the remote side is reachable, then log events will eventually arrive at
* remote client.
*
*
*
- If no remote clients are attached, the logging requests are simply
* dropped.
*
*
*
- Logging events are automatically buffered by the native TCP
* implementation. This means that if the link to remote client is slow but
* still faster than the rate of (log) event production, the application will
* not be affected by the slow network connection. However, if the network
* connection is slower then the rate of event production, then the local
* application can only progress at the network rate. In particular, if the
* network link to the the remote client is down, the application will be
* blocked.
*
*
* On the other hand, if the network link is up, but the remote client is down,
* the client will not be blocked when making log requests but the log events
* will be lost due to client unavailability.
*
*
* The single remote client case extends to multiple clients connections. The
* rate of logging will be determined by the slowest link.
*
*
*
- If the JVM hosting the
SocketHubAppender
exits before the
* SocketHubAppender
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
* SocketHubAppender
either explicitly or by calling the
* {@link org.apache.log4j.LogManager#shutdown} method before exiting the
* application.
*
*
*
* @author Mark Womack
*/
public class SocketHubAppender extends AppenderSkeleton {
/**
* The default port number of the ServerSocket will be created on.
*/
static final int DEFAULT_PORT = 4560;
private int port = DEFAULT_PORT;
private Vector oosList = new Vector();
private ServerMonitor serverMonitor = null;
private boolean locationInfo = false;
private CyclicBuffer buffer = null;
private String application;
private boolean advertiseViaMulticastDNS;
private ZeroConfSupport zeroConf;
/**
* The MulticastDNS zone advertised by a SocketHubAppender
*/
public static final String ZONE = "_log4j_obj_tcpaccept_appender.local.";
private ServerSocket serverSocket;
public SocketHubAppender() {
}
/**
* Connects to remote server at address
and port
.
*/
public SocketHubAppender(int _port) {
port = _port;
startServer();
}
/**
* Set up the socket server on the specified port.
*/
public void activateOptions() {
if (advertiseViaMulticastDNS) {
zeroConf = new ZeroConfSupport(ZONE, port, getName());
zeroConf.advertise();
}
startServer();
}
/**
* Close this appender.
*
* This will mark the appender as closed and call then {@link #cleanUp} method.
*/
synchronized public void close() {
if (closed)
return;
LogLog.debug("closing SocketHubAppender " + getName());
this.closed = true;
if (advertiseViaMulticastDNS) {
zeroConf.unadvertise();
}
cleanUp();
LogLog.debug("SocketHubAppender " + getName() + " closed");
}
/**
* Release the underlying ServerMonitor thread, and drop the connections to all
* connected remote servers.
*/
public void cleanUp() {
// stop the monitor thread
LogLog.debug("stopping ServerSocket");
serverMonitor.stopMonitor();
serverMonitor = null;
// close all of the connections
LogLog.debug("closing client connections");
while (oosList.size() != 0) {
ObjectOutputStream oos = (ObjectOutputStream) oosList.elementAt(0);
if (oos != null) {
try {
oos.close();
} catch (InterruptedIOException e) {
Thread.currentThread().interrupt();
LogLog.error("could not close oos.", e);
} catch (IOException e) {
LogLog.error("could not close oos.", e);
}
oosList.removeElementAt(0);
}
}
}
/**
* Append an event to all of current connections.
*/
public void append(LoggingEvent event) {
if (event != null) {
// set up location info if requested
if (locationInfo) {
event.getLocationInformation();
}
if (application != null) {
event.setProperty("application", application);
}
event.getNDC();
event.getThreadName();
event.getMDCCopy();
event.getRenderedMessage();
event.getThrowableStrRep();
if (buffer != null) {
buffer.add(event);
}
}
// if no event or no open connections, exit now
if ((event == null) || (oosList.size() == 0)) {
return;
}
// loop through the current set of open connections, appending the event to each
for (int streamCount = 0; streamCount < oosList.size(); streamCount++) {
ObjectOutputStream oos = null;
try {
oos = (ObjectOutputStream) oosList.elementAt(streamCount);
} catch (ArrayIndexOutOfBoundsException e) {
// catch this, but just don't assign a value
// this should not really occur as this method is
// the only one that can remove oos's (besides cleanUp).
}
// list size changed unexpectedly? Just exit the append.
if (oos == null)
break;
try {
oos.writeObject(event);
oos.flush();
// Failing to reset the object output stream every now and
// then creates a serious memory leak.
// right now we always reset. TODO - set up frequency counter per oos?
oos.reset();
} catch (IOException e) {
if (e instanceof InterruptedIOException) {
Thread.currentThread().interrupt();
}
// there was an io exception so just drop the connection
oosList.removeElementAt(streamCount);
LogLog.debug("dropped connection");
// decrement to keep the counter in place (for loop always increments)
streamCount--;
}
}
}
/**
* The SocketHubAppender does not use a layout. Hence, this method returns
* false
.
*/
public boolean requiresLayout() {
return false;
}
/**
* The Port option takes a positive integer representing the port where
* the server is waiting for connections.
*/
public void setPort(int _port) {
port = _port;
}
/**
* 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.
*/
public void setApplication(String lapp) {
this.application = lapp;
}
/**
* Returns value of the Application option.
*/
public String getApplication() {
return application;
}
/**
* Returns value of the Port option.
*/
public int getPort() {
return port;
}
/**
* The BufferSize option takes a positive integer representing the number
* of events this appender will buffer and send to newly connected clients.
*/
public void setBufferSize(int _bufferSize) {
buffer = new CyclicBuffer(_bufferSize);
}
/**
* Returns value of the bufferSize option.
*/
public int getBufferSize() {
if (buffer == null) {
return 0;
} else {
return buffer.getMaxSize();
}
}
/**
* 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) {
locationInfo = _locationInfo;
}
/**
* Returns value of the LocationInfo option.
*/
public boolean getLocationInfo() {
return locationInfo;
}
public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
}
public boolean isAdvertiseViaMulticastDNS() {
return advertiseViaMulticastDNS;
}
/**
* Start the ServerMonitor thread.
*/
private void startServer() {
serverMonitor = new ServerMonitor(port, oosList);
}
/**
* Creates a server socket to accept connections.
*
* @param socketPort port on which the socket should listen, may be zero.
* @return new socket.
* @throws IOException IO error when opening the socket.
*/
protected ServerSocket createServerSocket(final int socketPort) throws IOException {
return new ServerSocket(socketPort);
}
/**
* This class is used internally to monitor a ServerSocket and register new
* connections in a vector passed in the constructor.
*/
private class ServerMonitor implements Runnable {
private int port;
private Vector oosList;
private boolean keepRunning;
private Thread monitorThread;
/**
* Create a thread and start the monitor.
*/
public ServerMonitor(int _port, Vector _oosList) {
port = _port;
oosList = _oosList;
keepRunning = true;
monitorThread = new Thread(this);
monitorThread.setDaemon(true);
monitorThread.setName("SocketHubAppender-Monitor-" + port);
monitorThread.start();
}
/**
* Stops the monitor. This method will not return until the thread has finished
* executing.
*/
public synchronized void stopMonitor() {
if (keepRunning) {
LogLog.debug("server monitor thread shutting down");
keepRunning = false;
try {
if (serverSocket != null) {
serverSocket.close();
serverSocket = null;
}
} catch (IOException ioe) {
}
try {
monitorThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// do nothing?
}
// release the thread
monitorThread = null;
LogLog.debug("server monitor thread shut down");
}
}
private void sendCachedEvents(ObjectOutputStream stream) throws IOException {
if (buffer != null) {
for (int i = 0; i < buffer.length(); i++) {
stream.writeObject(buffer.get(i));
}
stream.flush();
stream.reset();
}
}
/**
* Method that runs, monitoring the ServerSocket and adding connections as they
* connect to the socket.
*/
public void run() {
serverSocket = null;
try {
serverSocket = createServerSocket(port);
serverSocket.setSoTimeout(1000);
} catch (Exception e) {
if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
LogLog.error("exception setting timeout, shutting down server socket.", e);
keepRunning = false;
return;
}
try {
try {
serverSocket.setSoTimeout(1000);
} catch (SocketException e) {
LogLog.error("exception setting timeout, shutting down server socket.", e);
return;
}
while (keepRunning) {
Socket socket = null;
try {
socket = serverSocket.accept();
} catch (InterruptedIOException e) {
// timeout occurred, so just loop
} catch (SocketException e) {
LogLog.error("exception accepting socket, shutting down server socket.", e);
keepRunning = false;
} catch (IOException e) {
LogLog.error("exception accepting socket.", e);
}
// if there was a socket accepted
if (socket != null) {
try {
InetAddress remoteAddress = socket.getInetAddress();
LogLog.debug("accepting connection from " + remoteAddress.getHostName() + " ("
+ remoteAddress.getHostAddress() + ")");
// create an ObjectOutputStream
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
if (buffer != null && buffer.length() > 0) {
sendCachedEvents(oos);
}
// add it to the oosList. OK since Vector is synchronized.
oosList.addElement(oos);
} catch (IOException e) {
if (e instanceof InterruptedIOException) {
Thread.currentThread().interrupt();
}
LogLog.error("exception creating output stream on socket.", e);
}
}
}
} finally {
// close the socket
try {
serverSocket.close();
} catch (InterruptedIOException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
// do nothing with it?
}
}
}
}
}