| /* |
| * 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}. |
| |
| <p>Acts just like {@link SocketAppender} except that instead of |
| connecting to a given remote log server, |
| <code>SocketHubAppender</code> 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 <code>SocketHubAppender</code> is running on. |
| |
| <p>The <code>SocketHubAppender</code> 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. |
| |
| <p>This implementation borrows heavily from the {@link |
| SocketAppender}. |
| |
| <p>The SocketHubAppender has the following characteristics: |
| |
| <ul> |
| |
| <p><li>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. |
| |
| <p><li><code>SocketHubAppender</code> does not use a layout. It |
| ships a serialized {@link LoggingEvent} object to the remote side. |
| |
| <p><li><code>SocketHubAppender</code> relies on the TCP |
| protocol. Consequently, if the remote side is reachable, then log |
| events will eventually arrive at remote client. |
| |
| <p><li>If no remote clients are attached, the logging requests are |
| simply dropped. |
| |
| <p><li>Logging events are automatically <em>buffered</em> 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. |
| |
| <p>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. |
| |
| <p>The single remote client case extends to multiple clients |
| connections. The rate of logging will be determined by the slowest |
| link. |
| |
| <p><li>If the JVM hosting the <code>SocketHubAppender</code> exits |
| before the <code>SocketHubAppender</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>SocketHubAppender</code> either explicitly or by calling |
| the {@link org.apache.log4j.LogManager#shutdown} method before |
| exiting the application. |
| |
| </ul> |
| |
| @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 <code>address</code> and <code>port</code>. */ |
| 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. |
| <p>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 |
| <code>false</code>. */ |
| public |
| boolean requiresLayout() { |
| return false; |
| } |
| |
| /** |
| The <b>Port</b> option takes a positive integer representing |
| the port where the server is waiting for connections. */ |
| public |
| void setPort(int _port) { |
| port = _port; |
| } |
| |
| /** |
| * 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. |
| */ |
| public |
| void setApplication(String lapp) { |
| this.application = lapp; |
| } |
| |
| /** |
| * Returns value of the <b>Application</b> option. |
| */ |
| public |
| String getApplication() { |
| return application; |
| } |
| |
| /** |
| Returns value of the <b>Port</b> option. */ |
| public |
| int getPort() { |
| return port; |
| } |
| |
| /** |
| * The <b>BufferSize</b> 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 <b>bufferSize</b> option. |
| */ |
| public |
| int getBufferSize() { |
| if (buffer == null) { |
| return 0; |
| } else { |
| return buffer.getMaxSize(); |
| } |
| } |
| |
| /** |
| 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) { |
| locationInfo = _locationInfo; |
| } |
| |
| /** |
| Returns value of the <b>LocationInfo</b> 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? |
| } |
| } |
| } |
| } |
| } |
| |