/* Copyright (c) 2008-2009 HomeAway, Inc. * All rights reserved. http://www.perf4j.org * * Licensed 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. */ package org.perf4j.helpers; import org.perf4j.GroupedTimingStatistics; import org.perf4j.StopWatch; import java.util.Iterator; import java.util.LinkedList; import java.util.NoSuchElementException; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; /** * This class provides the implementation for the AsyncCoalescingStatisticsAppenders made available for different * logging frameworks. This class itself is generic in that it does not use any logging-framework-specific APIs, but * is intended to be wrapped by classes that DO use those specific APIs. * * @see org.perf4j.log4j.AsyncCoalescingStatisticsAppender */ public class GenericAsyncCoalescingStatisticsAppender { /** * The GroupedTimingStatisticsHandler defines a callback interface so that logging-framework-specific * implementations can decide what to do with the coalesced GroupedTimingStatistics. */ public interface GroupedTimingStatisticsHandler { /** * This callback method is called for each GroupedTimingStatistics instance that is formed by coalescing * individual StopWatch messages from the logs. Implementations will most likely pass this instance to * downstream appenders or handlers. * * @param statistics The GroupedTimingStatistics instance. */ void handle(GroupedTimingStatistics statistics); /** * This method is called whenever an error occurs that should be handled in a logging-framework specific * manner. * * @param errorMessage The message that describes the error. */ void error(String errorMessage); } // --- configuration options --- /** * The name of this appender. */ private String name = ""; /** * TimeSlice option */ private long timeSlice = 30000L; /** * CreateRollupStatistics option */ private boolean createRollupStatistics = false; /** * The QueueSize option, used to set the capacity of the loggedMessages queue */ private int queueSize = 1024; /** * Wait time for queue to clear when shutting down, in milliseconds. */ private long shutdownWaitMillis = 10000L; /** * The fully qualified class name of the class to use for StopWatch parsing, defaults to the standard * org.perf4j.helpers.StopWatchParser */ private String stopWatchParserClassName = StopWatchParser.class.getName(); // --- contained objects --- /** * All GroupedTimingStatistics created by this appender are passed to the handler object for further handling. * This variable is set by the param passed to the start() method. */ private GroupedTimingStatisticsHandler handler = null; /** * StopWatch log messages are pushed onto this queue, which is initialized in start(). */ private BlockingQueue<String> loggedMessages = null; /** * This parser is used to convert String log messages to StopWatches */ private StopWatchParser stopWatchParser; /** * This thread pumps logs from the loggedMessages queue. It is created in start(). */ private Thread drainingThread = null; /** * This int keeps track of the total number of messages that had to be discarded due to the queue being full. */ private volatile int numDiscardedMessages = 0; // --- options --- /** * The name of this appender. * * @return The name of this appender. */ public String getName() { return name; } /** * Sets the name of this appender. * * @param name The new appender name. */ public void setName(String name) { this.name = name; } /** * The <b>TimeSlice</b> option represents the length of time, in milliseconds, of the window in which appended * log events are coalesced to a single GroupedTimingStatistics and sent to the GroupedTimingStatisticsHandler. * Defaults to 30,000 milliseconds. * * @return the TimeSlice option. */ public long getTimeSlice() { return timeSlice; } /** * Sets the value of the <b>TimeSlice</b> option. * * @param timeSlice The new TimeSlice option, in milliseconds. */ public void setTimeSlice(long timeSlice) { this.timeSlice = timeSlice; } /** * The <b>CreateRollupStatistics</b> option is used to determine whether "rollup" statistics should be created. * If the tag name of a StopWatch in a log message contains periods, then the GroupedTimingStatistics will be * created as if each substring of the tag up to the period was also logged with a separate StopWatch instance. * For example, suppose a StopWatch was logged with a tag of "requests.specificReq.PASS". For grouping purposes * a StopWatch entry would be logged under each of the following tags: * <ul> * <li>requests * <li>requests.specificReq * <li>requests.specificReq.PASS * </ul> * This allows you to view statistics at both an individual and aggregated level. If there were other StopWatch * entries with a tag of "requests.specificReq.FAIL", then the data collected at the "requests.specificReq" level * would include BOTH PASS and FAIL events. * * @return The CreateRollupStatistics option. */ public boolean isCreateRollupStatistics() { return createRollupStatistics; } /** * Sets the value of the <b>CreateRollupStatistics</b> option. * * @param createRollupStatistics The new CreateRollupStatistics option. */ public void setCreateRollupStatistics(boolean createRollupStatistics) { this.createRollupStatistics = createRollupStatistics; } /** * The <b>QueueSize</b> option is used to control the size of the internal queue used by this appender to store * logged messages before they are sent to downstream appenders. Defaults to 1024. If set too small and the queue * fills up, then logged StopWatches will be discarded. The number of discarded messages can be accessed using the * {@link #getNumDiscardedMessages()} method. * * @return The QueueSize option. */ public int getQueueSize() { return queueSize; } /** * Sets the value of the <b>QueueSize</b> option. * * @param queueSize The new QueueSize option. */ public void setQueueSize(int queueSize) { this.queueSize = queueSize; } /** * The <b>ShutdownWaitMillis</b> option is used to control how long this class will block, waiting for the queue * to drain, when shutting-down the Appender. * * After this timeout expires, no new messages will be drained from the log queue, the log queue will be * truncated and shutdown of the Appender will complete. * * @return The ShutdownWaitMillis option. */ public long getShutdownWaitMillis() { return shutdownWaitMillis; } /** * Sets the value of the <b>ShutdownWaitMillis</b> option. * * @param shutdownWaitMillis The new ShutdownWaitMillis option. */ public void setShutdownWaitMillis(long shutdownWaitMillis) { this.shutdownWaitMillis = shutdownWaitMillis; } /** * The <b>StopWatchParserClassName</b> option is used to determine the class used to parse stop watch messages * into StopWatch instances. This defaults to the standard "org.perf4j.helpers.StopWatchParser" class. * * @return The StopWatchParserClassName option. */ public String getStopWatchParserClassName() { return stopWatchParserClassName; } /** * Sets the value of the <b>StopWatchParserClassName</b> option. * * @param stopWatchParserClassName The new StopWatchParserClassName option. */ public void setStopWatchParserClassName(String stopWatchParserClassName) { this.stopWatchParserClassName = stopWatchParserClassName; } // --- attributes --- /** * Returns the number of StopWatch messages that have been discarded due to the queue being full. * * @return The number of discarded messages. */ public int getNumDiscardedMessages() { return numDiscardedMessages; } // --- main lifecycle methods --- /** * The start method should only be called once, before the append method is called, to initialize options. * * @param handler The GroupedTimingStatisticsHandler used to process GroupedTimingStatistics created by aggregating * StopWatch log message. */ public void start(GroupedTimingStatisticsHandler handler) { //start should only be called once, but just in case: if (drainingThread != null) { stopDrainingThread(); } this.handler = handler; stopWatchParser = newStopWatchParser(); numDiscardedMessages = 0; loggedMessages = new ArrayBlockingQueue<String>(getQueueSize()); drainingThread = new Thread(new Dispatcher(), "perf4j-async-stats-appender-sink-" + getName()); drainingThread.setDaemon(true); drainingThread.start(); } /** * The append method should be called each time a StopWatch log message is handled by the logging framework. * * @param message The log message, may not be null. If this message is not a valid StopWatch log message it will * be discarded. */ public void append(String message) { //Do a quick check to cull out any messages not meant for us if (stopWatchParser.isPotentiallyValid(message)) { if (!loggedMessages.offer(message)) { ++numDiscardedMessages; handler.error(message); } } } /** * This method should be called on shutdown to flush any pending messages in the queue and create a final * GroupedTimingStatistics instance if necessary. */ public void stop() { stopDrainingThread(); } // --- Helper Methods --- /** * Helper method stops the draining thread and waits for it to finish. */ private void stopDrainingThread() { try { //pushing an empty string on the queue tells the draining thread that we're closing loggedMessages.put(""); //wait for the draining thread to finish drainingThread.join(shutdownWaitMillis); drainingThread.interrupt(); if (loggedMessages.size() > 0) { handler.error("Shutdown, queued/undrained stopwatch count: " + loggedMessages.size()); } } catch (Exception e) { handler.error("Unexpected error stopping AsyncCoalescingStatisticsAppender draining thread: " + e.getMessage()); } finally { loggedMessages.clear(); } } /** * Helper method instantiates a new StopWatchParser based on the StopWatchParserClassName option. * * @return The newly created StopWatchParser */ private StopWatchParser newStopWatchParser() { try { return (StopWatchParser) Class.forName(stopWatchParserClassName).newInstance(); } catch (Exception e) { throw new RuntimeException("Could not create StopWatchParser: " + e.getMessage(), e); } } // --- Support Classes --- /** * This Dispatcher Runnable uses a StopWatchesFromQueueIterator to pull StopWatch logging message off the * loggedMessages queue, which are grouped to create GroupedTimingStatistics by the GroupingStatisticsIterator. * The GroupedTimingStatisticsHandler is then called to deal with the created GroupedTimingStatistics. */ private class Dispatcher implements Runnable { public void run() { GroupingStatisticsIterator statsIterator = new GroupingStatisticsIterator(new StopWatchesFromQueueIterator(), timeSlice, createRollupStatistics); while (statsIterator.hasNext()) { try { handler.handle(statsIterator.next()); } catch (Exception e) { handler.error("Error calling the GroupedTimingStatisticsHandler: " + e.getMessage()); } } } } /** * This helper class pulls StopWatch log messages off the loggedMessages queue and exposes them through the * Iterator interface. */ private class StopWatchesFromQueueIterator implements Iterator<StopWatch> { /** * Messages are drained to this list in blocks. */ private LinkedList<String> drainedMessages = new LinkedList<String>(); /** * Keeps track of the NEXT stop watch we will return. */ private StopWatch nextStopWatch; /** * State variable keeps track of whether we've already determined that the loggedMessages queue has been closed. */ private boolean done; /** * State variable keeps track of whether we've finished waiting for a timeslice. * If true, hasNext will return true and next will return null. */ private boolean timeSliceOver; public boolean hasNext() { if (nextStopWatch == null) { nextStopWatch = getNext(); //then try to get it } return timeSliceOver || nextStopWatch != null; } public StopWatch next() { if (timeSliceOver) { timeSliceOver = false; return null; } else if (nextStopWatch == null) { nextStopWatch = getNext(); //then try to get it, and barf if there is no more if (nextStopWatch == null) { throw new NoSuchElementException(); } } StopWatch retVal = nextStopWatch; nextStopWatch = null; return retVal; } public void remove() { throw new UnsupportedOperationException(); } private StopWatch getNext() { if (done) { //if we already found out we're done, short circuit so we won't block return null; } while (true) { if (drainedMessages.isEmpty()) { loggedMessages.drainTo(drainedMessages, 64); //drainTo is more efficient but it doesn't block, so if we're still empty call take() to block if (drainedMessages.isEmpty()) { //then wait for a message to show up try { String message = loggedMessages.poll(timeSlice, TimeUnit.MILLISECONDS); if (message == null) { // no new messages, but want to indicate to check the timeslice timeSliceOver = true; return null; } else { drainedMessages.add(message); } } catch (InterruptedException ie) { //someone interrupted us, we're done done = true; return null; } } } while (!drainedMessages.isEmpty()) { String message = drainedMessages.removeFirst(); if (message.length() == 0) { //the empty message is pushed onto the queue by the enclosing class' close() method //to indicate that we're done done = true; return null; } StopWatch parsedStopWatch = stopWatchParser.parseStopWatch(message); if (parsedStopWatch != null) { return parsedStopWatch; } //otherwise the message wasn't a valid stopWatch, so let the loop continue to get the next one } } } } }