/* 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.logback;
import java.io.Flushable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import org.perf4j.GroupedTimingStatistics;
import org.perf4j.StopWatch;
import org.perf4j.chart.GoogleChartGenerator;
import org.perf4j.chart.StatisticsChartGenerator;
import org.perf4j.helpers.MiscUtils;
import org.perf4j.helpers.StatsValueRetriever;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.AppenderBase;
import ch.qos.logback.core.spi.AppenderAttachable;
import ch.qos.logback.core.spi.AppenderAttachableImpl;
/**
* This appender is designed to be attached to an {@link AsyncCoalescingStatisticsAppender}. It takes the incoming
* GroupedTimingStatistics log messages and uses this data to update a graphical view of the logged statistics. If
* ANOTHER appender is then attached to this appender then the graph URLs will be written to the appender on a scheduled
* basis. Alternatively, the graph can be viewed by setting up a
* {@link org.perf4j.logback.servlet.GraphingServlet} to expose the graph images.
*
* @author Alex Devine
* @author Xu Huisheng
*/
public class GraphingStatisticsAppender extends AppenderBase<LoggingEvent>
implements AppenderAttachable<LoggingEvent>, Flushable {
/**
* This class keeps track of all appenders of this type that have been created. This allows static access to
* the appenders from the org.perf4j.logback.servlet.GraphingServlet class.
*/
protected final static Map<String, GraphingStatisticsAppender> APPENDERS_BY_NAME =
Collections.synchronizedMap(new LinkedHashMap<String, GraphingStatisticsAppender>());
// --- configuration options ---
/**
* The type of data to display on the graph. Defaults to "Mean" to display mean values. Acceptable values are any
* constant name from the {@link org.perf4j.helpers.StatsValueRetriever} class, such as Mean, Min, Max, Count,
* StdDev or TPS.
*/
private String graphType = StatsValueRetriever.MEAN_VALUE_RETRIEVER.getValueName();
/**
* A comma-separated list of the tag names that should be graphed. If not set then a separate series will be
* displayed on the graph for each tag name logged.
*/
private String tagNamesToGraph = null;
/**
* Gets the number of data points that will be written on each graph before the graph URL is written to any
* attached appenders. Thus, this option is only relevant if there are attached appenders.
* Defaults to <tt>StatisticsChartGenerator.DEFAULT_MAX_DATA_POINTS</tt>.
*/
private int dataPointsPerGraph = StatisticsChartGenerator.DEFAULT_MAX_DATA_POINTS;
// --- contained objects/state variables ---
/**
* The chart genertor, initialized in the <tt>activateOptions</tt> method, that stores the data for the chart.
*/
private StatisticsChartGenerator chartGenerator;
/**
* Keeps track of the number of logged GroupedTimingStatistics, which is used to determine when a graph should
* be written to any attached appenders.
*/
private AtomicLong numLoggedStatistics = new AtomicLong();
/**
* Keeps track of whether there is existing data that hasn't yet been flushed to downstream appenders.
*/
private volatile boolean hasUnflushedData = false;
/**
* Keeps track of the Level of the last appended event. This is just used to determine what level we send to OUR
* downstream events.
*/
private Level lastAppendedEventLevel = Level.INFO;
/**
* Any downstream appenders are contained in this AppenderAttachableImpl
*/
private final AppenderAttachableImpl<LoggingEvent> downstreamAppenders = new AppenderAttachableImpl<LoggingEvent>();
// --- options ---
/**
* The <b>GraphType</b> option is used to specify the data that should be displayed on the graph. Acceptable
* values are Mean, Min, Max, Count, StdDev and TPS (for transactions per second). Defaults to Mean if not
* explicitly set.
*
* @return The value of the GraphType option
*/
public String getGraphType() {
return graphType;
}
/**
* Sets the value of the <b>GraphType</b> option. This must be a valid type, one of
* Mean, Min, Max, Count, StdDev or TPS (for transactions per second).
*
* @param graphType The new value for the GraphType option.
*/
public void setGraphType(String graphType) {
this.graphType = graphType;
}
/**
* The <b>TagNamesToGraph</b> option is used to specify which tags should be logged as a data series on the
* graph. If not specified ALL tags will be drawn on the graph, one series for each tag.
*
* @return The value of the TagNamesToGraph option
*/
public String getTagNamesToGraph() {
return tagNamesToGraph;
}
/**
* Sets the value of the <b>TagNamesToGraph</b> option.
*
* @param tagNamesToGraph The new value for the TagNamesToGraph option.
*/
public void setTagNamesToGraph(String tagNamesToGraph) {
this.tagNamesToGraph = tagNamesToGraph;
}
/**
* The <b>DataPointsPerGraph</b> option is used to specify how much data should be displayed on each graph before
* it is written to any attached appenders. Defaults to <tt>StatisticsChartGenerator.DEFAULT_MAX_DATA_POINTS</tt>.
*
* @return The value of the DataPointsPerGraph option
*/
public int getDataPointsPerGraph() {
return dataPointsPerGraph;
}
/**
* Sets the value of the <b>DataPointsPerGraph</b> option.
*
* @param dataPointsPerGraph The new value for the DataPointsPerGraph option.
*/
public void setDataPointsPerGraph(int dataPointsPerGraph) {
if (dataPointsPerGraph <= 0) {
throw new IllegalArgumentException("The DataPointsPerGraph option must be positive");
}
this.dataPointsPerGraph = dataPointsPerGraph;
}
// --- lifecycle ---
@Override
public void start() {
super.start();
chartGenerator = createChartGenerator();
//update the static APPENDERS_BY_NAME object
if (getName() != null) {
APPENDERS_BY_NAME.put(getName(), this);
}
}
@Override
public void stop() {
//close any downstream appenders
synchronized (downstreamAppenders) {
flush();
downstreamAppenders.detachAndStopAllAppenders();
}
super.stop();
}
/**
* Helper method creates a new StatisticsChartGenerator based on the options set on this appender. By default
* a GoogleChartGenerator is created, though subclasses may override this method to create a different type of
* chart generator.
*
* @return A newly created StatisticsChartGenerator.
*/
protected StatisticsChartGenerator createChartGenerator() {
StatsValueRetriever statsValueRetriever = StatsValueRetriever.DEFAULT_RETRIEVERS.get(getGraphType());
if (statsValueRetriever == null) {
throw new RuntimeException("Unknown GraphType: " + getGraphType() +
". See the StatsValueRetriever class for the list of acceptable types.");
}
//create the chart generator and set the enabled tags
GoogleChartGenerator retVal = new GoogleChartGenerator(statsValueRetriever);
if (getTagNamesToGraph() != null) {
Set<String> enabledTags =
new HashSet<String>(Arrays.asList(MiscUtils.splitAndTrim(getTagNamesToGraph(), ",")));
retVal.setEnabledTags(enabledTags);
}
return retVal;
}
// --- exposed objects ---
/**
* Gets the contained StatisticsChartGenerator that is used to generate the graphs.
*
* @return The StatisticsChartGenerator used by this appender.
*/
public StatisticsChartGenerator getChartGenerator() {
return chartGenerator;
}
/**
* This static method returns any created GraphingStatisticsAppender by its name.
*
* @param appenderName the name of the GraphingStatisticsAppender to return
* @return the specified GraphingStatisticsAppender, or null if not found
*/
public static GraphingStatisticsAppender getAppenderByName(String appenderName) {
return APPENDERS_BY_NAME.get(appenderName);
}
/**
* This static method returns an unmodifiable collection of all GraphingStatisticsAppenders that have been created.
*
* @return The collection of GraphingStatisticsAppenders created in this VM.
*/
public static Collection<GraphingStatisticsAppender> getAllGraphingStatisticsAppenders() {
return Collections.unmodifiableCollection(APPENDERS_BY_NAME.values());
}
// --- appender attachable methods ---
public void addAppender(Appender<LoggingEvent> appender) {
synchronized (downstreamAppenders) {
downstreamAppenders.addAppender(appender);
}
}
public Iterator<Appender<LoggingEvent>> iteratorForAppenders() {
synchronized (downstreamAppenders) {
return downstreamAppenders.iteratorForAppenders();
}
}
public Appender<LoggingEvent> getAppender(String name) {
synchronized (downstreamAppenders) {
return downstreamAppenders.getAppender(name);
}
}
public boolean isAttached(Appender<LoggingEvent> appender) {
synchronized (downstreamAppenders) {
return downstreamAppenders.isAttached(appender);
}
}
public void detachAndStopAllAppenders() {
synchronized (downstreamAppenders) {
downstreamAppenders.detachAndStopAllAppenders();
}
}
public boolean detachAppender(Appender<LoggingEvent> appender) {
synchronized (downstreamAppenders) {
return downstreamAppenders.detachAppender(appender);
}
}
public boolean detachAppender(String name) {
synchronized (downstreamAppenders) {
return downstreamAppenders.detachAppender(name);
}
}
// --- appender methods ---
@Override
protected void append(LoggingEvent event) {
if ((event.getArgumentArray() != null)
&& (event.getArgumentArray().length > 0)) {
Object logMessage = event.getArgumentArray()[0];
if (logMessage instanceof GroupedTimingStatistics && chartGenerator != null) {
chartGenerator.appendData((GroupedTimingStatistics) logMessage);
hasUnflushedData = true;
lastAppendedEventLevel = event.getLevel();
//output the graph if necessary to any attached appenders
if ((numLoggedStatistics.incrementAndGet() % getDataPointsPerGraph()) == 0) {
flush();
}
}
}
}
// --- Flushable method ---
/**
* This flush method writes the graph, with the data that exists at the time it is calld, to any attached appenders.
*/
public void flush() {
synchronized (downstreamAppenders) {
if (hasUnflushedData) {
downstreamAppenders.appendLoopOnAppenders(new LoggingEvent(
Logger.class.getName(),
(Logger) LoggerFactory.getLogger(StopWatch.DEFAULT_LOGGER_NAME),
lastAppendedEventLevel,
chartGenerator.getChartUrl(), null, null));
hasUnflushedData = false;
}
}
}
}