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// Contibutors:  Aaron Greenhouse <aarong@cs.cmu.edu>
019//               Thomas Tuft Muller <ttm@online.no>
020package org.apache.log4j;
021
022import java.text.MessageFormat;
023import java.util.ArrayList;
024import java.util.Enumeration;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029
030import org.apache.log4j.helpers.AppenderAttachableImpl;
031import org.apache.log4j.spi.AppenderAttachable;
032import org.apache.log4j.spi.LoggingEvent;
033
034
035/**
036 * The AsyncAppender lets users log events asynchronously.
037 * <p/>
038 * <p/>
039 * The AsyncAppender will collect the events sent to it and then dispatch them
040 * to all the appenders that are attached to it. You can attach multiple
041 * appenders to an AsyncAppender.
042 * </p>
043 * <p/>
044 * <p/>
045 * The AsyncAppender uses a separate thread to serve the events in its buffer.
046 * </p>
047 * <p/>
048 * <b>Important note:</b> The <code>AsyncAppender</code> can only be script
049 * configured using the {@link org.apache.log4j.xml.DOMConfigurator}.
050 * </p>
051 *
052 * @author Ceki G&uuml;lc&uuml;
053 * @author Curt Arnold
054 * @since 0.9.1
055 */
056public class AsyncAppender extends AppenderSkeleton
057  implements AppenderAttachable {
058  /**
059   * The default buffer size is set to 128 events.
060   */
061  public static final int DEFAULT_BUFFER_SIZE = 128;
062
063  /**
064   * Event buffer, also used as monitor to protect itself and
065   * discardMap from simulatenous modifications.
066   */
067  private final List buffer = new ArrayList();
068
069  /**
070   * Map of DiscardSummary objects keyed by logger name.
071   */
072  private final Map discardMap = new HashMap();
073
074  /**
075   * Buffer size.
076   */
077  private int bufferSize = DEFAULT_BUFFER_SIZE;
078
079  /** Nested appenders. */
080  AppenderAttachableImpl aai;
081
082  /**
083   * Nested appenders.
084   */
085  private final AppenderAttachableImpl appenders;
086
087  /**
088   * Dispatcher.
089   */
090  private final Thread dispatcher;
091
092  /**
093   * Should location info be included in dispatched messages.
094   */
095  private boolean locationInfo = false;
096
097  /**
098   * Does appender block when buffer is full.
099   */
100  private boolean blocking = true;
101
102  /**
103   * Create new instance.
104   */
105  public AsyncAppender() {
106    appenders = new AppenderAttachableImpl();
107
108    //
109    //   only set for compatibility
110    aai = appenders;
111
112    dispatcher =
113      new Thread(new Dispatcher(this, buffer, discardMap, appenders));
114
115    // It is the user's responsibility to close appenders before
116    // exiting.
117    dispatcher.setDaemon(true);
118
119    // set the dispatcher priority to lowest possible value
120    //        dispatcher.setPriority(Thread.MIN_PRIORITY);
121    dispatcher.setName("AsyncAppender-Dispatcher-" + dispatcher.getName());
122    dispatcher.start();
123  }
124
125  /**
126   * Add appender.
127   *
128   * @param newAppender appender to add, may not be null.
129   */
130  public void addAppender(final Appender newAppender) {
131    synchronized (appenders) {
132      appenders.addAppender(newAppender);
133    }
134  }
135
136  /**
137   * {@inheritDoc}
138   */
139  public void append(final LoggingEvent event) {
140    //
141    //   if dispatcher thread has died then
142    //      append subsequent events synchronously
143    //   See bug 23021
144    if ((dispatcher == null) || !dispatcher.isAlive() || (bufferSize <= 0)) {
145      synchronized (appenders) {
146        appenders.appendLoopOnAppenders(event);
147      }
148
149      return;
150    }
151
152    // Set the NDC and thread name for the calling thread as these
153    // LoggingEvent fields were not set at event creation time.
154    event.getNDC();
155    event.getThreadName();
156    // Get a copy of this thread's MDC.
157    event.getMDCCopy();
158    if (locationInfo) {
159      event.getLocationInformation();
160    }
161    event.getRenderedMessage();
162    event.getThrowableStrRep();
163
164    synchronized (buffer) {
165      while (true) {
166        int previousSize = buffer.size();
167
168        if (previousSize < bufferSize) {
169          buffer.add(event);
170
171          //
172          //   if buffer had been empty
173          //       signal all threads waiting on buffer
174          //       to check their conditions.
175          //
176          if (previousSize == 0) {
177            buffer.notifyAll();
178          }
179
180          break;
181        }
182
183        //
184        //   Following code is only reachable if buffer is full
185        //
186        //
187        //   if blocking and thread is not already interrupted
188        //      and not the dispatcher then
189        //      wait for a buffer notification
190        boolean discard = true;
191        if (blocking
192                && !Thread.interrupted()
193                && Thread.currentThread() != dispatcher) {
194          try {
195            buffer.wait();
196            discard = false;
197          } catch (InterruptedException e) {
198            //
199            //  reset interrupt status so
200            //    calling code can see interrupt on
201            //    their next wait or sleep.
202            Thread.currentThread().interrupt();
203          }
204        }
205
206        //
207        //   if blocking is false or thread has been interrupted
208        //   add event to discard map.
209        //
210        if (discard) {
211          String loggerName = event.getLoggerName();
212          DiscardSummary summary = (DiscardSummary) discardMap.get(loggerName);
213
214          if (summary == null) {
215            summary = new DiscardSummary(event);
216            discardMap.put(loggerName, summary);
217          } else {
218            summary.add(event);
219          }
220
221          break;
222        }
223      }
224    }
225  }
226
227  /**
228   * Close this <code>AsyncAppender</code> by interrupting the dispatcher
229   * thread which will process all pending events before exiting.
230   */
231  public void close() {
232    /**
233     * Set closed flag and notify all threads to check their conditions.
234     * Should result in dispatcher terminating.
235     */
236    synchronized (buffer) {
237      closed = true;
238      buffer.notifyAll();
239    }
240
241    try {
242      dispatcher.join();
243    } catch (InterruptedException e) {
244      Thread.currentThread().interrupt();
245      org.apache.log4j.helpers.LogLog.error(
246        "Got an InterruptedException while waiting for the "
247        + "dispatcher to finish.", e);
248    }
249
250    //
251    //    close all attached appenders.
252    //
253    synchronized (appenders) {
254      Enumeration iter = appenders.getAllAppenders();
255
256      if (iter != null) {
257        while (iter.hasMoreElements()) {
258          Object next = iter.nextElement();
259
260          if (next instanceof Appender) {
261            ((Appender) next).close();
262          }
263        }
264      }
265    }
266  }
267
268  /**
269   * Get iterator over attached appenders.
270   * @return iterator or null if no attached appenders.
271   */
272  public Enumeration getAllAppenders() {
273    synchronized (appenders) {
274      return appenders.getAllAppenders();
275    }
276  }
277
278  /**
279   * Get appender by name.
280   *
281   * @param name name, may not be null.
282   * @return matching appender or null.
283   */
284  public Appender getAppender(final String name) {
285    synchronized (appenders) {
286      return appenders.getAppender(name);
287    }
288  }
289
290  /**
291   * Gets whether the location of the logging request call
292   * should be captured.
293   *
294   * @return the current value of the <b>LocationInfo</b> option.
295   */
296  public boolean getLocationInfo() {
297    return locationInfo;
298  }
299
300  /**
301   * Determines if specified appender is attached.
302   * @param appender appender.
303   * @return true if attached.
304   */
305  public boolean isAttached(final Appender appender) {
306    synchronized (appenders) {
307      return appenders.isAttached(appender);
308    }
309  }
310
311  /**
312   * {@inheritDoc}
313   */
314  public boolean requiresLayout() {
315    return false;
316  }
317
318  /**
319   * Removes and closes all attached appenders.
320   */
321  public void removeAllAppenders() {
322    synchronized (appenders) {
323      appenders.removeAllAppenders();
324    }
325  }
326
327  /**
328   * Removes an appender.
329   * @param appender appender to remove.
330   */
331  public void removeAppender(final Appender appender) {
332    synchronized (appenders) {
333      appenders.removeAppender(appender);
334    }
335  }
336
337  /**
338   * Remove appender by name.
339   * @param name name.
340   */
341  public void removeAppender(final String name) {
342    synchronized (appenders) {
343      appenders.removeAppender(name);
344    }
345  }
346
347  /**
348   * The <b>LocationInfo</b> option takes a boolean value. By default, it is
349   * set to false which means there will be no effort to extract the location
350   * information related to the event. As a result, the event that will be
351   * ultimately logged will likely to contain the wrong location information
352   * (if present in the log format).
353   * <p/>
354   * <p/>
355   * Location information extraction is comparatively very slow and should be
356   * avoided unless performance is not a concern.
357   * </p>
358   * @param flag true if location information should be extracted.
359   */
360  public void setLocationInfo(final boolean flag) {
361    locationInfo = flag;
362  }
363
364  /**
365   * Sets the number of messages allowed in the event buffer
366   * before the calling thread is blocked (if blocking is true)
367   * or until messages are summarized and discarded.  Changing
368   * the size will not affect messages already in the buffer.
369   *
370   * @param size buffer size, must be positive.
371   */
372  public void setBufferSize(final int size) {
373    //
374    //   log4j 1.2 would throw exception if size was negative
375    //      and deadlock if size was zero.
376    //
377    if (size < 0) {
378      throw new java.lang.NegativeArraySizeException("size");
379    }
380
381    synchronized (buffer) {
382      //
383      //   don't let size be zero.
384      //
385      bufferSize = (size < 1) ? 1 : size;
386      buffer.notifyAll();
387    }
388  }
389
390  /**
391   * Gets the current buffer size.
392   * @return the current value of the <b>BufferSize</b> option.
393   */
394  public int getBufferSize() {
395    return bufferSize;
396  }
397
398  /**
399   * Sets whether appender should wait if there is no
400   * space available in the event buffer or immediately return.
401   *
402   * @since 1.2.14
403   * @param value true if appender should wait until available space in buffer.
404   */
405  public void setBlocking(final boolean value) {
406    synchronized (buffer) {
407      blocking = value;
408      buffer.notifyAll();
409    }
410  }
411
412  /**
413   * Gets whether appender should block calling thread when buffer is full.
414   * If false, messages will be counted by logger and a summary
415   * message appended after the contents of the buffer have been appended.
416   *
417   * @since 1.2.14
418   * @return true if calling thread will be blocked when buffer is full.
419   */
420  public boolean getBlocking() {
421    return blocking;
422  }
423
424  /**
425   * Summary of discarded logging events for a logger.
426   */
427  private static final class DiscardSummary {
428    /**
429     * First event of the highest severity.
430     */
431    private LoggingEvent maxEvent;
432
433    /**
434     * Total count of messages discarded.
435     */
436    private int count;
437
438    /**
439     * Create new instance.
440     *
441     * @param event event, may not be null.
442     */
443    public DiscardSummary(final LoggingEvent event) {
444      maxEvent = event;
445      count = 1;
446    }
447
448    /**
449     * Add discarded event to summary.
450     *
451     * @param event event, may not be null.
452     */
453    public void add(final LoggingEvent event) {
454      if (event.getLevel().toInt() > maxEvent.getLevel().toInt()) {
455        maxEvent = event;
456      }
457
458      count++;
459    }
460
461    /**
462     * Create event with summary information.
463     *
464     * @return new event.
465     */
466    public LoggingEvent createEvent() {
467      String msg =
468        MessageFormat.format(
469          "Discarded {0} messages due to full event buffer including: {1}",
470          new Object[] { new Integer(count), maxEvent.getMessage() });
471
472      return new LoggingEvent(
473              "org.apache.log4j.AsyncAppender.DONT_REPORT_LOCATION",
474              Logger.getLogger(maxEvent.getLoggerName()),
475              maxEvent.getLevel(),
476              msg,
477              null);
478    }
479  }
480
481  /**
482   * Event dispatcher.
483   */
484  private static class Dispatcher implements Runnable {
485    /**
486     * Parent AsyncAppender.
487     */
488    private final AsyncAppender parent;
489
490    /**
491     * Event buffer.
492     */
493    private final List buffer;
494
495    /**
496     * Map of DiscardSummary keyed by logger name.
497     */
498    private final Map discardMap;
499
500    /**
501     * Wrapped appenders.
502     */
503    private final AppenderAttachableImpl appenders;
504
505    /**
506     * Create new instance of dispatcher.
507     *
508     * @param parent     parent AsyncAppender, may not be null.
509     * @param buffer     event buffer, may not be null.
510     * @param discardMap discard map, may not be null.
511     * @param appenders  appenders, may not be null.
512     */
513    public Dispatcher(
514      final AsyncAppender parent, final List buffer, final Map discardMap,
515      final AppenderAttachableImpl appenders) {
516
517      this.parent = parent;
518      this.buffer = buffer;
519      this.appenders = appenders;
520      this.discardMap = discardMap;
521    }
522
523    /**
524     * {@inheritDoc}
525     */
526    public void run() {
527      boolean isActive = true;
528
529      //
530      //   if interrupted (unlikely), end thread
531      //
532      try {
533        //
534        //   loop until the AsyncAppender is closed.
535        //
536        while (isActive) {
537          LoggingEvent[] events = null;
538
539          //
540          //   extract pending events while synchronized
541          //       on buffer
542          //
543          synchronized (buffer) {
544            int bufferSize = buffer.size();
545            isActive = !parent.closed;
546
547            while ((bufferSize == 0) && isActive) {
548              buffer.wait();
549              bufferSize = buffer.size();
550              isActive = !parent.closed;
551            }
552
553            if (bufferSize > 0) {
554              events = new LoggingEvent[bufferSize + discardMap.size()];
555              buffer.toArray(events);
556
557              //
558              //   add events due to buffer overflow
559              //
560              int index = bufferSize;
561
562              for (
563                Iterator iter = discardMap.values().iterator();
564                  iter.hasNext();) {
565                events[index++] = ((DiscardSummary) iter.next()).createEvent();
566              }
567
568              //
569              //    clear buffer and discard map
570              //
571              buffer.clear();
572              discardMap.clear();
573
574              //
575              //    allow blocked appends to continue
576              buffer.notifyAll();
577            }
578          }
579
580          //
581          //   process events after lock on buffer is released.
582          //
583          if (events != null) {
584            for (int i = 0; i < events.length; i++) {
585              synchronized (appenders) {
586                appenders.appendLoopOnAppenders(events[i]);
587              }
588            }
589          }
590        }
591      } catch (InterruptedException ex) {
592        Thread.currentThread().interrupt();
593      }
594    }
595  }
596}