package edu.brown.logging; import java.io.PrintStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.apache.log4j.Appender; import org.apache.log4j.AppenderSkeleton; import org.apache.log4j.Layout; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggerRepository; import org.apache.log4j.spi.LoggingEvent; import edu.brown.utils.CollectionUtil; import edu.brown.utils.StringUtil; /** * An appender that stores LoggingEvents in a ringbuffer (in-memory) * and allows to retrieve the latest log messages. * @author rschwarzkopf * @author pavlo * http://ds.informatik.uni-marburg.de/~fallenbeck/ICSd/javadoc/0.9.16/de/fb12/ics/logging/RingbufferAppender.html */ public class RingBufferAppender extends AppenderSkeleton { private static final Logger LOG = Logger.getLogger(RingBufferAppender.class); private static final int DEFAULT_SIZE = 1000; private LoggingEvent[] eventRing; private int currentPosition; private long counter; private int stackOffset = -1; private boolean useFastLocation = false; private boolean storeLocation = false; private boolean storeThreadName = false; /** * Create an appender instance. * @param bufferSize The size of the ringbuffer. */ public RingBufferAppender() { this.init(DEFAULT_SIZE); } public RingBufferAppender(int size) { this.init(size); } private void init(int size) { this.eventRing = new LoggingEvent[size]; this.currentPosition = -1; this.counter = 0; if (LOG.isDebugEnabled()) LOG.debug(String.format("Initialized appender with new buffer [size=%d, useFastLocation=%s, storeLocation=%s, storeThreadName=%s, layout=%s]", size, this.useFastLocation, this.storeLocation, this.storeThreadName, (this.getLayout() != null ? this.getLayout().getClass().getSimpleName() : null))); } public void setUseFastLocation(boolean val) { this.useFastLocation = val; } public void setStoreLocation(boolean val) { this.storeLocation = val; } public void setStoreThreadName(boolean val) { this.storeThreadName = val; } public void setSize(int size) { this.init(size); } /** * Return the size of the ringbuffer. * @return Size of ringbuffer */ public int getSize() { return this.eventRing.length; } @Override protected void append(LoggingEvent event) { if (this.useFastLocation) { if (this.stackOffset < 0) this.stackOffset = FastLoggingEvent.getStackOffset(event); event = new FastLoggingEvent(event, this.stackOffset); } if (this.storeLocation) event.getLocationInformation(); if (this.storeThreadName) event.getThreadName(); int position = -1; synchronized (this) { this.currentPosition = position = ++this.currentPosition % this.eventRing.length; this.counter++; } // SYNCH this.eventRing[position] = event; // assert(event.getLoggerName().contains("Handler") == false) : event; } @Override public void close() { // free memory for (int i = 0; i < this.eventRing.length; i++) this.eventRing[i] = null; } @Override public boolean requiresLayout() { return true; } /** * Returns the number of lines logged since ICS startup * @return Number of lines logged */ public long getLoggedLines() { return this.counter; } public LoggingEvent[] getLogEvents() { // create a snapshot by copying the complete storage LoggingEvent[] events = new LoggingEvent[this.eventRing.length]; System.arraycopy(this.eventRing, 0, events, 0, this.eventRing.length); // store the index of the next element to be stored in the rrStorage // this should be the oldest entry int nextEntryIndex = (this.currentPosition + 1) % this.eventRing.length; LoggingEvent[] sortedEvents; if (events[nextEntryIndex] == null) { // event entry has not yet been filled completely, start at zero sortedEvents = new LoggingEvent[nextEntryIndex]; for (int i = 0; i < sortedEvents.length; i++) { sortedEvents[i] = events[i]; } // the storage position might have changed between copying the array and // calculating the next entry index, compare with oldest entry } else { // find the oldest entry by comparing the timestamps int oldestEntryIndex = -1; long oldestEntryTimestamp = Long.MAX_VALUE; LoggingEvent entry; long entryTimestamp; for (int entryIndex = 0; entryIndex < events.length; entryIndex++) { // FIXME <= ? start at the nextEntryIndex? Maybe backwards? if ((entry = events[entryIndex]) != null && (entryTimestamp = entry.getTimeStamp()) < oldestEntryTimestamp) { oldestEntryIndex = entryIndex; oldestEntryTimestamp = entryTimestamp; } } // FOR // if the next entry index and oldest entry index are different and have the same timestamp // (which only happens if much logging is done), use the next entry index if (nextEntryIndex != oldestEntryIndex && oldestEntryTimestamp == events[nextEntryIndex].getTimeStamp()) oldestEntryIndex = nextEntryIndex; // sort the events into a new array // LoggingEvent[] sortedEvents = new LoggingEvent[events.length]; sortedEvents = new LoggingEvent[events.length]; for (int i = 0; i < sortedEvents.length; i++) { sortedEvents[i] = events[oldestEntryIndex++ % events.length]; } } // throw away the old array events = null; return (sortedEvents); } public String[] getLogMessages() { LoggingEvent[] events = this.getLogEvents(); String[] ret = new String[events.length]; Layout layout = this.getLayout(); for (int i = 0; i < events.length; i++) { ret[i] = layout.format(events[i]); } // FOR return (ret); } @SuppressWarnings("unchecked") public static void enableRingBufferAppender(Logger logger, int bufferSize) { Layout l = null; if (LOG.isDebugEnabled()) LOG.debug(logger + " => " + logger.getAllAppenders()); for (Object o : CollectionUtil.iterable(logger.getAllAppenders())) { Appender a = (Appender)o; l = a.getLayout(); } // FOR if (l != null) { logger.removeAllAppenders(); logger.addAppender(new RingBufferAppender(bufferSize)); Logger.getRootLogger().info("Enabled RingBuffer logging for '" + logger.getName() + "'"); } } @SuppressWarnings("unchecked") public static RingBufferAppender getRingBufferAppender(Logger logger) { RingBufferAppender rba = null; if (LOG.isTraceEnabled()) LOG.trace("Checking whether " + logger.getName() + " has a RingBufferAppender attached: " + CollectionUtil.list(logger.getAllAppenders())); for (Object o : CollectionUtil.iterable(logger.getAllAppenders())) { if (o instanceof RingBufferAppender) { rba = (RingBufferAppender)o; if (LOG.isDebugEnabled()) LOG.debug("Found " + rba + " for " + logger.getName()); break; } } // FOR return (rba); } @SuppressWarnings("unchecked") public static Collection<LoggingEvent> getLoggingEvents(LoggerRepository repo) { Set<Logger> loggers = new HashSet<Logger>(); for (Object o : CollectionUtil.iterable(repo.getCurrentLoggers())) { Logger logger = (Logger)o; RingBufferAppender rba = getRingBufferAppender(logger); if (rba != null) { if (LOG.isDebugEnabled()) LOG.debug(logger.getName() + " => " + rba + " / " + rba.getLayout()); loggers.add(logger); } } // FOR if (loggers.isEmpty()) return (Collections.emptyList()); return (getLoggingEvents(loggers.toArray(new Logger[0]))); } @SuppressWarnings("unchecked") public static Collection<String> getLoggingMessages(LoggerRepository repo) { Set<RingBufferAppender> appenders = new HashSet<RingBufferAppender>(); for (Object o : CollectionUtil.iterable(repo.getCurrentLoggers())) { Logger logger = (Logger)o; RingBufferAppender rba = getRingBufferAppender(logger); if (rba != null) { if (LOG.isDebugEnabled()) LOG.debug(logger.getName() + " => " + rba + " / " + rba.getLayout()); appenders.add(rba); } } // FOR if (appenders.isEmpty()) return (Collections.emptyList()); return (getLoggingMessages(appenders.toArray(new RingBufferAppender[0]))); } public static Collection<LoggingEvent> getLoggingEvents(Logger...loggers) { SortedSet<LoggingEvent> events = new TreeSet<LoggingEvent>(new Comparator<LoggingEvent>() { @Override public int compare(LoggingEvent o1, LoggingEvent o2) { return (int)(o1.timeStamp - o2.timeStamp); } }); for (Logger log : loggers) { RingBufferAppender rba = getRingBufferAppender(log); if (rba != null) { CollectionUtil.addAll(events, rba.getLogEvents()); } } // FOR return (events); } public static Collection<String> getLoggingMessages(RingBufferAppender...appenders) { List<LoggingEvent> events = new ArrayList<LoggingEvent>(); Layout layout = null; for (RingBufferAppender rba : appenders) { LoggingEvent e[] = rba.getLogEvents(); if (LOG.isDebugEnabled()) LOG.debug("Got " + e.length + " LoggingEvents for " + rba); CollectionUtil.addAll(events, e); if (layout == null) layout = rba.getLayout(); } // FOR if (events.isEmpty() == false) assert(layout != null); Collections.sort(events, new Comparator<LoggingEvent>() { @Override public int compare(LoggingEvent o1, LoggingEvent o2) { return (int)(o1.timeStamp - o2.timeStamp); } }); List<String> messages = new ArrayList<String>(); for (LoggingEvent event : events) { messages.add(layout.format(event)); } // FOR return (messages); } public void dump(PrintStream out) { int width = 100; out.println(StringUtil.header(this.getClass().getSimpleName(), "=", width)); for (String log : this.getLogMessages()) { out.println(log.trim()); } out.println(StringUtil.repeat("=", width)); out.flush(); } }