package com.kendelong.util.jmx.statistics;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
/**
* A base implementation for adding simple statistics to a class with JMX.
*
* The instrumented class should create a private final instance of this
* class. Then, it should call the increment*() methods when it detects a
* success, failure, or error (every call results in on of these three). The instrumented
* class should also implement IStatisticsProvider and delegate all the methods to
* contained instance of this class. Finally, an MBean (which can extend
* StatisticsProvierMBeanBase for convenience) wraps the instrumented class.
*
* Another possible setup would be to instantiate this class as a Spring
* Bean, and inject it into both the instrumented class and the MBean. This
* would be infeasible if the instrumented class is not a singleton.
*
* Definitions:
* success - duh
* failure - the invocation of the service succeeded, but the service indicates
* that it was unable to fulfill the request.
* error - an exception is detected: something is broken.
*
* @author kdelong
*/
public class StatisticsProvider implements IStatisticsProvider
{
private final AtomicLong successes = new AtomicLong();
private final AtomicLong failures = new AtomicLong();
private final AtomicLong errors = new AtomicLong();
private final AtomicReference<Date> startTime = new AtomicReference<Date>();
private final Map<String, AtomicLong> errorHistogram = new ConcurrentHashMap<String, AtomicLong>();
public StatisticsProvider()
{
startTime.set(new Date());
}
@Override
public double getAverageRequestsPerSecond()
{
long now = System.currentTimeMillis();
long start = startTime.get().getTime();
long elapsedTime = now - start;
if(elapsedTime == 0)
return 0;
return 1000.0*getTotalNumberOfAccesses()/elapsedTime;
}
@Override
public long getNumberOfErrors()
{
return errors.get();
}
@Override
public long getNumberOfFailures()
{
return failures.get();
}
@Override
public long getNumberOfSuccesses()
{
return successes.get();
}
@Override
public long getTotalNumberOfAccesses()
{
return getNumberOfSuccesses() + getNumberOfFailures() + getNumberOfErrors();
}
@Override
public Map<String, AtomicLong> getErrorHistogram()
{
return errorHistogram;
}
@Override
public void resetStatistics()
{
successes.set(0);
failures.set(0);
errors.set(0);
startTime.set(new Date());
errorHistogram.clear();
}
// Internal API for client class
public void incrementErrors()
{
errors.incrementAndGet();
}
public void incrementFailures()
{
failures.incrementAndGet();
}
public void incrementSuccess()
{
successes.incrementAndGet();
}
public synchronized void logValidationFailures(BindingResult errors)
{
for (ObjectError error : (List<ObjectError>)errors.getAllErrors())
{
String errorName = error.getCode();
if (errorHistogram.containsKey(errorName))
{
// error exists in map -- update!
AtomicLong count = errorHistogram.get(errorName);
count.addAndGet(1L);
}
else
{
// add error type to map
AtomicLong count = new AtomicLong();
count.set(1L);
errorHistogram.put(errorName, count);
}
}
}
/**
* Increment an error in the validation map. We can use the validationError map to
* record validation failures, but also to create histograms of any sort of error
* condition.
*
* Synchronized to be sure the creation of the atomics are done correctly. They should
* add a "GetOrCreate" atomic method to the map impl.
* @param errorName The name of the error
*/
public synchronized void addError(String errorName)
{
AtomicLong bucket = errorHistogram.get(errorName);
if(bucket == null)
{
bucket = new AtomicLong();
errorHistogram.put(errorName, bucket);
}
bucket.incrementAndGet();
}
}