001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018// Contributors: Dan MacDonald <dan@redknee.com> 019 020package org.apache.log4j.net; 021 022import java.io.IOException; 023import java.io.ObjectOutputStream; 024import java.io.InterruptedIOException; 025import java.net.InetAddress; 026import java.net.Socket; 027 028import org.apache.log4j.AppenderSkeleton; 029import org.apache.log4j.helpers.LogLog; 030import org.apache.log4j.spi.ErrorCode; 031import org.apache.log4j.spi.LoggingEvent; 032 033/** 034 Sends {@link LoggingEvent} objects to a remote a log server, 035 usually a {@link SocketNode}. 036 037 <p>The SocketAppender has the following properties: 038 039 <ul> 040 041 <p><li>If sent to a {@link SocketNode}, remote logging is 042 non-intrusive as far as the log event is concerned. In other 043 words, the event will be logged with the same time stamp, {@link 044 org.apache.log4j.NDC}, location info as if it were logged locally by 045 the client. 046 047 <p><li>SocketAppenders do not use a layout. They ship a 048 serialized {@link LoggingEvent} object to the server side. 049 050 <p><li>Remote logging uses the TCP protocol. Consequently, if 051 the server is reachable, then log events will eventually arrive 052 at the server. 053 054 <p><li>If the remote server is down, the logging requests are 055 simply dropped. However, if and when the server comes back up, 056 then event transmission is resumed transparently. This 057 transparent reconneciton is performed by a <em>connector</em> 058 thread which periodically attempts to connect to the server. 059 060 <p><li>Logging events are automatically <em>buffered</em> by the 061 native TCP implementation. This means that if the link to server 062 is slow but still faster than the rate of (log) event production 063 by the client, the client will not be affected by the slow 064 network connection. However, if the network connection is slower 065 then the rate of event production, then the client can only 066 progress at the network rate. In particular, if the network link 067 to the the server is down, the client will be blocked. 068 069 <p>On the other hand, if the network link is up, but the server 070 is down, the client will not be blocked when making log requests 071 but the log events will be lost due to server unavailability. 072 073 <p><li>Even if a <code>SocketAppender</code> is no longer 074 attached to any category, it will not be garbage collected in 075 the presence of a connector thread. A connector thread exists 076 only if the connection to the server is down. To avoid this 077 garbage collection problem, you should {@link #close} the the 078 <code>SocketAppender</code> explicitly. See also next item. 079 080 <p>Long lived applications which create/destroy many 081 <code>SocketAppender</code> instances should be aware of this 082 garbage collection problem. Most other applications can safely 083 ignore it. 084 085 <p><li>If the JVM hosting the <code>SocketAppender</code> exits 086 before the <code>SocketAppender</code> is closed either 087 explicitly or subsequent to garbage collection, then there might 088 be untransmitted data in the pipe which might be lost. This is a 089 common problem on Windows based systems. 090 091 <p>To avoid lost data, it is usually sufficient to {@link 092 #close} the <code>SocketAppender</code> either explicitly or by 093 calling the {@link org.apache.log4j.LogManager#shutdown} method 094 before exiting the application. 095 096 097 </ul> 098 099 @author Ceki Gülcü 100 @since 0.8.4 */ 101 102public class SocketAppender extends AppenderSkeleton { 103 104 /** 105 The default port number of remote logging server (4560). 106 @since 1.2.15 107 */ 108 static public final int DEFAULT_PORT = 4560; 109 110 /** 111 The default reconnection delay (30000 milliseconds or 30 seconds). 112 */ 113 static final int DEFAULT_RECONNECTION_DELAY = 30000; 114 115 /** 116 We remember host name as String in addition to the resolved 117 InetAddress so that it can be returned via getOption(). 118 */ 119 String remoteHost; 120 121 /** 122 * The MulticastDNS zone advertised by a SocketAppender 123 */ 124 public static final String ZONE = "_log4j_obj_tcpconnect_appender.local."; 125 126 InetAddress address; 127 int port = DEFAULT_PORT; 128 ObjectOutputStream oos; 129 int reconnectionDelay = DEFAULT_RECONNECTION_DELAY; 130 boolean locationInfo = false; 131 private String application; 132 133 private Connector connector; 134 135 int counter = 0; 136 137 // reset the ObjectOutputStream every 70 calls 138 //private static final int RESET_FREQUENCY = 70; 139 private static final int RESET_FREQUENCY = 1; 140 private boolean advertiseViaMulticastDNS; 141 private ZeroConfSupport zeroConf; 142 143 public SocketAppender() { 144 } 145 146 /** 147 Connects to remote server at <code>address</code> and <code>port</code>. 148 */ 149 public SocketAppender(InetAddress address, int port) { 150 this.address = address; 151 this.remoteHost = address.getHostName(); 152 this.port = port; 153 connect(address, port); 154 } 155 156 /** 157 Connects to remote server at <code>host</code> and <code>port</code>. 158 */ 159 public SocketAppender(String host, int port) { 160 this.port = port; 161 this.address = getAddressByName(host); 162 this.remoteHost = host; 163 connect(address, port); 164 } 165 166 /** 167 Connect to the specified <b>RemoteHost</b> and <b>Port</b>. 168 */ 169 public void activateOptions() { 170 if (advertiseViaMulticastDNS) { 171 zeroConf = new ZeroConfSupport(ZONE, port, getName()); 172 zeroConf.advertise(); 173 } 174 connect(address, port); 175 } 176 177 /** 178 * Close this appender. 179 * 180 * <p>This will mark the appender as closed and call then {@link 181 * #cleanUp} method. 182 * */ 183 synchronized public void close() { 184 if(closed) 185 return; 186 187 this.closed = true; 188 if (advertiseViaMulticastDNS) { 189 zeroConf.unadvertise(); 190 } 191 192 cleanUp(); 193 } 194 195 /** 196 * Drop the connection to the remote host and release the underlying 197 * connector thread if it has been created 198 * */ 199 public void cleanUp() { 200 if(oos != null) { 201 try { 202 oos.close(); 203 } catch(IOException e) { 204 if (e instanceof InterruptedIOException) { 205 Thread.currentThread().interrupt(); 206 } 207 LogLog.error("Could not close oos.", e); 208 } 209 oos = null; 210 } 211 if(connector != null) { 212 //LogLog.debug("Interrupting the connector."); 213 connector.interrupted = true; 214 connector = null; // allow gc 215 } 216 } 217 218 void connect(InetAddress address, int port) { 219 if(this.address == null) 220 return; 221 try { 222 // First, close the previous connection if any. 223 cleanUp(); 224 oos = new ObjectOutputStream(new Socket(address, port).getOutputStream()); 225 } catch(IOException e) { 226 if (e instanceof InterruptedIOException) { 227 Thread.currentThread().interrupt(); 228 } 229 String msg = "Could not connect to remote log4j server at [" 230 +address.getHostName()+"]."; 231 if(reconnectionDelay > 0) { 232 msg += " We will try again later."; 233 fireConnector(); // fire the connector thread 234 } else { 235 msg += " We are not retrying."; 236 errorHandler.error(msg, e, ErrorCode.GENERIC_FAILURE); 237 } 238 LogLog.error(msg); 239 } 240 } 241 242 243 public void append(LoggingEvent event) { 244 if(event == null) 245 return; 246 247 if(address==null) { 248 errorHandler.error("No remote host is set for SocketAppender named \""+ 249 this.name+"\"."); 250 return; 251 } 252 253 if(oos != null) { 254 try { 255 256 if(locationInfo) { 257 event.getLocationInformation(); 258 } 259 if (application != null) { 260 event.setProperty("application", application); 261 } 262 event.getNDC(); 263 event.getThreadName(); 264 event.getMDCCopy(); 265 event.getRenderedMessage(); 266 event.getThrowableStrRep(); 267 268 oos.writeObject(event); 269 //LogLog.debug("=========Flushing."); 270 oos.flush(); 271 if(++counter >= RESET_FREQUENCY) { 272 counter = 0; 273 // Failing to reset the object output stream every now and 274 // then creates a serious memory leak. 275 //System.err.println("Doing oos.reset()"); 276 oos.reset(); 277 } 278 } catch(IOException e) { 279 if (e instanceof InterruptedIOException) { 280 Thread.currentThread().interrupt(); 281 } 282 oos = null; 283 LogLog.warn("Detected problem with connection: "+e); 284 if(reconnectionDelay > 0) { 285 fireConnector(); 286 } else { 287 errorHandler.error("Detected problem with connection, not reconnecting.", e, 288 ErrorCode.GENERIC_FAILURE); 289 } 290 } 291 } 292 } 293 294 public void setAdvertiseViaMulticastDNS(boolean advertiseViaMulticastDNS) { 295 this.advertiseViaMulticastDNS = advertiseViaMulticastDNS; 296 } 297 298 public boolean isAdvertiseViaMulticastDNS() { 299 return advertiseViaMulticastDNS; 300 } 301 302 void fireConnector() { 303 if(connector == null) { 304 LogLog.debug("Starting a new connector thread."); 305 connector = new Connector(); 306 connector.setDaemon(true); 307 connector.setPriority(Thread.MIN_PRIORITY); 308 connector.start(); 309 } 310 } 311 312 static 313 InetAddress getAddressByName(String host) { 314 try { 315 return InetAddress.getByName(host); 316 } catch(Exception e) { 317 if (e instanceof InterruptedIOException || e instanceof InterruptedException) { 318 Thread.currentThread().interrupt(); 319 } 320 LogLog.error("Could not find address of ["+host+"].", e); 321 return null; 322 } 323 } 324 325 /** 326 * The SocketAppender does not use a layout. Hence, this method 327 * returns <code>false</code>. 328 * */ 329 public boolean requiresLayout() { 330 return false; 331 } 332 333 /** 334 * The <b>RemoteHost</b> option takes a string value which should be 335 * the host name of the server where a {@link SocketNode} is 336 * running. 337 * */ 338 public void setRemoteHost(String host) { 339 address = getAddressByName(host); 340 remoteHost = host; 341 } 342 343 /** 344 Returns value of the <b>RemoteHost</b> option. 345 */ 346 public String getRemoteHost() { 347 return remoteHost; 348 } 349 350 /** 351 The <b>Port</b> option takes a positive integer representing 352 the port where the server is waiting for connections. 353 */ 354 public void setPort(int port) { 355 this.port = port; 356 } 357 358 /** 359 Returns value of the <b>Port</b> option. 360 */ 361 public int getPort() { 362 return port; 363 } 364 365 /** 366 The <b>LocationInfo</b> option takes a boolean value. If true, 367 the information sent to the remote host will include location 368 information. By default no location information is sent to the server. 369 */ 370 public void setLocationInfo(boolean locationInfo) { 371 this.locationInfo = locationInfo; 372 } 373 374 /** 375 Returns value of the <b>LocationInfo</b> option. 376 */ 377 public boolean getLocationInfo() { 378 return locationInfo; 379 } 380 381 /** 382 * The <b>App</b> option takes a string value which should be the name of the 383 * application getting logged. 384 * If property was already set (via system property), don't set here. 385 * @since 1.2.15 386 */ 387 public void setApplication(String lapp) { 388 this.application = lapp; 389 } 390 391 /** 392 * Returns value of the <b>Application</b> option. 393 * @since 1.2.15 394 */ 395 public String getApplication() { 396 return application; 397 } 398 399 /** 400 The <b>ReconnectionDelay</b> option takes a positive integer 401 representing the number of milliseconds to wait between each 402 failed connection attempt to the server. The default value of 403 this option is 30000 which corresponds to 30 seconds. 404 405 <p>Setting this option to zero turns off reconnection 406 capability. 407 */ 408 public void setReconnectionDelay(int delay) { 409 this.reconnectionDelay = delay; 410 } 411 412 /** 413 Returns value of the <b>ReconnectionDelay</b> option. 414 */ 415 public int getReconnectionDelay() { 416 return reconnectionDelay; 417 } 418 419 /** 420 The Connector will reconnect when the server becomes available 421 again. It does this by attempting to open a new connection every 422 <code>reconnectionDelay</code> milliseconds. 423 424 <p>It stops trying whenever a connection is established. It will 425 restart to try reconnect to the server when previously open 426 connection is droppped. 427 428 @author Ceki Gülcü 429 @since 0.8.4 430 */ 431 class Connector extends Thread { 432 433 boolean interrupted = false; 434 435 public 436 void run() { 437 Socket socket; 438 while(!interrupted) { 439 try { 440 sleep(reconnectionDelay); 441 LogLog.debug("Attempting connection to "+address.getHostName()); 442 socket = new Socket(address, port); 443 synchronized(this) { 444 oos = new ObjectOutputStream(socket.getOutputStream()); 445 connector = null; 446 LogLog.debug("Connection established. Exiting connector thread."); 447 break; 448 } 449 } catch(InterruptedException e) { 450 LogLog.debug("Connector interrupted. Leaving loop."); 451 return; 452 } catch(java.net.ConnectException e) { 453 LogLog.debug("Remote host "+address.getHostName() 454 +" refused connection."); 455 } catch(IOException e) { 456 if (e instanceof InterruptedIOException) { 457 Thread.currentThread().interrupt(); 458 } 459 LogLog.debug("Could not connect to " + address.getHostName()+ 460 ". Exception is " + e); 461 } 462 } 463 //LogLog.debug("Exiting Connector.run() method."); 464 } 465 466 /** 467 public 468 void finalize() { 469 LogLog.debug("Connector finalize() has been called."); 470 } 471 */ 472 } 473 474}