http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java b/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java new file mode 100644 index 0000000..eec19d3 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/JMSReceiverBeanInfo.java @@ -0,0 +1,52 @@ +/* + * 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.beans.PropertyDescriptor; +import java.beans.SimpleBeanInfo; + + +/** + * BeanInfo class for the JMSReceiver. + * + * @author Paul Smith <[email protected]> + * + */ +public class JMSReceiverBeanInfo extends SimpleBeanInfo { + + /* (non-Javadoc) + * @see java.beans.BeanInfo#getPropertyDescriptors() + */ + public PropertyDescriptor[] getPropertyDescriptors() { + + try { + + return new PropertyDescriptor[] { + new PropertyDescriptor("name", JMSReceiver.class), + new PropertyDescriptor("topicFactoryName", JMSReceiver.class), + new PropertyDescriptor("topicName", JMSReceiver.class), + new PropertyDescriptor("threshold", JMSReceiver.class), + new PropertyDescriptor("jndiPath", JMSReceiver.class), + new PropertyDescriptor("userId", + JMSReceiver.class), + }; + } catch (Exception e) { + } + + return null; + } +}
http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/MulticastAppender.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/MulticastAppender.java b/src/main/java/org/apache/log4j/net/MulticastAppender.java new file mode 100644 index 0000000..de002c5 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/MulticastAppender.java @@ -0,0 +1,345 @@ +/* + * 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.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; + +import org.apache.log4j.AppenderSkeleton; +import org.apache.log4j.helpers.Constants; +import org.apache.log4j.spi.LoggingEvent; +import org.apache.log4j.helpers.LogLog; +import org.apache.log4j.xml.XMLLayout; + + +/** + * Multicast-based Appender. Works in conjunction with the MulticastReceiver, which expects + * a LoggingEvent encoded using XMLLayout. + * + * Sends log information as a multicast datagrams. + * + * <p>Messages are not sent as LoggingEvent objects but as text after + * applying XMLLayout. + * + * <p>The port and remoteHost properties can be set in configuration properties. + * By setting the remoteHost to a broadcast address any number of clients can + * listen for log messages. + * + * <p>This was inspired and really extended/copied from {@link SocketAppender}. Please + * see the docs for the proper credit to the authors of that class. + * + * @author <a href="mailto:[email protected]">Kevin Brown</a> + * @author Scott Deboy <[email protected]> + * + */ +public class MulticastAppender extends AppenderSkeleton implements PortBased { + /** + The default port number for the multicast packets. (9991). + */ + static final int DEFAULT_PORT = 9991; + + /** + * The MulticastDNS zone advertised by a MulticastAppender + * the MulticastAppender also adds a 'multicastAddress' property with the multicast address value as a string + */ + public static final String ZONE = "_log4j_xml_mcast_appender.local."; + + /** + We remember host name as String in addition to the resolved + InetAddress so that it can be returned via getOption(). + */ + String hostname; + String remoteHost; + String application; + int timeToLive; + InetAddress address; + int port = DEFAULT_PORT; + MulticastSocket outSocket; + private String encoding; + + private boolean locationInfo = false; + private boolean advertiseViaMulticastDNS; + private ZeroConfSupport zeroConf; + + public MulticastAppender() { + super(false); + } + + /** + Open the multicast sender for the <b>RemoteHost</b> and <b>Port</b>. + */ + public void activateOptions() { + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException uhe) { + try { + hostname = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException uhe2) { + hostname = "unknown"; + } + } + + //allow system property of application to be primary + if (application == null) { + application = System.getProperty(Constants.APPLICATION_KEY); + } else { + if (System.getProperty(Constants.APPLICATION_KEY) != null) { + application = application + "-" + System.getProperty(Constants.APPLICATION_KEY); + } + } + + if(remoteHost != null) { + address = getAddressByName(remoteHost); + } else { + String err = "The RemoteHost property is required for MulticastAppender named "+ name; + LogLog.error(err); + throw new IllegalStateException(err); + } + + if (layout == null) { + layout = new XMLLayout(); + } + + if (advertiseViaMulticastDNS) { + Map properties = new HashMap(); + properties.put("multicastAddress", remoteHost); + zeroConf = new ZeroConfSupport(ZONE, port, getName(), properties); + zeroConf.advertise(); + } + connect(); + super.activateOptions(); + } + + /** + Close this appender. + <p>This will mark the appender as closed and + call then {@link #cleanUp} method. + */ + public synchronized void close() { + if (closed) { + return; + } + + this.closed = true; + if (advertiseViaMulticastDNS) { + zeroConf.unadvertise(); + } + cleanUp(); + } + + /** + Close the Socket and release the underlying + connector thread if it has been created + */ + public void cleanUp() { + if (outSocket != null) { + try { + outSocket.close(); + } catch (Exception e) { + LogLog.error("Could not close outSocket.", e); + } + + outSocket = null; + } + } + + void connect() { + if (this.address == null) { + return; + } + + try { + // First, close the previous connection if any. + cleanUp(); + outSocket = new MulticastSocket(); + outSocket.setTimeToLive(timeToLive); + } catch (IOException e) { + LogLog.error("Error in connect method of MulticastAppender named "+name, e); + } + } + + public void append(LoggingEvent event) { + if (event == null) { + return; + } + + if(locationInfo) { + event.getLocationInformation(); + } + + if (outSocket != null) { + event.setProperty(Constants.HOSTNAME_KEY, hostname); + + if (application != null) { + event.setProperty(Constants.APPLICATION_KEY, application); + } + + if(locationInfo) { + event.getLocationInformation(); + } + + + try { + StringBuffer buf = new StringBuffer(layout.format(event)); + + byte[] payload; + if(encoding == null) { + payload = buf.toString().getBytes(); + } else { + payload = buf.toString().getBytes(encoding); + } + + DatagramPacket dp = + new DatagramPacket(payload, payload.length, address, port); + outSocket.send(dp); + } catch (IOException e) { + outSocket = null; + LogLog.warn("Detected problem with Multicast connection: " + e); + } + } + } + + InetAddress getAddressByName(String host) { + try { + return InetAddress.getByName(host); + } catch (Exception e) { + LogLog.error("Could not find address of [" + host + "].", e); + return null; + } + } + + /** + The <b>RemoteHost</b> option takes a string value which should be + the host name or ipaddress to send the multicast packets. + */ + public void setRemoteHost(String host) { + remoteHost = host; + } + + /** + Returns value of the <b>RemoteHost</b> option. + */ + public String getRemoteHost() { + return remoteHost; + } + + /** + 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>Encoding</b> option specifies how the bytes are encoded. If this option is not specified, + the System encoding is used. + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + Returns value of the <b>Encoding</b> option. + */ + public String getEncoding() { + return encoding; + } + /** + 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 app) { + this.application = app; + } + + /** + Returns value of the <b>App</b> option. + */ + public String getApplication() { + return application; + } + + /** + The <b>Time to live</b> option takes a positive integer representing + the time to live value. + */ + public void setTimeToLive(int timeToLive) { + this.timeToLive = timeToLive; + } + + /** + Returns value of the <b>Time to Live</b> option. + */ + public int getTimeToLive() { + return timeToLive; + } + + /** + The <b>Port</b> option takes a positive integer representing + the port where multicast packets will be sent. + */ + public void setPort(int port) { + this.port = port; + } + + /** + Returns value of the <b>Port</b> option. + */ + public int getPort() { + return port; + } + + /* (non-Javadoc) + * @see org.apache.log4j.net.NetworkBased#isActive() + */ + public boolean isActive() { + // TODO handle active/inactive + return true; + } + + /** + * Gets whether appender requires a layout. + * @return false + */ + public boolean requiresLayout() { + return true; + } + + public boolean isAdvertiseViaMulticastDNS() { + return advertiseViaMulticastDNS; + } + + public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) { + this.advertiseViaMulticastDNS = advertiseViaMulticastDNS; + } +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/MulticastReceiver.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/MulticastReceiver.java b/src/main/java/org/apache/log4j/net/MulticastReceiver.java new file mode 100644 index 0000000..2dfcec2 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/MulticastReceiver.java @@ -0,0 +1,276 @@ +/* + * 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.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.log4j.plugins.Pauseable; +import org.apache.log4j.plugins.Receiver; +import org.apache.log4j.spi.Decoder; +import org.apache.log4j.spi.LoggingEvent; + + +/** + * Multicast-based receiver. Accepts LoggingEvents encoded using + * MulticastAppender and XMLLayout. The the XML data is converted + * back to a LoggingEvent and is posted. + * + * @author Scott Deboy <[email protected]> + * + */ +public class MulticastReceiver extends Receiver implements PortBased, + AddressBased, Pauseable { + private static final int PACKET_LENGTH = 16384; + private int port; + private String address; + private String encoding; + private MulticastSocket socket = null; + + //default to log4j xml decoder + private String decoder = "org.apache.log4j.xml.XMLDecoder"; + private Decoder decoderImpl; + private MulticastHandlerThread handlerThread; + private MulticastReceiverThread receiverThread; + private boolean paused; + private boolean advertiseViaMulticastDNS; + private ZeroConfSupport zeroConf; + + /** + * The MulticastDNS zone advertised by a MulticastReceiver + */ + public static final String ZONE = "_log4j_xml_mcast_receiver.local."; + + public String getDecoder() { + return decoder; + } + + public void setDecoder(String decoder) { + this.decoder = decoder; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getAddress() { + return address; + } + + /** + The <b>Encoding</b> option specifies how the bytes are encoded. If this option is not specified, + the system encoding will be used. + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + Returns value of the <b>Encoding</b> option. + */ + public String getEncoding() { + return encoding; + } + + public synchronized void shutdown() { + active = false; + if (advertiseViaMulticastDNS) { + zeroConf.unadvertise(); + } + if (handlerThread != null) { + handlerThread.interrupt(); + } + if (receiverThread != null) { + receiverThread.interrupt(); + } + if (socket != null) { + socket.close(); + } + } + + public void setAddress(String address) { + this.address = address; + } + + public boolean isPaused() { + return paused; + } + + public void setPaused(boolean b) { + paused = b; + } + + public void activateOptions() { + InetAddress addr = null; + + try { + Class c = Class.forName(decoder); + Object o = c.newInstance(); + + if (o instanceof Decoder) { + this.decoderImpl = (Decoder) o; + } + } catch (ClassNotFoundException cnfe) { + getLogger().warn("Unable to find decoder", cnfe); + } catch (IllegalAccessException iae) { + getLogger().warn("Could not construct decoder", iae); + } catch (InstantiationException ie) { + getLogger().warn("Could not construct decoder", ie); + } + + try { + addr = InetAddress.getByName(address); + } catch (UnknownHostException uhe) { + uhe.printStackTrace(); + } + + try { + active = true; + socket = new MulticastSocket(port); + socket.joinGroup(addr); + receiverThread = new MulticastReceiverThread(); + receiverThread.start(); + handlerThread = new MulticastHandlerThread(); + handlerThread.start(); + if (advertiseViaMulticastDNS) { + zeroConf = new ZeroConfSupport(ZONE, port, getName()); + zeroConf.advertise(); + } + + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) { + this.advertiseViaMulticastDNS = advertiseViaMulticastDNS; + } + + public boolean isAdvertiseViaMulticastDNS() { + return advertiseViaMulticastDNS; + } + + class MulticastHandlerThread extends Thread { + private List list = new ArrayList(); + + public MulticastHandlerThread() { + setDaemon(true); + } + + public void append(String data) { + synchronized (list) { + list.add(data); + list.notify(); + } + } + + public void run() { + ArrayList list2 = new ArrayList(); + + while (isAlive()) { + synchronized (list) { + try { + while (list.size() == 0) { + list.wait(); + } + + if (list.size() > 0) { + list2.addAll(list); + list.clear(); + } + } catch (InterruptedException ie) { + } + } + + if (list2.size() > 0) { + Iterator iter = list2.iterator(); + + while (iter.hasNext()) { + String data = (String) iter.next(); + List v = decoderImpl.decodeEvents(data.trim()); + + if (v != null) { + Iterator eventIter = v.iterator(); + + while (eventIter.hasNext()) { + if (!isPaused()) { + doPost((LoggingEvent) eventIter.next()); + } + } + } + } + + list2.clear(); + } else { + try { + synchronized (this) { + wait(1000); + } + } catch (InterruptedException ie) { + } + } + } + } + } + + class MulticastReceiverThread extends Thread { + public MulticastReceiverThread() { + setDaemon(true); + } + + public void run() { + active = true; + + byte[] b = new byte[PACKET_LENGTH]; + DatagramPacket p = new DatagramPacket(b, b.length); + + while (active) { + try { + socket.receive(p); + + //this string constructor which accepts a charset throws an exception if it is + //null + if (encoding == null) { + handlerThread.append( + new String(p.getData(), 0, p.getLength())); + } else { + handlerThread.append( + new String(p.getData(), 0, p.getLength(), encoding)); + } + } catch (SocketException se) { + //disconnected + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + getLogger().debug("{}'s thread is ending.", MulticastReceiver.this.getName()); + } + } +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java b/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java new file mode 100644 index 0000000..4dec14c --- /dev/null +++ b/src/main/java/org/apache/log4j/net/MulticastReceiverBeanInfo.java @@ -0,0 +1,51 @@ +/* + * 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.beans.PropertyDescriptor; +import java.beans.SimpleBeanInfo; + + +/** + * BeanInfo class for the meta-data of the MulticastReceiver. + * + * @author Paul Smith <[email protected]> + * + */ +public class MulticastReceiverBeanInfo extends SimpleBeanInfo { + + /* (non-Javadoc) + * @see java.beans.BeanInfo#getPropertyDescriptors() + */ + public PropertyDescriptor[] getPropertyDescriptors() { + + try { + + return new PropertyDescriptor[] { + new PropertyDescriptor("name", MulticastReceiver.class), + new PropertyDescriptor("address", MulticastReceiver.class), + new PropertyDescriptor("port", MulticastReceiver.class), + new PropertyDescriptor("threshold", MulticastReceiver.class), + new PropertyDescriptor("decoder", MulticastReceiver.class), + new PropertyDescriptor("advertiseViaMulticastDNS", MulticastReceiver.class), + }; + } catch (Exception e) { + } + + return null; + } +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/NetworkBased.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/NetworkBased.java b/src/main/java/org/apache/log4j/net/NetworkBased.java new file mode 100644 index 0000000..9c5153f --- /dev/null +++ b/src/main/java/org/apache/log4j/net/NetworkBased.java @@ -0,0 +1,39 @@ +/* + * 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; + +/** + * The parent of all the Network based interfaces. + * + * @author Paul Smith ([email protected]) + * + */ +public interface NetworkBased { + + /** + * Get name. + * @return name. + */ + String getName(); + + /** + * Get if item is active. + * @return if true, item is active. + */ + boolean isActive(); +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/PortBased.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/PortBased.java b/src/main/java/org/apache/log4j/net/PortBased.java new file mode 100644 index 0000000..c7c1f97 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/PortBased.java @@ -0,0 +1,34 @@ +/* + * 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; + + +/** + * Net based entities that 'work with' a Port should consider implementing this + * interface so that they can be treated generically. + * + * @author Paul Smith ([email protected]) + * + */ +public interface PortBased extends NetworkBased { + /** + * Returns the Port # that this net based thing is using. + * @return int port number + */ + int getPort(); +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/SocketHubReceiver.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/SocketHubReceiver.java b/src/main/java/org/apache/log4j/net/SocketHubReceiver.java new file mode 100644 index 0000000..85058e8 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/SocketHubReceiver.java @@ -0,0 +1,409 @@ +/* + * 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 org.apache.log4j.plugins.Plugin; +import org.apache.log4j.plugins.Receiver; +import org.apache.log4j.spi.LoggerRepository; + +import java.io.IOException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + SocketHubReceiver receives a remote logging event on a configured + socket and "posts" it to a LoggerRepository as if the event was + generated locally. This class is designed to receive events from + the SocketHubAppender class (or classes that send compatible events). + + <p>Once the event has been "posted", it will be handled by the + appenders currently configured in the LoggerRespository. + + @author Mark Womack + @author Ceki Gülcü + @author Paul Smith ([email protected]) +*/ +public class SocketHubReceiver +extends Receiver implements SocketNodeEventListener, PortBased { + + /** + * Default reconnection delay. + */ + static final int DEFAULT_RECONNECTION_DELAY = 30000; + + /** + * Host. + */ + protected String host; + + /** + * Port. + */ + protected int port; + /** + * Reconnection delay. + */ + protected int reconnectionDelay = DEFAULT_RECONNECTION_DELAY; + + /** + * The MulticastDNS zone advertised by a SocketHubReceiver + */ + public static final String ZONE = "_log4j_obj_tcpconnect_receiver.local."; + + /** + * Active. + */ + protected boolean active = false; + + /** + * Connector. + */ + protected Connector connector; + + /** + * Socket. + */ + protected SocketNode13 socketNode; + + /** + * Listener list. + */ + private List listenerList = Collections.synchronizedList(new ArrayList()); + + private boolean advertiseViaMulticastDNS; + private ZeroConfSupport zeroConf; + + /** + * Create new instance. + */ + public SocketHubReceiver() { + super(); + } + + /** + * Create new instance. + * @param h host + * @param p port + */ + public SocketHubReceiver(final String h, + final int p) { + super(); + host = h; + port = p; + } + + /** + * Create new instance. + * @param h host + * @param p port + * @param repo logger repository + */ + public SocketHubReceiver(final String h, + final int p, + final LoggerRepository repo) { + super(); + host = h; + port = p; + repository = repo; + } + + /** + * Adds a SocketNodeEventListener to this receiver to be notified + * of SocketNode events. + * @param l listener + */ + public void addSocketNodeEventListener(final SocketNodeEventListener l) { + listenerList.add(l); + } + + /** + * Removes a specific SocketNodeEventListener from this instance + * so that it will no longer be notified of SocketNode events. + * @param l listener + */ + public void removeSocketNodeEventListener( + final SocketNodeEventListener l) { + listenerList.remove(l); + } + + /** + Get the remote host to connect to for logging events. + @return host + */ + public String getHost() { + return host; + } + + /** + * Configures the Host property, this will require activateOptions + * to be called for this to take effect. + * @param remoteHost address of remote host. + */ + public void setHost(final String remoteHost) { + this.host = remoteHost; + } + /** + Set the remote host to connect to for logging events. + Equivalent to setHost. + @param remoteHost address of remote host. + */ + public void setPort(final String remoteHost) { + host = remoteHost; + } + + /** + Get the remote port to connect to for logging events. + @return port + */ + public int getPort() { + return port; + } + + /** + Set the remote port to connect to for logging events. + @param p port + */ + public void setPort(final int p) { + this.port = p; + } + + /** + 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. + @param delay milliseconds to wait or zero to not reconnect. + */ + public void setReconnectionDelay(final int delay) { + int oldValue = this.reconnectionDelay; + this.reconnectionDelay = delay; + firePropertyChange("reconnectionDelay", oldValue, this.reconnectionDelay); + } + + /** + Returns value of the <b>ReconnectionDelay</b> option. + @return value of reconnection delay option. + */ + public int getReconnectionDelay() { + return reconnectionDelay; + } + + /** + * Returns true if the receiver is the same class and they are + * configured for the same properties, and super class also considers + * them to be equivalent. This is used by PluginRegistry when determining + * if the a similarly configured receiver is being started. + * + * @param testPlugin The plugin to test equivalency against. + * @return boolean True if the testPlugin is equivalent to this plugin. + */ + public boolean isEquivalent(final Plugin testPlugin) { + if (testPlugin != null && testPlugin instanceof SocketHubReceiver) { + SocketHubReceiver sReceiver = (SocketHubReceiver) testPlugin; + + return (port == sReceiver.getPort() + && host.equals(sReceiver.getHost()) + && reconnectionDelay == sReceiver.getReconnectionDelay() + && super.isEquivalent(testPlugin)); + } + return false; + } + + /** + Sets the flag to indicate if receiver is active or not. + @param b new value + */ + protected synchronized void setActive(final boolean b) { + active = b; + } + + /** + Starts the SocketReceiver with the current options. */ + public void activateOptions() { + if (!isActive()) { + setActive(true); + if (advertiseViaMulticastDNS) { + zeroConf = new ZeroConfSupport(ZONE, port, getName()); + zeroConf.advertise(); + } + + fireConnector(false); + } + } + + /** + Called when the receiver should be stopped. Closes the socket */ + public synchronized void shutdown() { + // mark this as no longer running + active = false; + + // close the socket + try { + if (socketNode != null) { + socketNode.close(); + socketNode = null; + } + } catch (Exception e) { + getLogger().info("Excpetion closing socket", e); + // ignore for now + } + + // stop the connector + if (connector != null) { + connector.interrupted = true; + connector = null; // allow gc + } + if (advertiseViaMulticastDNS) { + zeroConf.unadvertise(); + } + } + + /** + Listen for a socketClosedEvent from the SocketNode. Reopen the + socket if this receiver is still active. + @param e exception not used. + */ + public void socketClosedEvent(final Exception e) { + // if it is a non-normal closed event + // we clear the connector object here + // so that it actually does reconnect if the + // remote socket dies. + if (e != null) { + connector = null; + fireConnector(true); + } + } + + /** + * Fire connectors. + * @param isReconnect true if reconnect. + */ + private synchronized void fireConnector(final boolean isReconnect) { + if (active && connector == null) { + getLogger().debug("Starting a new connector thread."); + connector = new Connector(isReconnect); + connector.setDaemon(true); + connector.setPriority(Thread.MIN_PRIORITY); + connector.start(); + } + } + + /** + * Set socket. + * @param newSocket new value for socket. + */ + private synchronized void setSocket(final Socket newSocket) { + connector = null; + socketNode = new SocketNode13(newSocket, this); + socketNode.addSocketNodeEventListener(this); + + synchronized (listenerList) { + for (Iterator iter = listenerList.iterator(); iter.hasNext();) { + SocketNodeEventListener listener = + (SocketNodeEventListener) iter.next(); + socketNode.addSocketNodeEventListener(listener); + } + } + new Thread(socketNode).start(); + } + + public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) { + this.advertiseViaMulticastDNS = advertiseViaMulticastDNS; + } + + public boolean isAdvertiseViaMulticastDNS() { + return advertiseViaMulticastDNS; + } + + /** + 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 previpously open + connection is droppped. + + @author Ceki Gülcü + */ + private final class Connector extends Thread { + + /** + * Interruption status. + */ + boolean interrupted = false; + /** + * If true, then delay on next iteration. + */ + boolean doDelay; + + /** + * Create new instance. + * @param isReconnect true if reconnecting. + */ + public Connector(final boolean isReconnect) { + super(); + doDelay = isReconnect; + } + + /** + * Attempt to connect until interrupted. + */ + public void run() { + while (!interrupted) { + try { + if (doDelay) { + getLogger().debug("waiting for " + reconnectionDelay + + " milliseconds before reconnecting."); + sleep(reconnectionDelay); + } + doDelay = true; + getLogger().debug("Attempting connection to " + host); + Socket s = new Socket(host, port); + setSocket(s); + getLogger().debug( + "Connection established. Exiting connector thread."); + break; + } catch (InterruptedException e) { + getLogger().debug("Connector interrupted. Leaving loop."); + return; + } catch (java.net.ConnectException e) { + getLogger().debug("Remote host {} refused connection.", host); + } catch (IOException e) { + getLogger().debug("Could not connect to {}. Exception is {}.", + host, e); + } + } + } + } + + /** + * This method does nothing. + * @param remoteInfo remote info. + */ + public void socketOpened(final String remoteInfo) { + + // This method does nothing. + } +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/SocketNode13.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/SocketNode13.java b/src/main/java/org/apache/log4j/net/SocketNode13.java new file mode 100644 index 0000000..e27c68e --- /dev/null +++ b/src/main/java/org/apache/log4j/net/SocketNode13.java @@ -0,0 +1,299 @@ +/* + * 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.BufferedInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.apache.log4j.Logger; +import org.apache.log4j.helpers.Constants; +import org.apache.log4j.plugins.Pauseable; +import org.apache.log4j.plugins.Receiver; +import org.apache.log4j.spi.ComponentBase; +import org.apache.log4j.spi.LoggerRepository; +import org.apache.log4j.spi.LoggingEvent; + + +// Contributors: Moses Hohman <[email protected]> + +/** + Read {@link LoggingEvent} objects sent from a remote client using + Sockets (TCP). These logging events are logged according to local + policy, as if they were generated locally. + + <p>For example, the socket node might decide to log events to a + local file and also resent them to a second socket node. + + Implementation lifted from org.apache.log4j.net.SocketNode + in log4j 1.3 and renamed to prevent collision with + log4j 1.2 implementation. + + @author Ceki Gülcü + @author Paul Smith ([email protected]) + + +*/ +public class SocketNode13 extends ComponentBase implements Runnable, Pauseable { + + /** + * Paused state. + */ + private boolean paused; + /** + * Closed state. + */ + private boolean closed; + /** + * Socket. + */ + private Socket socket; + /** + * Receiver. + */ + private Receiver receiver; + /** + * List of listeners. + */ + private List listenerList = Collections.synchronizedList(new ArrayList()); + + + + /** + Constructor for socket and logger repository. + @param s socket + @param hierarchy logger repository + */ + public SocketNode13(final Socket s, + final LoggerRepository hierarchy) { + super(); + this.socket = s; + this.repository = hierarchy; + } + + /** + Constructor for socket and receiver. + @param s socket + @param r receiver + */ + public SocketNode13(final Socket s, final Receiver r) { + super(); + this.socket = s; + this.receiver = r; + } + + /** + * Set the event listener on this node. + * + * @deprecated Now supports mutliple listeners, this method + * simply invokes the removeSocketNodeEventListener() to remove + * the listener, and then readds it. + * @param l listener + */ + public void setListener(final SocketNodeEventListener l) { + removeSocketNodeEventListener(l); + addSocketNodeEventListener(l); + } + + /** + * Adds the listener to the list of listeners to be notified of the + * respective event. + * @param listener the listener to add to the list + */ + public void addSocketNodeEventListener( + final SocketNodeEventListener listener) { + listenerList.add(listener); + } + + /** + * Removes the registered Listener from this instances list of + * listeners. If the listener has not been registered, then invoking + * this method has no effect. + * + * @param listener the SocketNodeEventListener to remove + */ + public void removeSocketNodeEventListener( + final SocketNodeEventListener listener) { + listenerList.remove(listener); + } + + + /** + * Deserialize events from socket until interrupted. + */ + public void run() { + LoggingEvent event; + Logger remoteLogger; + Exception listenerException = null; + ObjectInputStream ois = null; + + try { + ois = + new ObjectInputStream( + new BufferedInputStream(socket.getInputStream())); + } catch (Exception e) { + ois = null; + listenerException = e; + getLogger().error("Exception opening ObjectInputStream to " + socket, e); + } + + if (ois != null) { + + String hostName = socket.getInetAddress().getHostName(); + String remoteInfo = hostName + ":" + socket.getPort(); + + /** + * notify the listener that the socket has been + * opened and this SocketNode is ready and waiting + */ + fireSocketOpened(remoteInfo); + + try { + while (!isClosed()) { + // read an event from the wire + event = (LoggingEvent) ois.readObject(); + event.setProperty(Constants.HOSTNAME_KEY, hostName); + // store the known remote info in an event property + event.setProperty("log4j.remoteSourceInfo", remoteInfo); + + // if configured with a receiver, tell it to post the event + if (!isPaused() && !isClosed()) { + if ((receiver != null)) { + receiver.doPost(event); + + // else post it via the hierarchy + } else { + // get a logger from the hierarchy. The name of the logger + // is taken to be the name contained in the event. + remoteLogger = repository.getLogger(event.getLoggerName()); + + //event.logger = remoteLogger; + // apply the logger-level filter + if (event + .getLevel() + .isGreaterOrEqual(remoteLogger.getEffectiveLevel())) { + // finally log the event as if was generated locally + remoteLogger.callAppenders(event); + } + } + } else { + //we simply discard this event. + } + } + } catch (java.io.EOFException e) { + getLogger().info("Caught java.io.EOFException closing connection."); + listenerException = e; + } catch (java.net.SocketException e) { + getLogger().info("Caught java.net.SocketException closing connection."); + listenerException = e; + } catch (IOException e) { + getLogger().info("Caught java.io.IOException: " + e); + getLogger().info("Closing connection."); + listenerException = e; + } catch (Exception e) { + getLogger().error("Unexpected exception. Closing connection.", e); + listenerException = e; + } + } + + // close the socket + try { + if (ois != null) { + ois.close(); + } + } catch (Exception e) { + //getLogger().info("Could not close connection.", e); + } + + // send event to listener, if configured + if (listenerList.size() > 0 && !isClosed()) { + fireSocketClosedEvent(listenerException); + } + } + + /** + * Notifies all registered listeners regarding the closing of the Socket. + * @param listenerException listener exception + */ + private void fireSocketClosedEvent(final Exception listenerException) { + synchronized (listenerList) { + for (Iterator iter = listenerList.iterator(); iter.hasNext();) { + SocketNodeEventListener snel = + (SocketNodeEventListener) iter.next(); + if (snel != null) { + snel.socketClosedEvent(listenerException); + } + } + } + } + + /** + * Notifies all registered listeners regarding the opening of a Socket. + * @param remoteInfo remote info + */ + private void fireSocketOpened(final String remoteInfo) { + synchronized (listenerList) { + for (Iterator iter = listenerList.iterator(); iter.hasNext();) { + SocketNodeEventListener snel = + (SocketNodeEventListener) iter.next(); + if (snel != null) { + snel.socketOpened(remoteInfo); + } + } + } + } + + /** + * Sets if node is paused. + * @param b new value + */ + public void setPaused(final boolean b) { + this.paused = b; + } + + /** + * Get if node is paused. + * @return true if pause. + */ + public boolean isPaused() { + return this.paused; + } + + /** + * Close the node and underlying socket + */ + public void close() throws IOException { + getLogger().debug("closing socket"); + this.closed = true; + socket.close(); + fireSocketClosedEvent(null); + } + + /** + * Get if node is closed. + * @return true if closed. + */ + public boolean isClosed() { + return this.closed; + } +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/SocketNodeEventListener.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/SocketNodeEventListener.java b/src/main/java/org/apache/log4j/net/SocketNodeEventListener.java new file mode 100644 index 0000000..6d17602 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/SocketNodeEventListener.java @@ -0,0 +1,43 @@ +/* + * 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.util.EventListener; + +/** + Interface used to listen for {@link SocketNode} related + events. Clients register an instance of the interface and the + instance is called back when the various events occur. + + @author Mark Womack + @author Paul Smith ([email protected]) +*/ +public interface SocketNodeEventListener extends EventListener { + + /** + * Called when the SocketNode is created and begins awaiting data. + * @param remoteInfo remote info + */ + void socketOpened(String remoteInfo); + + /** + Called when the socket the node was given has been closed. + @param e exception + */ + void socketClosedEvent(Exception e); +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/SocketReceiver.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/SocketReceiver.java b/src/main/java/org/apache/log4j/net/SocketReceiver.java new file mode 100644 index 0000000..9d4aac9 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/SocketReceiver.java @@ -0,0 +1,479 @@ +/* + * 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.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import org.apache.log4j.plugins.Pauseable; +import org.apache.log4j.plugins.Plugin; +import org.apache.log4j.plugins.Receiver; +import org.apache.log4j.spi.LoggerRepository; +import org.apache.log4j.spi.LoggingEvent; + + +/** + SocketReceiver receives a remote logging event on a configured + socket and "posts" it to a LoggerRepository as if the event was + generated locally. This class is designed to receive events from + the SocketAppender class (or classes that send compatible events). + + <p>Once the event has been "posted", it will be handled by the + appenders currently configured in the LoggerRespository. + + @author Mark Womack + @author Scott Deboy ([email protected]) + @author Paul Smith ([email protected]) +*/ +public class SocketReceiver extends Receiver implements Runnable, PortBased, + Pauseable { + /** + * socket map. + */ + private Map socketMap = new HashMap(); + /** + * Paused. + */ + private boolean paused; + /** + * Thread. + */ + private Thread rThread; + /** + * Port. + */ + protected int port; + /** + * Server socket. + */ + private ServerSocket serverSocket; + /** + * Socket list. + */ + private Vector socketList = new Vector(); + + /** + * The MulticastDNS zone advertised by a SocketReceiver + */ + public static final String ZONE = "_log4j_obj_tcpaccept_receiver.local."; + + /** + * Listener. + */ + private SocketNodeEventListener listener = null; + /** + * Listeners. + */ + private List listenerList = Collections.synchronizedList(new ArrayList()); + private boolean advertiseViaMulticastDNS; + private ZeroConfSupport zeroConf; + + /** + * Create new instance. + */ + public SocketReceiver() { + super(); + } + + /** + * Create new instance. + * @param p port + */ + public SocketReceiver(final int p) { + super(); + port = p; + } + + /** + * Create new instance. + * @param p port + * @param repo logger repository + */ + public SocketReceiver(final int p, final LoggerRepository repo) { + super(); + this.port = p; + repository = repo; + } + + /** {@inheritDoc} */ + public int getPort() { + return port; + } + + /** {@inheritDoc} */ + public void setPort(final int p) { + port = p; + } + + /** + * Returns true if the receiver is the same class and they are + * configured for the same properties, and super class also considers + * them to be equivalent. This is used by PluginRegistry when determining + * if the a similarly configured receiver is being started. + * + * @param testPlugin The plugin to test equivalency against. + * @return boolean True if the testPlugin is equivalent to this plugin. + */ + public boolean isEquivalent(final Plugin testPlugin) { + if ((testPlugin != null) && testPlugin instanceof SocketReceiver) { + SocketReceiver sReceiver = (SocketReceiver) testPlugin; + + return (port == sReceiver.getPort() && super.isEquivalent(testPlugin)); + } + + return false; + } + + /** + Starts the SocketReceiver with the current options. */ + public void activateOptions() { + if (!isActive()) { + // shutdown(); + rThread = new Thread(this); + rThread.setDaemon(true); + rThread.start(); + if (advertiseViaMulticastDNS) { + zeroConf = new ZeroConfSupport(ZONE, port, getName()); + zeroConf.advertise(); + } + + active = true; + } + } + + /** + * Called when the receiver should be stopped. Closes the + * server socket and all of the open sockets. + */ + public synchronized void shutdown() { + getLogger().debug(getName() + " received shutdown request"); + + // mark this as no longer running + active = false; + + if (rThread != null) { + rThread.interrupt(); + rThread = null; + } + if (advertiseViaMulticastDNS) { + zeroConf.unadvertise(); + } + + doShutdown(); + } + + /** + * Does the actual shutting down by closing the server socket + * and any connected sockets that have been created. + */ + private synchronized void doShutdown() { + active = false; + + getLogger().debug(getName() + " doShutdown called"); + + // close the server socket + closeServerSocket(); + + // close all of the accepted sockets + closeAllAcceptedSockets(); + } + + /** + * Closes the server socket, if created. + */ + private void closeServerSocket() { + getLogger().debug("{} closing server socket", getName()); + + try { + if (serverSocket != null) { + serverSocket.close(); + } + } catch (Exception e) { + // ignore for now + } + + serverSocket = null; + } + + /** + * Closes all the connected sockets in the List. + */ + private synchronized void closeAllAcceptedSockets() { + for (int x = 0; x < socketList.size(); x++) { + try { + ((Socket) socketList.get(x)).close(); + } catch (Exception e) { + // ignore for now + } + } + + // clear member variables + socketMap.clear(); + socketList.clear(); + } + + /** + Sets the flag to indicate if receiver is active or not. + @param b new value + */ + protected synchronized void setActive(final boolean b) { + active = b; + } + + public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) { + this.advertiseViaMulticastDNS = advertiseViaMulticastDNS; + } + + public boolean isAdvertiseViaMulticastDNS() { + return advertiseViaMulticastDNS; + } + + /** + Loop, accepting new socket connections. */ + public void run() { + /** + * Ensure we start fresh. + */ + closeServerSocket(); + closeAllAcceptedSockets(); + + // start the server socket + try { + serverSocket = new ServerSocket(port); + } catch (Exception e) { + getLogger().error( + "error starting SocketReceiver (" + this.getName() + + "), receiver did not start", e); + active = false; + + return; + } + + Socket socket = null; + + try { + getLogger().debug("in run-about to enter while not interrupted loop"); + + active = true; + + while (!rThread.isInterrupted()) { + // if we have a socket, start watching it + if (socket != null) { + getLogger().debug( + "socket not null - creating and starting socketnode"); + socketList.add(socket); + + SocketNode13 node = new SocketNode13(socket, this); + synchronized (listenerList) { + for (Iterator iter = listenerList.iterator(); + iter.hasNext();) { + SocketNodeEventListener l = + (SocketNodeEventListener) iter.next(); + node.addSocketNodeEventListener(l); + } + } + socketMap.put(socket, node); + new Thread(node).start(); + socket = null; + } + + getLogger().debug("waiting to accept socket"); + + // wait for a socket to open, then loop to start it + socket = serverSocket.accept(); + getLogger().debug("accepted socket"); + } + } catch (Exception e) { + getLogger().warn( + "exception while watching socket server in SocketReceiver (" + + this.getName() + "), stopping"); + } + + getLogger().debug("{} has exited the not interrupted loop", getName()); + + // socket not watched because we a no longer running + // so close it now. + if (socket != null) { + try { + socket.close(); + } catch (IOException e1) { + getLogger().warn("socket exception caught - socket closed"); + } + } + + getLogger().debug("{} is exiting main run loop", getName()); + } + + /** + * Returns a Vector of SocketDetail representing the IP/Domain name + * of the currently connected sockets that this receiver has + * been responsible for creating. + * @return Vector of SocketDetails + */ + public Vector getConnectedSocketDetails() { + Vector details = new Vector(socketList.size()); + + for (Enumeration enumeration = socketList.elements(); + enumeration.hasMoreElements(); + ) { + Socket socket = (Socket) enumeration.nextElement(); + details.add( + new SocketDetail(socket, (SocketNode13) socketMap.get(socket))); + } + + return details; + } + + /** + * Returns the currently configured SocketNodeEventListener that + * will be automatically set for each SocketNode created. + * @return SocketNodeEventListener currently configured + * + * @deprecated This receiver now supports multiple listeners + */ + public SocketNodeEventListener getListener() { + return listener; + } + + /** + * Adds the listener to the list of listeners to be notified of the + * respective event. + * @param l the listener to add to the list + */ + public void addSocketNodeEventListener( + final SocketNodeEventListener l) { + listenerList.add(l); + } + + /** + * Removes the registered Listener from this instances list of + * listeners. If the listener has not been registered, then invoking + * this method has no effect. + * + * @param l the SocketNodeEventListener to remove + */ + public void removeSocketNodeEventListener( + final SocketNodeEventListener l) { + listenerList.remove(l); + } + + /** + * Sets the SocketNodeEventListener that will be used for each + * created SocketNode. + * @param l the listener to set on each creation of a SocketNode + * @deprecated This receiver now supports multiple listeners and + * so this method simply removes the listener (if there already) + * and readds it to the list. + * + * The passed listener will also be returned via the getListener() + * method still, but this is also deprecated + */ + public void setListener(final SocketNodeEventListener l) { + removeSocketNodeEventListener(l); + addSocketNodeEventListener(l); + this.listener = l; + } + + /** {@inheritDoc} */ + public boolean isPaused() { + return paused; + } + + /** {@inheritDoc} */ + public void setPaused(final boolean b) { + paused = b; + } + + /** + * Socket detail. + */ + private static final class SocketDetail implements AddressBased, PortBased, + Pauseable { + /** + * Address. + */ + private String address; + /** + * Port. + */ + private int port; + /** + * Socket node. + */ + private SocketNode13 socketNode; + + /** + * Create new instance. + * @param socket socket + * @param node socket node + */ + private SocketDetail(final Socket socket, + final SocketNode13 node) { + super(); + this.address = socket.getInetAddress().getHostName(); + this.port = socket.getPort(); + this.socketNode = node; + } + + /** {@inheritDoc} */ + public String getAddress() { + return address; + } + + /** {@inheritDoc} */ + public int getPort() { + return port; + } + + /** {@inheritDoc} */ + public String getName() { + return "Socket"; + } + + /** {@inheritDoc} */ + public boolean isActive() { + return true; + } + + /** {@inheritDoc} */ + public boolean isPaused() { + return socketNode.isPaused(); + } + + /** {@inheritDoc} */ + public void setPaused(final boolean b) { + socketNode.setPaused(b); + } + } + /** {@inheritDoc} */ + public void doPost(final LoggingEvent event) { + if (!isPaused()) { + super.doPost(event); + } + } + +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/UDPAppender.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/UDPAppender.java b/src/main/java/org/apache/log4j/net/UDPAppender.java new file mode 100644 index 0000000..55bc6f1 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/UDPAppender.java @@ -0,0 +1,330 @@ +/* + * 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 org.apache.log4j.AppenderSkeleton; +import org.apache.log4j.helpers.Constants; +import org.apache.log4j.spi.LoggingEvent; +import org.apache.log4j.helpers.LogLog; +import org.apache.log4j.xml.XMLLayout; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.UnknownHostException; + + + +/** + * Sends log information as a UDP datagrams. + * + * <p>The UDPAppender is meant to be used as a diagnostic logging tool + * so that logging can be monitored by a simple UDP client. + * + * <p>Messages are not sent as LoggingEvent objects but as text after + * applying the designated Layout. + * + * <p>The port and remoteHost properties can be set in configuration properties. + * By setting the remoteHost to a broadcast address any number of clients can + * listen for log messages. + * + * <p>This was inspired and really extended/copied from {@link SocketAppender}. + * Please see the docs for the proper credit to the authors of that class. + * + * @author <a href="mailto:[email protected]">Kevin Brown</a> + * @author Scott Deboy <[email protected]> + */ +public class UDPAppender extends AppenderSkeleton implements PortBased{ + /** + * The default port number for the UDP packets, 9991. + */ + public static final int DEFAULT_PORT = 9991; + + /** + We remember host name as String in addition to the resolved + InetAddress so that it can be returned via getOption(). + */ + String hostname; + String remoteHost; + String application; + String encoding; + InetAddress address; + int port = DEFAULT_PORT; + DatagramSocket outSocket; + + /** + * The MulticastDNS zone advertised by a UDPAppender + */ + public static final String ZONE = "_log4j_xml_udp_appender.local."; + + // if there is something irrecoverably wrong with the settings, there is no + // point in sending out packeets. + boolean inError = false; + private boolean advertiseViaMulticastDNS; + private ZeroConfSupport zeroConf; + + public UDPAppender() { + super(false); + } + + /** + Sends UDP packets to the <code>address</code> and <code>port</code>. + */ + public UDPAppender(final InetAddress address, final int port) { + super(false); + this.address = address; + this.remoteHost = address.getHostName(); + this.port = port; + activateOptions(); + } + + /** + Sends UDP packets to the <code>address</code> and <code>port</code>. + */ + public UDPAppender(final String host, final int port) { + super(false); + this.port = port; + this.address = getAddressByName(host); + this.remoteHost = host; + activateOptions(); + } + + /** + Open the UDP sender for the <b>RemoteHost</b> and <b>Port</b>. + */ + public void activateOptions() { + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException uhe) { + try { + hostname = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException uhe2) { + hostname = "unknown"; + } + } + + //allow system property of application to be primary + if (application == null) { + application = System.getProperty(Constants.APPLICATION_KEY); + } else { + if (System.getProperty(Constants.APPLICATION_KEY) != null) { + application = application + "-" + System.getProperty(Constants.APPLICATION_KEY); + } + } + + if(remoteHost != null) { + address = getAddressByName(remoteHost); + connect(address, port); + } else { + String err = "The RemoteHost property is required for SocketAppender named "+ name; + LogLog.error(err); + throw new IllegalStateException(err); + } + + if (layout == null) { + layout = new XMLLayout(); + } + + if (advertiseViaMulticastDNS) { + zeroConf = new ZeroConfSupport(ZONE, port, getName()); + zeroConf.advertise(); + } + + super.activateOptions(); + } + + /** + Close this appender. + <p>This will mark the appender as closed and + call then {@link #cleanUp} method. + */ + public synchronized void close() { + if (closed) { + return; + } + + if (advertiseViaMulticastDNS) { + zeroConf.unadvertise(); + } + + this.closed = true; + cleanUp(); + } + + /** + Close the UDP Socket and release the underlying + connector thread if it has been created + */ + public void cleanUp() { + if (outSocket != null) { + try { + outSocket.close(); + } catch (Exception e) { + LogLog.error("Could not close outSocket.", e); + } + + outSocket = null; + } + } + + void connect(InetAddress address, int port) { + if (this.address == null) { + return; + } + + try { + // First, close the previous connection if any. + cleanUp(); + outSocket = new DatagramSocket(); + outSocket.connect(address, port); + } catch (IOException e) { + LogLog.error( + "Could not open UDP Socket for sending.", e); + inError = true; + } + } + + public void append(LoggingEvent event) { + if(inError) { + return; + } + + if (event == null) { + return; + } + + if (address == null) { + return; + } + + if (outSocket != null) { + event.setProperty(Constants.HOSTNAME_KEY, hostname); + if (application != null) { + event.setProperty(Constants.APPLICATION_KEY, application); + } + + try { + StringBuffer buf = new StringBuffer(layout.format(event)); + + byte[] payload; + if(encoding == null) { + payload = buf.toString().getBytes(); + } else { + payload = buf.toString().getBytes(encoding); + } + + DatagramPacket dp = + new DatagramPacket(payload, payload.length, address, port); + outSocket.send(dp); + } catch (IOException e) { + outSocket = null; + LogLog.warn("Detected problem with UDP connection: " + e); + } + } + } + + public boolean isActive() { + return !inError; + } + + InetAddress getAddressByName(String host) { + try { + return InetAddress.getByName(host); + } catch (Exception e) { + LogLog.error("Could not find address of [" + host + "].", e); + return null; + } + } + + /** + The UDPAppender uses layouts. Hence, this method returns + <code>true</code>. + */ + public boolean requiresLayout() { + return true; + } + + /** + The <b>RemoteHost</b> option takes a string value which should be + the host name or ipaddress to send the UDP packets. + */ + public void setRemoteHost(String host) { + remoteHost = host; + } + + /** + Returns value of the <b>RemoteHost</b> option. + */ + public String getRemoteHost() { + return remoteHost; + } + + /** + 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 app) { + this.application = app; + } + + /** + Returns value of the <b>App</b> option. + */ + public String getApplication() { + return application; + } + + /** + The <b>Encoding</b> option specifies how the bytes are encoded. If this option is not specified, + the System encoding is used. + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + Returns value of the <b>Encoding</b> option. + */ + public String getEncoding() { + return encoding; + } + + /** + The <b>Port</b> option takes a positive integer representing + the port where UDP packets will be sent. + */ + public void setPort(int port) { + this.port = port; + } + + /** + Returns value of the <b>Port</b> option. + */ + public int getPort() { + return port; + } + + public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) { + this.advertiseViaMulticastDNS = advertiseViaMulticastDNS; + } + + public boolean isAdvertiseViaMulticastDNS() { + return advertiseViaMulticastDNS; + } +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/UDPReceiver.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/UDPReceiver.java b/src/main/java/org/apache/log4j/net/UDPReceiver.java new file mode 100644 index 0000000..a8c7375 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/UDPReceiver.java @@ -0,0 +1,281 @@ +/* + * 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.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.apache.log4j.plugins.Pauseable; +import org.apache.log4j.plugins.Receiver; +import org.apache.log4j.spi.Decoder; +import org.apache.log4j.spi.LoggingEvent; + + +/** + * Receive LoggingEvents encoded with an XMLLayout, convert the XML data to a + * LoggingEvent and post the LoggingEvent. + * + * @author Scott Deboy <[email protected]> + * + */ +public class UDPReceiver extends Receiver implements PortBased, Pauseable { + private static final int PACKET_LENGTH = 16384; + private UDPReceiverThread receiverThread; + private String encoding; + + //default to log4j xml decoder + private String decoder = "org.apache.log4j.xml.XMLDecoder"; + private Decoder decoderImpl; + protected boolean paused; + private transient boolean closed = false; + private int port; + private DatagramSocket socket; + UDPHandlerThread handlerThread; + private boolean advertiseViaMulticastDNS; + private ZeroConfSupport zeroConf; + + /** + * The MulticastDNS zone advertised by a UDPReceiver + */ + public static final String ZONE = "_log4j_xml_udp_receiver.local."; + + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + /** + * The <b>Encoding</b> option specifies how the bytes are encoded. If this + * option is not specified, the system encoding will be used. + * */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * Returns value of the <b>Encoding</b> option. + */ + public String getEncoding() { + return encoding; + } + + public String getDecoder() { + return decoder; + } + + public void setDecoder(String decoder) { + this.decoder = decoder; + } + + public boolean isPaused() { + return paused; + } + + public void setPaused(boolean b) { + paused = b; + } + + public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) { + this.advertiseViaMulticastDNS = advertiseViaMulticastDNS; + } + + public boolean isAdvertiseViaMulticastDNS() { + return advertiseViaMulticastDNS; + } + + public synchronized void shutdown() { + if(closed == true) { + return; + } + closed = true; + active = false; + // Closing the datagram socket will unblock the UDPReceiverThread if it is + // was waiting to receive data from the socket. + if (socket != null) { + socket.close(); + } + + if (advertiseViaMulticastDNS) { + zeroConf.unadvertise(); + } + + try { + if(handlerThread != null) { + handlerThread.close(); + handlerThread.join(); + } + if(receiverThread != null) { + receiverThread.join(); + } + } catch(InterruptedException ie) { + } + } + + /** + Returns true if this receiver is active. */ +// public synchronized boolean isActive() { +// return isActive; +//} + + public void activateOptions() { + try { + Class c = Class.forName(decoder); + Object o = c.newInstance(); + + if (o instanceof Decoder) { + this.decoderImpl = (Decoder) o; + } + } catch (ClassNotFoundException cnfe) { + getLogger().warn("Unable to find decoder", cnfe); + } catch (IllegalAccessException iae) { + getLogger().warn("Could not construct decoder", iae); + } catch (InstantiationException ie) { + getLogger().warn("Could not construct decoder", ie); + } + + try { + socket = new DatagramSocket(port); + receiverThread = new UDPReceiverThread(); + receiverThread.start(); + handlerThread = new UDPHandlerThread(); + handlerThread.start(); + if (advertiseViaMulticastDNS) { + zeroConf = new ZeroConfSupport(ZONE, port, getName()); + zeroConf.advertise(); + } + active = true; + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + class UDPHandlerThread extends Thread { + private List list = new ArrayList(); + + public UDPHandlerThread() { + setDaemon(true); + } + + public void append(String data) { + synchronized (list) { + list.add(data); + list.notify(); + } + } + + /** + * Allow the UDPHandlerThread to wakeup and exit gracefully. + */ + void close() { + synchronized(list) { + list.notify(); + } + } + + public void run() { + ArrayList list2 = new ArrayList(); + + while (!UDPReceiver.this.closed) { + synchronized (list) { + try { + while (!UDPReceiver.this.closed && list.size() == 0) { + list.wait(300); + } + + if (list.size() > 0) { + list2.addAll(list); + list.clear(); + } + } catch (InterruptedException ie) { + } + } + + if (list2.size() > 0) { + Iterator iter = list2.iterator(); + + while (iter.hasNext()) { + String data = (String) iter.next(); + List v = decoderImpl.decodeEvents(data); + + if (v != null) { + Iterator eventIter = v.iterator(); + + while (eventIter.hasNext()) { + if (!isPaused()) { + doPost((LoggingEvent) eventIter.next()); + } + } + } + } + + list2.clear(); + } else { + try { + synchronized (this) { + wait(1000); + } + } catch (InterruptedException ie) { + } + } + } // while + getLogger().debug(UDPReceiver.this.getName()+ "'s handler thread is exiting"); + } // run + } // UDPHandlerThread + + class UDPReceiverThread extends Thread { + public UDPReceiverThread() { + setDaemon(true); + } + + public void run() { + byte[] b = new byte[PACKET_LENGTH]; + DatagramPacket p = new DatagramPacket(b, b.length); + + while (!UDPReceiver.this.closed) { + try { + socket.receive(p); + + //this string constructor which accepts a charset throws an exception if it is + //null + if (encoding == null) { + handlerThread.append( + new String(p.getData(), 0, p.getLength())); + } else { + handlerThread.append( + new String(p.getData(), 0, p.getLength(), encoding)); + } + } catch (SocketException se) { + //disconnected + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + + //LogLog.debug(UDPReceiver.this.getName() + "'s thread is ending."); + } + } +} http://git-wip-us.apache.org/repos/asf/logging-chainsaw/blob/08c7be5c/src/main/java/org/apache/log4j/net/XMLSocketNode.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/log4j/net/XMLSocketNode.java b/src/main/java/org/apache/log4j/net/XMLSocketNode.java new file mode 100644 index 0000000..95ab638 --- /dev/null +++ b/src/main/java/org/apache/log4j/net/XMLSocketNode.java @@ -0,0 +1,205 @@ +/* + * 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 org.apache.log4j.*; +import org.apache.log4j.helpers.Constants; +import org.apache.log4j.plugins.Receiver; +import org.apache.log4j.spi.*; + +import java.io.IOException; +import java.io.InputStream; + +import java.net.Socket; + +import java.util.Iterator; +import java.util.List; + + +/** + Read {@link LoggingEvent} objects sent from a remote client using XML over + Sockets (TCP). These logging events are logged according to local + policy, as if they were generated locally. + + <p>For example, the socket node might decide to log events to a + local file and also resent them to a second socket node. + + @author Scott Deboy <[email protected]>; + + @since 0.8.4 +*/ +public class XMLSocketNode extends ComponentBase implements Runnable { + Socket socket; + Receiver receiver; + Decoder decoder; + SocketNodeEventListener listener; + + /** + Constructor for socket and logger repository. */ + public XMLSocketNode( + String decoder, Socket socket, LoggerRepository hierarchy) { + this.repository = hierarchy; + try { + Class c = Class.forName(decoder); + Object o = c.newInstance(); + + if (o instanceof Decoder) { + this.decoder = (Decoder) o; + } + } catch (ClassNotFoundException cnfe) { + getLogger().warn("Unable to find decoder", cnfe); + } catch (IllegalAccessException iae) { + getLogger().warn("Unable to construct decoder", iae); + } catch (InstantiationException ie) { + getLogger().warn("Unable to construct decoder", ie); + } + + this.socket = socket; + } + + /** + Constructor for socket and reciever. */ + public XMLSocketNode(String decoder, Socket socket, Receiver receiver) { + try { + Class c = Class.forName(decoder); + Object o = c.newInstance(); + + if (o instanceof Decoder) { + this.decoder = (Decoder) o; + } + } catch (ClassNotFoundException cnfe) { + getLogger().warn("Unable to find decoder", cnfe); + } catch (IllegalAccessException iae) { + getLogger().warn("Unable to construct decoder", iae); + } catch (InstantiationException ie) { + getLogger().warn("Unable to construct decoder", ie); + } + + this.socket = socket; + this.receiver = receiver; + } + + /** + Set the event listener on this node. */ + public void setListener(SocketNodeEventListener _listener) { + listener = _listener; + } + + public void run() { + Logger remoteLogger; + Exception listenerException = null; + InputStream is = null; + + if ((this.receiver == null) || (this.decoder == null)) { + is = null; + listenerException = + new Exception( + "No receiver or decoder provided. Cannot process xml socket events"); + getLogger().error( + "Exception constructing XML Socket Receiver", listenerException); + } + + try { + is = socket.getInputStream(); + } catch (Exception e) { + is = null; + listenerException = e; + getLogger().error("Exception opening ObjectInputStream to " + socket, e); + } + + if (is != null) { + String hostName = socket.getInetAddress().getHostName(); + String remoteInfo = hostName + ":" + socket.getPort(); + + try { + //read data from the socket + //it's up to the individual decoder to handle incomplete event data + while (true) { + byte[] b = new byte[1024]; + int length = is.read(b); + if (length == -1) { + getLogger().info( + "no bytes read from stream - closing connection."); + break; + } + List v = decoder.decodeEvents(new String(b, 0, length)); + + if (v != null) { + Iterator iter = v.iterator(); + + while (iter.hasNext()) { + LoggingEvent e = (LoggingEvent) iter.next(); + e.setProperty(Constants.HOSTNAME_KEY, hostName); + + // store the known remote info in an event property + e.setProperty("log4j.remoteSourceInfo", remoteInfo); + + // if configured with a receiver, tell it to post the event + if (receiver != null) { + receiver.doPost(e); + + // else post it via the hierarchy + } else { + // get a logger from the hierarchy. The name of the logger + // is taken to be the name contained in the event. + remoteLogger = repository.getLogger(e.getLoggerName()); + + //event.logger = remoteLogger; + // apply the logger-level filter + if ( + e.getLevel().isGreaterOrEqual( + remoteLogger.getEffectiveLevel())) { + // finally log the event as if was generated locally + remoteLogger.callAppenders(e); + } + } + } + } + } + } catch (java.io.EOFException e) { + getLogger().info("Caught java.io.EOFException closing connection."); + listenerException = e; + } catch (java.net.SocketException e) { + getLogger().info( + "Caught java.net.SocketException closing connection."); + listenerException = e; + } catch (IOException e) { + getLogger().info("Caught java.io.IOException: " + e); + getLogger().info("Closing connection."); + listenerException = e; + } catch (Exception e) { + getLogger().error("Unexpected exception. Closing connection.", e); + listenerException = e; + } + } + + // close the socket + try { + if (is != null) { + is.close(); + } + } catch (Exception e) { + //logger.info("Could not close connection.", e); + } + + // send event to listener, if configured + if (listener != null) { + listener.socketClosedEvent(listenerException); + } + } +}
