1
2
3
4
5
6
7
8
9
10
11 package net.sf.jour.log4j;
12
13 import java.net.InetAddress;
14 import java.net.Socket;
15 import java.io.IOException;
16 import java.io.ObjectOutputStream;
17
18 import org.apache.log4j.spi.LoggingEvent;
19 import org.apache.log4j.AppenderSkeleton;
20 import org.apache.log4j.Logger;
21
22 /***
23 Sends {@link LoggingEvent} objects to a remote a log server,
24 usually a {@link SocketNode}.
25
26 <p>The SocketAppender has the following properties:
27
28 <ul>
29
30 <p><li>If sent to a {@link SocketNode}, remote logging is
31 non-intrusive as far as the log event is concerned. In other
32 words, the event will be logged with the same time stamp, {@link
33 org.apache.log4j.NDC}, location info as if it were logged locally by
34 the client.
35
36 <p><li>SocketAppenders do not use a layout. They ship a
37 serialized {@link LoggingEvent} object to the server side.
38
39 <p><li>Remote logging uses the TCP protocol. Consequently, if
40 the server is reachable, then log events will eventually arrive
41 at the server.
42
43 <p><li>If the remote server is down, the logging requests are
44 simply dropped. However, if and when the server comes back up,
45 then event transmission is resumed transparently. This
46 transparent reconneciton is performed by a <em>connector</em>
47 thread which periodically attempts to connect to the server.
48
49 <p><li>Logging events are automatically <em>buffered</em> by the
50 native TCP implementation. This means that if the link to server
51 is slow but still faster than the rate of (log) event production
52 by the client, the client will not be affected by the slow
53 network connection. However, if the network connection is slower
54 then the rate of event production, then the client can only
55 progress at the network rate. In particular, if the network link
56 to the the server is down, the client will be blocked.
57
58 <p>On the other hand, if the network link is up, but the server
59 is down, the client will not be blocked when making log requests
60 but the log events will be lost due to server unavailability.
61
62 <p><li>Even if a <code>SocketAppender</code> is no longer
63 attached to any category, it will not be garbage collected in
64 the presence of a connector thread. A connector thread exists
65 only if the connection to the server is down. To avoid this
66 garbage collection problem, you should {@link #close} the the
67 <code>SocketAppender</code> explicitly. See also next item.
68
69 <p>Long lived applications which create/destroy many
70 <code>SocketAppender</code> instances should be aware of this
71 garbage collection problem. Most other applications can safely
72 ignore it.
73
74 <p><li>If the JVM hosting the <code>SocketAppender</code> exits
75 before the <code>SocketAppender</code> is closed either
76 explicitly or subsequent to garbage collection, then there might
77 be untransmitted data in the pipe which might be lost. This is a
78 common problem on Windows based systems.
79
80 <p>To avoid lost data, it is usually sufficient to {@link
81 #close} the <code>SocketAppender</code> either explicitly or by
82 calling the {@link org.apache.log4j.LogManager#shutdown} method
83 before exiting the application.
84
85
86 </ul>
87
88 @author Ceki Gülcü
89 @since 0.8.4 */
90
91 public class ProfilerSocketAppender extends AppenderSkeleton {
92
93 /***
94 The default port number of remote logging server (4560).
95 */
96 static final int DEFAULT_PORT = 4560;
97
98 static Logger logLog = Logger.getLogger(ProfilerSocketAppender.class);
99
100 /***
101 The default reconnection delay (30000 milliseconds or 30 seconds).
102 */
103 static final int DEFAULT_RECONNECTION_DELAY = 30000;
104
105 /***
106 We remember host name as String in addition to the resolved
107 InetAddress so that it can be returned via getOption().
108 */
109 String remoteHost;
110
111 InetAddress address;
112 int port = DEFAULT_PORT;
113 ObjectOutputStream oos;
114 int reconnectionDelay = DEFAULT_RECONNECTION_DELAY;
115 boolean locationInfo = false;
116 int flushFrequency = 1;
117
118 private Connector connector;
119
120 int useLoggingEvent = 0;
121
122 int counter = 0;
123 int fcounter = 0;
124
125
126
127
128
129 private int RESET_FREQUENCY = 1;
130
131 public ProfilerSocketAppender() {
132 }
133
134 /***
135 Connects to remote server at <code>address</code> and <code>port</code>.
136 */
137 public ProfilerSocketAppender(InetAddress address, int port) {
138 this.address = address;
139 this.remoteHost = address.getHostName();
140 this.port = port;
141 connect(address, port);
142 }
143
144 /***
145 Connects to remote server at <code>host</code> and <code>port</code>.
146 */
147 public ProfilerSocketAppender(String host, int port) {
148 this.port = port;
149 this.address = getAddressByName(host);
150 this.remoteHost = host;
151 connect(address, port);
152 }
153
154 /***
155 Connect to the specified <b>RemoteHost</b> and <b>Port</b>.
156 */
157 public void activateOptions() {
158 connect(address, port);
159 }
160
161 /***
162 * Close this appender.
163 *
164 * <p>This will mark the appender as closed and call then {@link
165 * #cleanUp} method.
166 * */
167 synchronized public void close() {
168 if(closed)
169 return;
170
171 this.closed = true;
172 cleanUp();
173 }
174
175 /***
176 * Drop the connection to the remote host and release the underlying
177 * connector thread if it has been created
178 * */
179 public void cleanUp() {
180 if(oos != null) {
181 try {
182 oos.close();
183 } catch(IOException e) {
184 logLog.error("Could not close oos.", e);
185 }
186 oos = null;
187 }
188 if(connector != null) {
189
190 connector.interrupted = true;
191 connector = null;
192 }
193 }
194
195 void connect(InetAddress address, int port) {
196 if(this.address == null)
197 return;
198 try {
199
200 cleanUp();
201 oos = new ObjectOutputStream(new Socket(address, port).getOutputStream());
202 } catch(IOException e) {
203
204 String msg = "Could not connect to remote log4j server at ["
205 +address.getHostName()+"].";
206 if(reconnectionDelay > 0) {
207 msg += " We will try again later.";
208 fireConnector();
209 }
210 logLog.error(msg, e);
211 }
212 }
213
214
215 public void append(LoggingEvent event) {
216 if (event == null)
217 return;
218
219 if (address == null) {
220 errorHandler.error("No remote host is set for SocketAppender named \"" + this.name + "\".");
221 return;
222 }
223
224 if (oos != null) {
225 try {
226 if (locationInfo) {
227 event.getLocationInformation();
228 }
229
230 Object o = getObject(event);
231 if (o == null) {
232 return;
233 }
234 oos.writeObject(o);
235
236
237 if (++fcounter >= RESET_FREQUENCY) {
238 oos.flush();
239 fcounter = 0;
240 }
241 if (++counter >= RESET_FREQUENCY) {
242 counter = 0;
243
244
245
246 oos.reset();
247 }
248 } catch (IOException e) {
249 oos = null;
250 logLog.warn("Detected problem with connection: " + e);
251 if (reconnectionDelay > 0) {
252 fireConnector();
253 }
254 }
255 }
256 }
257
258 protected Object getObject(LoggingEvent event) {
259 switch (useLoggingEvent) {
260 case 0:
261 return event;
262 case 1:
263 return ProfilerEvent.extract(event);
264 case 2:
265 return ProfilerEventExt.extract(event);
266 default:
267 errorHandler.error("Wrong useLoggingEvent" + useLoggingEvent);
268 return null;
269 }
270 }
271
272 void fireConnector() {
273 if(connector == null) {
274 logLog.debug("Starting a new connector thread.");
275 connector = new Connector();
276 connector.setDaemon(true);
277 connector.setPriority(Thread.MIN_PRIORITY);
278 connector.start();
279 }
280 }
281
282 static
283 InetAddress getAddressByName(String hostName) {
284 try {
285
286 String host = hostName;
287 if (hostName.startsWith("$")) {
288 host = System.getProperty(hostName.substring(1));
289 }
290 return InetAddress.getByName(host);
291 } catch(Exception e) {
292 logLog.error("Could not find address of ["+hostName+"].", e);
293 return null;
294 }
295 }
296
297 /***
298 * The SocketAppender does not use a layout. Hence, this method
299 * returns <code>false</code>.
300 * */
301 public boolean requiresLayout() {
302 return false;
303 }
304
305 /***
306 * The <b>RemoteHost</b> option takes a string value which should be
307 * the host name of the server where a {@link SocketNode} is
308 * running.
309 * */
310 public void setRemoteHost(String host) {
311 address = getAddressByName(host);
312 remoteHost = host;
313 }
314
315 /***
316 Returns value of the <b>RemoteHost</b> option.
317 */
318 public String getRemoteHost() {
319 return remoteHost;
320 }
321
322 /***
323 The <b>Port</b> option takes a positive integer representing
324 the port where the server is waiting for connections.
325 */
326 public void setPort(int port) {
327 this.port = port;
328 }
329
330 /***
331 Returns value of the <b>Port</b> option.
332 */
333 public int getPort() {
334 return port;
335 }
336
337 /***
338 The <b>LocationInfo</b> option takes a boolean value. If true,
339 the information sent to the remote host will include location
340 information. By default no location information is sent to the server.
341 */
342 public void setLocationInfo(boolean locationInfo) {
343 this.locationInfo = locationInfo;
344 }
345
346 /***
347 Returns value of the <b>LocationInfo</b> option.
348 */
349 public boolean getLocationInfo() {
350 return locationInfo;
351 }
352
353 /***
354 The <b>ReconnectionDelay</b> option takes a positive integer
355 representing the number of milliseconds to wait between each
356 failed connection attempt to the server. The default value of
357 this option is 30000 which corresponds to 30 seconds.
358
359 <p>Setting this option to zero turns off reconnection
360 capability.
361 */
362 public void setReconnectionDelay(int delay) {
363 this.reconnectionDelay = delay;
364 }
365
366 /***
367 Returns value of the <b>ReconnectionDelay</b> option.
368 */
369 public int getReconnectionDelay() {
370 return this.reconnectionDelay;
371 }
372
373 public void setFlushFrequency(int flushFrequency) {
374 this.flushFrequency = flushFrequency;
375 }
376
377 /***
378 Returns value of the <b>ReconnectionDelay</b> option.
379 */
380 public int getFlushFrequency() {
381 return this.flushFrequency;
382 }
383
384 public void setResetFrequency(int resetFrequency) {
385 this.RESET_FREQUENCY = resetFrequency;
386 }
387
388 /***
389 Returns value of the <b>ReconnectionDelay</b> option.
390 */
391 public int getResetFrequency() {
392 return this.RESET_FREQUENCY;
393 }
394
395 /***
396 * @return Returns the useLoggingEvent.
397 */
398 public int setUseLoggingEvent() {
399 return this.useLoggingEvent;
400 }
401
402 /***
403 * @param useLoggingEvent The useLoggingEvent to set.
404 */
405 public void setUseLoggingEvent(int useLoggingEvent) {
406 this.useLoggingEvent = useLoggingEvent;
407 }
408
409 /***
410 The Connector will reconnect when the server becomes available
411 again. It does this by attempting to open a new connection every
412 <code>reconnectionDelay</code> milliseconds.
413
414 <p>It stops trying whenever a connection is established. It will
415 restart to try reconnect to the server when previpously open
416 connection is droppped.
417
418 @author Ceki Gülcü
419 @since 0.8.4
420 */
421 class Connector extends Thread {
422
423 boolean interrupted = false;
424
425 public
426 void run() {
427 Socket socket;
428 while(!interrupted) {
429 try {
430 sleep(reconnectionDelay);
431 logLog.debug("Attempting connection to "+address.getHostName());
432 socket = new Socket(address, port);
433 synchronized(this) {
434 oos = new ObjectOutputStream(socket.getOutputStream());
435 connector = null;
436 logLog.debug("Connection established. Exiting connector thread.");
437 break;
438 }
439 } catch(InterruptedException e) {
440 logLog.debug("Connector interrupted. Leaving loop.");
441 return;
442 } catch(java.net.ConnectException e) {
443 logLog.debug("Remote host "+address.getHostName()
444 +" refused connection.");
445 } catch(IOException e) {
446 logLog.debug("Could not connect to " + address.getHostName()+
447 ". Exception is " + e);
448 }
449 }
450
451 }
452
453 /***
454 public
455 void finalize() {
456 LogLog.debug("Connector finalize() has been called.");
457 }
458 */
459 }
460
461 }