/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Contibutors: Aaron Greenhouse // Thomas Tuft Muller package org.apache.log4j; import org.apache.log4j.helpers.AppenderAttachableImpl; import org.apache.log4j.spi.AppenderAttachable; import org.apache.log4j.spi.LoggingEvent; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * The AsyncAppender lets users log events asynchronously. *

*

* The AsyncAppender will collect the events sent to it and then dispatch them * to all the appenders that are attached to it. You can attach multiple * appenders to an AsyncAppender. *

*

*

* The AsyncAppender uses a separate thread to serve the events in its buffer. *

*

* Important note: The AsyncAppender can only be script * configured using the {@link org.apache.log4j.xml.DOMConfigurator}. *

* * @author Ceki Gülcü * @author Curt Arnold * @since 0.9.1 */ public class AsyncAppender extends AppenderSkeleton implements AppenderAttachable { /** * The default buffer size is set to 128 events. */ public static final int DEFAULT_BUFFER_SIZE = 128; /** * Event buffer, also used as monitor to protect itself and * discardMap from simulatenous modifications. */ private final List buffer = new ArrayList(); /** * Map of DiscardSummary objects keyed by logger name. */ private final Map discardMap = new HashMap(); /** * Buffer size. */ private int bufferSize = DEFAULT_BUFFER_SIZE; /** Nested appenders. */ AppenderAttachableImpl aai; /** * Nested appenders. */ private final AppenderAttachableImpl appenders; /** * Dispatcher. */ private final Thread dispatcher; /** * Should location info be included in dispatched messages. */ private boolean locationInfo = false; /** * Does appender block when buffer is full. */ private boolean blocking = true; /** * Create new instance. */ public AsyncAppender() { appenders = new AppenderAttachableImpl(); // // only set for compatibility aai = appenders; dispatcher = new Thread(new Dispatcher(this, buffer, discardMap, appenders)); // It is the user's responsibility to close appenders before // exiting. dispatcher.setDaemon(true); // set the dispatcher priority to lowest possible value // dispatcher.setPriority(Thread.MIN_PRIORITY); dispatcher.setName("Dispatcher-" + dispatcher.getName()); dispatcher.start(); } /** * Add appender. * * @param newAppender appender to add, may not be null. */ public void addAppender(final Appender newAppender) { synchronized (appenders) { appenders.addAppender(newAppender); } } /** * {@inheritDoc} */ public void append(final LoggingEvent event) { // // if dispatcher thread has died then // append subsequent events synchronously // See bug 23021 if ((dispatcher == null) || !dispatcher.isAlive() || (bufferSize <= 0)) { synchronized (appenders) { appenders.appendLoopOnAppenders(event); } return; } // Set the NDC and thread name for the calling thread as these // LoggingEvent fields were not set at event creation time. event.getNDC(); event.getThreadName(); // Get a copy of this thread's MDC. event.getMDCCopy(); if (locationInfo) { event.getLocationInformation(); } synchronized (buffer) { while (true) { int previousSize = buffer.size(); if (previousSize < bufferSize) { buffer.add(event); // // if buffer had been empty // signal all threads waiting on buffer // to check their conditions. // if (previousSize == 0) { buffer.notifyAll(); } break; } // // Following code is only reachable if buffer is full // // // if blocking and thread is not already interrupted // and not the dispatcher then // wait for a buffer notification boolean discard = true; if (blocking && !Thread.interrupted() && Thread.currentThread() != dispatcher) { try { buffer.wait(); discard = false; } catch (InterruptedException e) { // // reset interrupt status so // calling code can see interrupt on // their next wait or sleep. Thread.currentThread().interrupt(); } } // // if blocking is false or thread has been interrupted // add event to discard map. // if (discard) { String loggerName = event.getLoggerName(); DiscardSummary summary = (DiscardSummary) discardMap.get(loggerName); if (summary == null) { summary = new DiscardSummary(event); discardMap.put(loggerName, summary); } else { summary.add(event); } break; } } } } /** * Close this AsyncAppender by interrupting the dispatcher * thread which will process all pending events before exiting. */ public void close() { /** * Set closed flag and notify all threads to check their conditions. * Should result in dispatcher terminating. */ synchronized (buffer) { closed = true; buffer.notifyAll(); } try { dispatcher.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); org.apache.log4j.helpers.LogLog.error( "Got an InterruptedException while waiting for the " + "dispatcher to finish.", e); } // // close all attached appenders. // synchronized (appenders) { Enumeration iter = appenders.getAllAppenders(); if (iter != null) { while (iter.hasMoreElements()) { Object next = iter.nextElement(); if (next instanceof Appender) { ((Appender) next).close(); } } } } } /** * Get iterator over attached appenders. * @return iterator or null if no attached appenders. */ public Enumeration getAllAppenders() { synchronized (appenders) { return appenders.getAllAppenders(); } } /** * Get appender by name. * * @param name name, may not be null. * @return matching appender or null. */ public Appender getAppender(final String name) { synchronized (appenders) { return appenders.getAppender(name); } } /** * Gets whether the location of the logging request call * should be captured. * * @return the current value of the LocationInfo option. */ public boolean getLocationInfo() { return locationInfo; } /** * Determines if specified appender is attached. * @param appender appender. * @return true if attached. */ public boolean isAttached(final Appender appender) { synchronized (appenders) { return appenders.isAttached(appender); } } /** * {@inheritDoc} */ public boolean requiresLayout() { return false; } /** * Removes and closes all attached appenders. */ public void removeAllAppenders() { synchronized (appenders) { appenders.removeAllAppenders(); } } /** * Removes an appender. * @param appender appender to remove. */ public void removeAppender(final Appender appender) { synchronized (appenders) { appenders.removeAppender(appender); } } /** * Remove appender by name. * @param name name. */ public void removeAppender(final String name) { synchronized (appenders) { appenders.removeAppender(name); } } /** * The LocationInfo option takes a boolean value. By default, it is * set to false which means there will be no effort to extract the location * information related to the event. As a result, the event that will be * ultimately logged will likely to contain the wrong location information * (if present in the log format). *

*

* Location information extraction is comparatively very slow and should be * avoided unless performance is not a concern. *

* @param flag true if location information should be extracted. */ public void setLocationInfo(final boolean flag) { locationInfo = flag; } /** * Sets the number of messages allowed in the event buffer * before the calling thread is blocked (if blocking is true) * or until messages are summarized and discarded. Changing * the size will not affect messages already in the buffer. * * @param size buffer size, must be positive. */ public void setBufferSize(final int size) { // // log4j 1.2 would throw exception if size was negative // and deadlock if size was zero. // if (size < 0) { throw new java.lang.NegativeArraySizeException("size"); } synchronized (buffer) { // // don't let size be zero. // bufferSize = (size < 1) ? 1 : size; buffer.notifyAll(); } } /** * Gets the current buffer size. * @return the current value of the BufferSize option. */ public int getBufferSize() { return bufferSize; } /** * Sets whether appender should wait if there is no * space available in the event buffer or immediately return. * * @param value true if appender should wait until available space in buffer. */ public void setBlocking(final boolean value) { synchronized (buffer) { blocking = value; buffer.notifyAll(); } } /** * Gets whether appender should block calling thread when buffer is full. * If false, messages will be counted by logger and a summary * message appended after the contents of the buffer have been appended. * * @return true if calling thread will be blocked when buffer is full. */ public boolean getBlocking() { return blocking; } /** * Summary of discarded logging events for a logger. */ private static final class DiscardSummary { /** * First event of the highest severity. */ private LoggingEvent maxEvent; /** * Total count of messages discarded. */ private int count; /** * Create new instance. * * @param event event, may not be null. */ public DiscardSummary(final LoggingEvent event) { maxEvent = event; count = 1; } /** * Add discarded event to summary. * * @param event event, may not be null. */ public void add(final LoggingEvent event) { if (event.getLevel().toInt() > maxEvent.getLevel().toInt()) { maxEvent = event; } count++; } /** * Create event with summary information. * * @return new event. */ public LoggingEvent createEvent() { String msg = MessageFormat.format( "Discarded {0} messages due to full event buffer including: {1}", new Object[] { new Integer(count), maxEvent.getMessage() }); return new LoggingEvent( "org.apache.log4j.AsyncAppender.DONT_REPORT_LOCATION", Logger.getLogger(maxEvent.getLoggerName()), maxEvent.getLevel(), msg, null); } } /** * Event dispatcher. */ private static class Dispatcher implements Runnable { /** * Parent AsyncAppender. */ private final AsyncAppender parent; /** * Event buffer. */ private final List buffer; /** * Map of DiscardSummary keyed by logger name. */ private final Map discardMap; /** * Wrapped appenders. */ private final AppenderAttachableImpl appenders; /** * Create new instance of dispatcher. * * @param parent parent AsyncAppender, may not be null. * @param buffer event buffer, may not be null. * @param discardMap discard map, may not be null. * @param appenders appenders, may not be null. */ public Dispatcher( final AsyncAppender parent, final List buffer, final Map discardMap, final AppenderAttachableImpl appenders) { this.parent = parent; this.buffer = buffer; this.appenders = appenders; this.discardMap = discardMap; } /** * {@inheritDoc} */ public void run() { boolean isActive = true; // // if interrupted (unlikely), end thread // try { // // loop until the AsyncAppender is closed. // while (isActive) { LoggingEvent[] events = null; // // extract pending events while synchronized // on buffer // synchronized (buffer) { int bufferSize = buffer.size(); isActive = !parent.closed; while ((bufferSize == 0) && isActive) { buffer.wait(); bufferSize = buffer.size(); isActive = !parent.closed; } if (bufferSize > 0) { events = new LoggingEvent[bufferSize + discardMap.size()]; buffer.toArray(events); // // add events due to buffer overflow // int index = bufferSize; for ( Iterator iter = discardMap.values().iterator(); iter.hasNext();) { events[index++] = ((DiscardSummary) iter.next()).createEvent(); } // // clear buffer and discard map // buffer.clear(); discardMap.clear(); // // allow blocked appends to continue buffer.notifyAll(); } } // // process events after lock on buffer is released. // if (events != null) { for (int i = 0; i < events.length; i++) { synchronized (appenders) { appenders.appendLoopOnAppenders(events[i]); } } } } } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } } }