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
128
129 public SocketHubAppender() { }
130
131 /**
132 Connects to remote server at <code>address</code> and <code>port</code>. */
133 public
134 SocketHubAppender(int _port) {
135 port = _port;
136 startServer();
137 }
138
139 /**
140 Set up the socket server on the specified port. */
141 public
142 void activateOptions() {
143 if (advertiseViaMulticastDNS) {
144 zeroConf = new ZeroConfSupport(ZONE, port, getName());
145 zeroConf.advertise();
146 }
147 startServer();
148 }
149
150 /**
151 Close this appender.
152 <p>This will mark the appender as closed and
153 call then {@link #cleanUp} method. */
154 synchronized
155 public
156 void close() {
157 if(closed)
158 return;
159
160 LogLog.debug("closing SocketHubAppender " + getName());
161 this.closed = true;
162 if (advertiseViaMulticastDNS) {
163 zeroConf.unadvertise();
164 }
165 cleanUp();
166
167 LogLog.debug("SocketHubAppender " + getName() + " closed");
168 }
169
170 /**
171 Release the underlying ServerMonitor thread, and drop the connections
172 to all connected remote servers. */
173 public
174 void cleanUp() {
175 // stop the monitor thread
176 LogLog.debug("stopping ServerSocket");
177 serverMonitor.stopMonitor();
178 serverMonitor = null;
179
180 // close all of the connections
181 LogLog.debug("closing client connections");
182 while (oosList.size() != 0) {
183 ObjectOutputStream oos = (ObjectOutputStream)oosList.elementAt(0);
184 if(oos != null) {
185 try {
186 oos.close();
187 } catch(InterruptedIOException e) {
188 Thread.currentThread().interrupt();
189 LogLog.error("could not close oos.", e);
190 } catch(IOException e) {
191 LogLog.error("could not close oos.", e);
192 }
193
194 oosList.removeElementAt(0);
195 }
196 }
197 }
198
199 /**
200 Append an event to all of current connections. */
201 public
202 void append(LoggingEvent event) {
203 if (event != null) {
204 // set up location info if requested
205 if (locationInfo) {
206 event.getLocationInformation();
207 }
208 if (application != null) {
209 event.setProperty("application", application);
210 }
211 event.getNDC();
212 event.getThreadName();
213 event.getMDCCopy();
214 event.getRenderedMessage();
215 event.getThrowableStrRep();
216
217 if (buffer != null) {
218 buffer.add(event);
219 }
220 }
221
222 // if no event or no open connections, exit now
223 if ((event == null) || (oosList.size() == 0)) {
224 return;
225 }
226
227 // loop through the current set of open connections, appending the event to each
228 for (int streamCount = 0; streamCount < oosList.size(); streamCount++) {
229
230 ObjectOutputStream oos = null;
231 try {
232 oos = (ObjectOutputStream)oosList.elementAt(streamCount);
233 }
234 catch (ArrayIndexOutOfBoundsException e) {
235 // catch this, but just don't assign a value
236 // this should not really occur as this method is
237 // the only one that can remove oos's (besides cleanUp).
238 }
239
240 // list size changed unexpectedly? Just exit the append.
241 if (oos == null)
242 break;
243
244 try {
245 oos.writeObject(event);
246 oos.flush();
247 // Failing to reset the object output stream every now and
248 // then creates a serious memory leak.
249 // right now we always reset. TODO - set up frequency counter per oos?
250 oos.reset();
251 }
252 catch(IOException e) {
253 if (e instanceof InterruptedIOException) {
254 Thread.currentThread().interrupt();
255 }
256 // there was an io exception so just drop the connection
257 oosList.removeElementAt(streamCount);
258 LogLog.debug("dropped connection");
259
260 // decrement to keep the counter in place (for loop always increments)
261 streamCount--;
262 }
263 }
264 }
265
266 /**
267 The SocketHubAppender does not use a layout. Hence, this method returns
268 <code>false</code>. */
269 public
270 boolean requiresLayout() {
271 return false;
272 }
273
274 /**
275 The <b>Port</b> option takes a positive integer representing
276 the port where the server is waiting for connections. */
277 public
278 void setPort(int _port) {
279 port = _port;
280 }
281
282 /**
283 * 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
284 * property), don't set here.
285 */
286 public
287 void setApplication(String lapp) {
288 this.application = lapp;
289 }
290
291 /**
292 * Returns value of the <b>Application</b> option.
293 */
294 public
295 String getApplication() {
296 return application;
297 }
298
299 /**
300 Returns value of the <b>Port</b> option. */
301 public
302 int getPort() {
303 return port;
304 }
305
306 /**
307 * The <b>BufferSize</b> option takes a positive integer representing the number of events this appender will buffer and send to newly connected
308 * clients.
309 */
310 public
311 void setBufferSize(int _bufferSize) {
312 buffer = new CyclicBuffer(_bufferSize);
313 }
314
315 /**
316 * Returns value of the <b>bufferSize</b> option.
317 */
318 public
319 int getBufferSize() {
320 if (buffer == null) {
321 return 0;
322 } else {
323 return buffer.getMaxSize();
324 }
325 }
326
327 /**
328 The <b>LocationInfo</b> option takes a boolean value. If true,
329 the information sent to the remote host will include location
330 information. By default no location information is sent to the server. */
331 public
332 void setLocationInfo(boolean _locationInfo) {
333 locationInfo = _locationInfo;
334 }
335
336 /**
337 Returns value of the <b>LocationInfo</b> option. */
338 public
339 boolean getLocationInfo() {
340 return locationInfo;
341 }
342
343 public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) {
344 this.advertiseViaMulticastDNS = advertiseViaMulticastDNS;
345 }
346
347 public boolean isAdvertiseViaMulticastDNS() {
348 return advertiseViaMulticastDNS;
349 }
350
351 /**
352 Start the ServerMonitor thread. */
353 private
354 void startServer() {
355 serverMonitor = new ServerMonitor(port, oosList);
356 }
357
358 /**
359 * Creates a server socket to accept connections.
360 * @param socketPort port on which the socket should listen, may be zero.
361 * @return new socket.
362 * @throws IOException IO error when opening the socket.
363 */
364 protected ServerSocket createServerSocket(final int socketPort) throws IOException {
365 return new ServerSocket(socketPort);
366 }
367
368 /**
369 This class is used internally to monitor a ServerSocket
370 and register new connections in a vector passed in the
371 constructor. */
372 private
373 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
396 synchronized
397 void stopMonitor() {
398 if (keepRunning) {
399 LogLog.debug("server monitor thread shutting down");
400 keepRunning = false;
401 try {
402 monitorThread.join();
403 }
404 catch (InterruptedException e) {
405 Thread.currentThread().interrupt();
406 // do nothing?
407 }
408
409 // release the thread
410 monitorThread = null;
411 LogLog.debug("server monitor thread shut down");
412 }
413 }
414
415 private
416 void sendCachedEvents(ObjectOutputStream stream) throws IOException {
417 if (buffer != null) {
418 for (int i = 0; i < buffer.length(); i++) {
419 stream.writeObject(buffer.get(i));
420 }
421 stream.flush();
422 stream.reset();
423 }
424 }
425
426 /**
427 Method that runs, monitoring the ServerSocket and adding connections as
428 they connect to the socket. */
429 public
430 void run() {
431 ServerSocket serverSocket = null;
432 try {
433 serverSocket = createServerSocket(port);
434 serverSocket.setSoTimeout(1000);
435 }
436 catch (Exception e) {
437 if (e instanceof InterruptedIOException || e instanceof InterruptedException) {
438 Thread.currentThread().interrupt();
439 }
440 LogLog.error("exception setting timeout, shutting down server socket.", e);
441 keepRunning = false;
442 return;
443 }
444
445 try {
446 try {
447 serverSocket.setSoTimeout(1000);
448 }
449 catch (SocketException e) {
450 LogLog.error("exception setting timeout, shutting down server socket.", e);
451 return;
452 }
453
454 while (keepRunning) {
455 Socket socket = null;
456 try {
457 socket = serverSocket.accept();
458 }
459 catch (InterruptedIOException e) {
460 // timeout occurred, so just loop
461 }
462 catch (SocketException e) {
463 LogLog.error("exception accepting socket, shutting down server socket.", e);
464 keepRunning = false;
465 }
466 catch (IOException e) {
467 LogLog.error("exception accepting socket.", e);
468 }
469
470 // if there was a socket accepted
471 if (socket != null) {
472 try {
473 InetAddress remoteAddress = socket.getInetAddress();
474 LogLog.debug("accepting connection from " + remoteAddress.getHostName()
475 + " (" + remoteAddress.getHostAddress() + ")");
476
477 // create an ObjectOutputStream
478 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
479 if (buffer != null && buffer.length() > 0) {
480 sendCachedEvents(oos);
481 }
482
483 // add it to the oosList. OK since Vector is synchronized.
484 oosList.addElement(oos);
485 } catch (IOException e) {
486 if (e instanceof InterruptedIOException) {
487 Thread.currentThread().interrupt();
488 }
489 LogLog.error("exception creating output stream on socket.", e);
490 }
491 }
492 }
493 }
494 finally {
495 // close the socket
496 try {
497 serverSocket.close();
498 } catch(InterruptedIOException e) {
499 Thread.currentThread().interrupt();
500 } catch (IOException e) {
501 // do nothing with it?
502 }
503 }
504 }
505 }
506 }
507