1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.log4j.net;
19
20 import java.io.IOException;
21 import java.io.InterruptedIOException;
22 import java.io.ObjectOutputStream;
23 import java.net.InetAddress;
24 import java.net.ServerSocket;
25 import java.net.Socket;
26 import java.net.SocketException;
27 import java.util.Vector;
28
29 import org.apache.log4j.AppenderSkeleton;
30 import org.apache.log4j.helpers.CyclicBuffer;
31 import org.apache.log4j.helpers.LogLog;
32 import org.apache.log4j.spi.LoggingEvent;
33
34 /**
35 Sends {@link LoggingEvent} objects to a set of remote log servers,
36 usually a {@link SocketNode SocketNodes}.
37
38 <p>Acts just like {@link SocketAppender} except that instead of
39 connecting to a given remote log server,
40 <code>SocketHubAppender</code> accepts connections from the remote
41 log servers as clients. It can accept more than one connection.
42 When a log event is received, the event is sent to the set of
43 currently connected remote log servers. Implemented this way it does
44 not require any update to the configuration file to send data to
45 another remote log server. The remote log server simply connects to
46 the host and port the <code>SocketHubAppender</code> is running on.
47
48 <p>The <code>SocketHubAppender</code> does not store events such
49 that the remote side will events that arrived after the
50 establishment of its connection. Once connected, events arrive in
51 order as guaranteed by the TCP protocol.
52
53 <p>This implementation borrows heavily from the {@link
54 SocketAppender}.
55
56 <p>The SocketHubAppender has the following characteristics:
57
58 <ul>
59
60 <p><li>If sent to a {@link SocketNode}, logging is non-intrusive as
61 far as the log event is concerned. In other words, the event will be
62 logged with the same time stamp, {@link org.apache.log4j.NDC},
63 location info as if it were logged locally.
64
65 <p><li><code>SocketHubAppender</code> does not use a layout. It
66 ships a serialized {@link LoggingEvent} object to the remote side.
67
68 <p><li><code>SocketHubAppender</code> relies on the TCP
69 protocol. Consequently, if the remote side is reachable, then log
70 events will eventually arrive at remote client.
71
72 <p><li>If no remote clients are attached, the logging requests are
73 simply dropped.
74
75 <p><li>Logging events are automatically <em>buffered</em> by the
76 native TCP implementation. This means that if the link to remote
77 client is slow but still faster than the rate of (log) event
78 production, the application will not be affected by the slow network
79 connection. However, if the network connection is slower then the
80 rate of event production, then the local application can only
81 progress at the network rate. In particular, if the network link to
82 the the remote client is down, the application will be blocked.
83
84 <p>On the other hand, if the network link is up, but the remote
85 client is down, the client will not be blocked when making log
86 requests but the log events will be lost due to client
87 unavailability.
88
89 <p>The single remote client case extends to multiple clients
90 connections. The rate of logging will be determined by the slowest
91 link.
92
93 <p><li>If the JVM hosting the <code>SocketHubAppender</code> exits
94 before the <code>SocketHubAppender</code> is closed either
95 explicitly or subsequent to garbage collection, then there might
96 be untransmitted data in the pipe which might be lost. This is a
97 common problem on Windows based systems.
98
99 <p>To avoid lost data, it is usually sufficient to {@link #close}
100 the <code>SocketHubAppender</code> either explicitly or by calling
101 the {@link org.apache.log4j.LogManager#shutdown} method before
102 exiting the application.
103
104 </ul>
105
106 @author Mark Womack */
107
108 public class SocketHubAppender extends AppenderSkeleton {
109
110 /**
111 The default port number of the ServerSocket will be created on. */
112 static final int DEFAULT_PORT = 4560;
113
114 private int port = DEFAULT_PORT;
115 private Vector oosList = new Vector();
116 private ServerMonitor serverMonitor = null;
117 private boolean locationInfo = false;
118 private CyclicBuffer buffer = null;
119 private String application;
120 private boolean advertiseViaMulticastDNS;
121 private ZeroConfSupport zeroConf;
122
123 /**
124 * The MulticastDNS zone advertised by a SocketHubAppender
125 */
126 public static final String ZONE = "_log4j_obj_tcpaccept_appender.local.";
127 private ServerSocket serverSocket;
128
129
130 public SocketHubAppender() { }
131
132 /**
133 Connects to remote server at <code>address</code> and <code>port</code>. */
134 public
135 SocketHubAppender(int _port) {
136 port = _port;
137 startServer();
138 }
139
140 /**
141 Set up the socket server on the specified port. */
142 public
143 void activateOptions() {
144 if (advertiseViaMulticastDNS) {
145 zeroConf = new ZeroConfSupport(ZONE, port, getName());
146 zeroConf.advertise();
147 }
148 startServer();
149 }
150
151 /**
152 Close this appender.
153 <p>This will mark the appender as closed and
154 call then {@link #cleanUp} method. */
155 synchronized
156 public
157 void close() {
158 if(closed)
159 return;
160
161 LogLog.debug("closing SocketHubAppender " + getName());
162 this.closed = true;
163 if (advertiseViaMulticastDNS) {
164 zeroConf.unadvertise();
165 }
166 cleanUp();
167
168 LogLog.debug("SocketHubAppender " + getName() + " closed");
169 }
170
171 /**
172 Release the underlying ServerMonitor thread, and drop the connections
173 to all connected remote servers. */
174 public
175 void cleanUp() {
176 // stop the monitor thread
177 LogLog.debug("stopping ServerSocket");
178 serverMonitor.stopMonitor();
179 serverMonitor = null;
180
181 // close all of the connections
182 LogLog.debug("closing client connections");
183 while (oosList.size() != 0) {
184 ObjectOutputStream oos = (ObjectOutputStream)oosList.elementAt(0);
185 if(oos != null) {
186 try {
187 oos.close();
188 } catch(InterruptedIOException e) {
189 Thread.currentThread().interrupt();
190 LogLog.error("could not close oos.", e);
191 } catch(IOException e) {
192 LogLog.error("could not close oos.", e);
193 }
194
195 oosList.removeElementAt(0);
196 }
197 }
198 }
199
200 /**
201 Append an event to all of current connections. */
202 public
203 void append(LoggingEvent event) {
204 if (event != null) {
205 // set up location info if requested
206 if (locationInfo) {
207 event.getLocationInformation();
208 }
209 if (application != null) {
210 event.setProperty("application", application);
211 }
212 event.getNDC();
213 event.getThreadName();
214 event.getMDCCopy();
215 event.getRenderedMessage();
216 event.getThrowableStrRep();
217
218 if (buffer != null) {
219 buffer.add(event);
220 }
221 }
222
223 // if no event or no open connections, exit now
224 if ((event == null) || (oosList.size() == 0)) {
225 return;
226 }
227
228 // loop through the current set of open connections, appending the event to each
229 for (int streamCount = 0; streamCount < oosList.size(); streamCount++) {
230
231 ObjectOutputStream oos = null;
232 try {
233 oos = (ObjectOutputStream)oosList.elementAt(streamCount);
234 }
235 catch (ArrayIndexOutOfBoundsException e) {
236 // catch this, but just don't assign a value
237 // this should not really occur as this method is
238 // the only one that can remove oos's (besides cleanUp).
239 }
240
241 // list size changed unexpectedly? Just exit the append.
242 if (oos == null)
243 break;
244
245 try {
246 oos.writeObject(event);
247 oos.flush();
248 // Failing to reset the object output stream every now and
249 // then creates a serious memory leak.
250 // right now we always reset. TODO - set up frequency counter per oos?
251 oos.reset();
252 }
253 catch(IOException e) {
254 if (e instanceof InterruptedIOException) {
255 Thread.currentThread().interrupt();
256 }
257 // there was an io exception so just drop the connection
258 oosList.removeElementAt(streamCount);
259 LogLog.debug("dropped connection");
260
261 // decrement to keep the counter in place (for loop always increments)
262 streamCount--;
263 }
264 }
265 }
266
267 /**
268 The SocketHubAppender does not use a layout. Hence, this method returns
269 <code>false</code>. */
270 public
271 boolean requiresLayout() {
272 return false;
273 }
274
275 /**
276 The <b>Port</b> option takes a positive integer representing
277 the port where the server is waiting for connections. */
278 public
279 void setPort(int _port) {
280 port = _port;
281 }
282
283 /**
284 * 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
285 * property), don't set here.
286 */
287 public
288 void setApplication(String lapp) {
289 this.application = lapp;
290 }
291
292 /**
293 * Returns value of the <b>Application</b> option.
294 */
295 public
296 String getApplication() {
297 return application;
298 }
299
300 /**
301 Returns value of the <b>Port</b> option. */
302 public
303 int getPort() {
304 return port;
305 }
306
307 /**
308 * The <b>BufferSize</b> option takes a positive integer representing the number of events this appender will buffer and send to newly connected
309 * clients.
310 */
311 public
312 void setBufferSize(int _bufferSize) {
313 buffer = new CyclicBuffer(_bufferSize);
314 }
315
316 /**
317 * Returns value of the <b>bufferSize</b> option.
318 */
319 public
320 int getBufferSize() {
321 if (buffer == null) {
322 return 0;
323 } else {
324 return buffer.getMaxSize();
325 }
326 }
327
328 /**
329 The <b>LocationInfo</b> option takes a boolean value. If true,
330 the information sent to the remote host will include location
331 information. By default no location information is sent to the server. */
332 public
333 void setLocationInfo(boolean _locationInfo) {
334 locationInfo = _locationInfo;
335 }
336
337 /**
338 Returns value of the <b>LocationInfo</b> option. */
339 public
340 boolean getLocationInfo() {
341 return locationInfo;
342 }
343
344 public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
345 this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
346 }
347
348 public boolean isAdvertiseViaMulticastDNS() {
349 return advertiseViaMulticastDNS;
350 }
351
352 /**
353 Start the ServerMonitor thread. */
354 private
355 void startServer() {
356 serverMonitor = new ServerMonitor(port, oosList);
357 }
358
359 /**
360 * Creates a server socket to accept connections.
361 * @param socketPort port on which the socket should listen, may be zero.
362 * @return new socket.
363 * @throws IOException IO error when opening the socket.
364 */
365 protected ServerSocket createServerSocket(final int socketPort) throws IOException {
366 return new ServerSocket(socketPort);
367 }
368
369 /**
370 This class is used internally to monitor a ServerSocket
371 and register new connections in a vector passed in the
372 constructor. */
373 private class ServerMonitor implements Runnable {
374 private int port;
375 private Vector oosList;
376 private boolean keepRunning;
377 private Thread monitorThread;
378
379 /**
380 Create a thread and start the monitor. */
381 public
382 ServerMonitor(int _port, Vector _oosList) {
383 port = _port;
384 oosList = _oosList;
385 keepRunning = true;
386 monitorThread = new Thread(this);
387 monitorThread.setDaemon(true);
388 monitorThread.setName("SocketHubAppender-Monitor-" + port);
389 monitorThread.start();
390 }
391
392 /**
393 Stops the monitor. This method will not return until
394 the thread has finished executing. */
395 public synchronized void stopMonitor() {
396 if (keepRunning) {
397 LogLog.debug("server monitor thread shutting down");
398 keepRunning = false;
399 try {
400 if (serverSocket != null) {
401 serverSocket.close();
402 serverSocket = null;
403 }
404 } catch (IOException ioe) {}
405
406 try {
407 monitorThread.join();
408 }
409 catch (InterruptedException e) {
410 Thread.currentThread().interrupt();
411 // do nothing?
412 }
413
414 // release the thread
415 monitorThread = null;
416 LogLog.debug("server monitor thread shut down");
417 }
418 }
419
420 private
421 void sendCachedEvents(ObjectOutputStream stream) throws IOException {
422 if (buffer != null) {
423 for (int i = 0; i < buffer.length(); i++) {
424 stream.writeObject(buffer.get(i));
425 }
426 stream.flush();
427 stream.reset();
428 }
429 }
430
431 /**
432 Method that runs, monitoring the ServerSocket and adding connections as
433 they connect to the socket. */
434 public
435 void run() {
436 serverSocket = null;
437 try {
438 serverSocket = createServerSocket(port);
439 serverSocket.setSoTimeout(1000);
440 }
441 catch (Exception e) {
442 if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
443 Thread.currentThread().interrupt();
444 }
445 LogLog.error("exception setting timeout, shutting down server socket.", e);
446 keepRunning = false;
447 return;
448 }
449
450 try {
451 try {
452 serverSocket.setSoTimeout(1000);
453 }
454 catch (SocketException e) {
455 LogLog.error("exception setting timeout, shutting down server socket.", e);
456 return;
457 }
458
459 while (keepRunning) {
460 Socket socket = null;
461 try {
462 socket = serverSocket.accept();
463 }
464 catch (InterruptedIOException e) {
465 // timeout occurred, so just loop
466 }
467 catch (SocketException e) {
468 LogLog.error("exception accepting socket, shutting down server socket.", e);
469 keepRunning = false;
470 }
471 catch (IOException e) {
472 LogLog.error("exception accepting socket.", e);
473 }
474
475 // if there was a socket accepted
476 if (socket != null) {
477 try {
478 InetAddress remoteAddress = socket.getInetAddress();
479 LogLog.debug("accepting connection from " + remoteAddress.getHostName()
480 + " (" + remoteAddress.getHostAddress() + ")");
481
482 // create an ObjectOutputStream
483 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
484 if (buffer != null && buffer.length() > 0) {
485 sendCachedEvents(oos);
486 }
487
488 // add it to the oosList. OK since Vector is synchronized.
489 oosList.addElement(oos);
490 } catch (IOException e) {
491 if (e instanceof InterruptedIOException) {
492 Thread.currentThread().interrupt();
493 }
494 LogLog.error("exception creating output stream on socket.", e);
495 }
496 }
497 }
498 }
499 finally {
500 // close the socket
501 try {
502 serverSocket.close();
503 } catch(InterruptedIOException e) {
504 Thread.currentThread().interrupt();
505 } catch (IOException e) {
506 // do nothing with it?
507 }
508 }
509 }
510 }
511 }
512