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.util.Vector;
21 import java.net.Socket;
22 import java.net.ServerSocket;
23 import java.net.SocketException;
24 import java.io.ObjectOutputStream;
25 import java.io.IOException;
26 import java.io.InterruptedIOException;
27 import java.net.InetAddress;
28
29 import org.apache.log4j.helpers.LogLog;
30 import org.apache.log4j.spi.LoggingEvent;
31 import org.apache.log4j.AppenderSkeleton;
32
33 /**
34 Sends {@link LoggingEvent} objects to a set of remote log servers,
35 usually a {@link SocketNode SocketNodes}.
36
37 <p>Acts just like {@link SocketAppender} except that instead of
38 connecting to a given remote log server,
39 <code>SocketHubAppender</code> accepts connections from the remote
40 log servers as clients. It can accept more than one connection.
41 When a log event is received, the event is sent to the set of
42 currently connected remote log servers. Implemented this way it does
43 not require any update to the configuration file to send data to
44 another remote log server. The remote log server simply connects to
45 the host and port the <code>SocketHubAppender</code> is running on.
46
47 <p>The <code>SocketHubAppender</code> does not store events such
48 that the remote side will events that arrived after the
49 establishment of its connection. Once connected, events arrive in
50 order as guaranteed by the TCP protocol.
51
52 <p>This implementation borrows heavily from the {@link
53 SocketAppender}.
54
55 <p>The SocketHubAppender has the following characteristics:
56
57 <ul>
58
59 <p><li>If sent to a {@link SocketNode}, logging is non-intrusive as
60 far as the log event is concerned. In other words, the event will be
61 logged with the same time stamp, {@link org.apache.log4j.NDC},
62 location info as if it were logged locally.
63
64 <p><li><code>SocketHubAppender</code> does not use a layout. It
65 ships a serialized {@link LoggingEvent} object to the remote side.
66
67 <p><li><code>SocketHubAppender</code> relies on the TCP
68 protocol. Consequently, if the remote side is reachable, then log
69 events will eventually arrive at remote client.
70
71 <p><li>If no remote clients are attached, the logging requests are
72 simply dropped.
73
74 <p><li>Logging events are automatically <em>buffered</em> by the
75 native TCP implementation. This means that if the link to remote
76 client is slow but still faster than the rate of (log) event
77 production, the application will not be affected by the slow network
78 connection. However, if the network connection is slower then the
79 rate of event production, then the local application can only
80 progress at the network rate. In particular, if the network link to
81 the the remote client is down, the application will be blocked.
82
83 <p>On the other hand, if the network link is up, but the remote
84 client is down, the client will not be blocked when making log
85 requests but the log events will be lost due to client
86 unavailability.
87
88 <p>The single remote client case extends to multiple clients
89 connections. The rate of logging will be determined by the slowest
90 link.
91
92 <p><li>If the JVM hosting the <code>SocketHubAppender</code> exits
93 before the <code>SocketHubAppender</code> is closed either
94 explicitly or subsequent to garbage collection, then there might
95 be untransmitted data in the pipe which might be lost. This is a
96 common problem on Windows based systems.
97
98 <p>To avoid lost data, it is usually sufficient to {@link #close}
99 the <code>SocketHubAppender</code> either explicitly or by calling
100 the {@link org.apache.log4j.LogManager#shutdown} method before
101 exiting the application.
102
103 </ul>
104
105 @author Mark Womack */
106
107 public class SocketHubAppender extends AppenderSkeleton {
108
109 /**
110 The default port number of the ServerSocket will be created on. */
111 static final int DEFAULT_PORT = 4560;
112
113 private int port = DEFAULT_PORT;
114 private Vector oosList = new Vector();
115 private ServerMonitor serverMonitor = null;
116 private boolean locationInfo = false;
117
118 public SocketHubAppender() { }
119
120 /**
121 Connects to remote server at <code>address</code> and <code>port</code>. */
122 public
123 SocketHubAppender(int _port) {
124 port = _port;
125 startServer();
126 }
127
128 /**
129 Set up the socket server on the specified port. */
130 public
131 void activateOptions() {
132 startServer();
133 }
134
135 /**
136 Close this appender.
137 <p>This will mark the appender as closed and
138 call then {@link #cleanUp} method. */
139 synchronized
140 public
141 void close() {
142 if(closed)
143 return;
144
145 LogLog.debug("closing SocketHubAppender " + getName());
146 this.closed = true;
147 cleanUp();
148 LogLog.debug("SocketHubAppender " + getName() + " closed");
149 }
150
151 /**
152 Release the underlying ServerMonitor thread, and drop the connections
153 to all connected remote servers. */
154 public
155 void cleanUp() {
156 // stop the monitor thread
157 LogLog.debug("stopping ServerSocket");
158 serverMonitor.stopMonitor();
159 serverMonitor = null;
160
161 // close all of the connections
162 LogLog.debug("closing client connections");
163 while (oosList.size() != 0) {
164 ObjectOutputStream oos = (ObjectOutputStream)oosList.elementAt(0);
165 if(oos != null) {
166 try {
167 oos.close();
168 }
169 catch(IOException e) {
170 LogLog.error("could not close oos.", e);
171 }
172
173 oosList.removeElementAt(0);
174 }
175 }
176 }
177
178 /**
179 Append an event to all of current connections. */
180 public
181 void append(LoggingEvent event) {
182 // if no event or no open connections, exit now
183 if(event == null || oosList.size() == 0)
184 return;
185
186 // set up location info if requested
187 if (locationInfo) {
188 event.getLocationInformation();
189 }
190
191 // loop through the current set of open connections, appending the event to each
192 for (int streamCount = 0; streamCount < oosList.size(); streamCount++) {
193
194 ObjectOutputStream oos = null;
195 try {
196 oos = (ObjectOutputStream)oosList.elementAt(streamCount);
197 }
198 catch (ArrayIndexOutOfBoundsException e) {
199 // catch this, but just don't assign a value
200 // this should not really occur as this method is
201 // the only one that can remove oos's (besides cleanUp).
202 }
203
204 // list size changed unexpectedly? Just exit the append.
205 if (oos == null)
206 break;
207
208 try {
209 oos.writeObject(event);
210 oos.flush();
211 // Failing to reset the object output stream every now and
212 // then creates a serious memory leak.
213 // right now we always reset. TODO - set up frequency counter per oos?
214 oos.reset();
215 }
216 catch(IOException e) {
217 // there was an io exception so just drop the connection
218 oosList.removeElementAt(streamCount);
219 LogLog.debug("dropped connection");
220
221 // decrement to keep the counter in place (for loop always increments)
222 streamCount--;
223 }
224 }
225 }
226
227 /**
228 The SocketHubAppender does not use a layout. Hence, this method returns
229 <code>false</code>. */
230 public
231 boolean requiresLayout() {
232 return false;
233 }
234
235 /**
236 The <b>Port</b> option takes a positive integer representing
237 the port where the server is waiting for connections. */
238 public
239 void setPort(int _port) {
240 port = _port;
241 }
242
243 /**
244 Returns value of the <b>Port</b> option. */
245 public
246 int getPort() {
247 return port;
248 }
249
250 /**
251 The <b>LocationInfo</b> option takes a boolean value. If true,
252 the information sent to the remote host will include location
253 information. By default no location information is sent to the server. */
254 public
255 void setLocationInfo(boolean _locationInfo) {
256 locationInfo = _locationInfo;
257 }
258
259 /**
260 Returns value of the <b>LocationInfo</b> option. */
261 public
262 boolean getLocationInfo() {
263 return locationInfo;
264 }
265
266 /**
267 Start the ServerMonitor thread. */
268 private
269 void startServer() {
270 serverMonitor = new ServerMonitor(port, oosList);
271 }
272
273 /**
274 This class is used internally to monitor a ServerSocket
275 and register new connections in a vector passed in the
276 constructor. */
277 private
278 class ServerMonitor implements Runnable {
279 private int port;
280 private Vector oosList;
281 private boolean keepRunning;
282 private Thread monitorThread;
283
284 /**
285 Create a thread and start the monitor. */
286 public
287 ServerMonitor(int _port, Vector _oosList) {
288 port = _port;
289 oosList = _oosList;
290 keepRunning = true;
291 monitorThread = new Thread(this);
292 monitorThread.setDaemon(true);
293 monitorThread.start();
294 }
295
296 /**
297 Stops the monitor. This method will not return until
298 the thread has finished executing. */
299 public
300 synchronized
301 void stopMonitor() {
302 if (keepRunning) {
303 LogLog.debug("server monitor thread shutting down");
304 keepRunning = false;
305 try {
306 monitorThread.join();
307 }
308 catch (InterruptedException e) {
309 // do nothing?
310 }
311
312 // release the thread
313 monitorThread = null;
314 LogLog.debug("server monitor thread shut down");
315 }
316 }
317
318 /**
319 Method that runs, monitoring the ServerSocket and adding connections as
320 they connect to the socket. */
321 public
322 void run() {
323 ServerSocket serverSocket = null;
324 try {
325 serverSocket = new ServerSocket(port);
326 serverSocket.setSoTimeout(1000);
327 }
328 catch (Exception e) {
329 LogLog.error("exception setting timeout, shutting down server socket.", e);
330 keepRunning = false;
331 return;
332 }
333
334 try {
335 try {
336 serverSocket.setSoTimeout(1000);
337 }
338 catch (SocketException e) {
339 LogLog.error("exception setting timeout, shutting down server socket.", e);
340 return;
341 }
342
343 while (keepRunning) {
344 Socket socket = null;
345 try {
346 socket = serverSocket.accept();
347 }
348 catch (InterruptedIOException e) {
349 // timeout occurred, so just loop
350 }
351 catch (SocketException e) {
352 LogLog.error("exception accepting socket, shutting down server socket.", e);
353 keepRunning = false;
354 }
355 catch (IOException e) {
356 LogLog.error("exception accepting socket.", e);
357 }
358
359 // if there was a socket accepted
360 if (socket != null) {
361 try {
362 InetAddress remoteAddress = socket.getInetAddress();
363 LogLog.debug("accepting connection from " + remoteAddress.getHostName()
364 + " (" + remoteAddress.getHostAddress() + ")");
365
366 // create an ObjectOutputStream
367 ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
368
369 // add it to the oosList. OK since Vector is synchronized.
370 oosList.addElement(oos);
371 }
372 catch (IOException e) {
373 LogLog.error("exception creating output stream on socket.", e);
374 }
375 }
376 }
377 }
378 finally {
379 // close the socket
380 try {
381 serverSocket.close();
382 }
383 catch (IOException e) {
384 // do nothing with it?
385 }
386 }
387 }
388 }
389 }
390