/*
* 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 com.addthis.basis.util;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
/**
* A utility for easy counting of events. This class introduces two concepts:
* <ul>
* <li>Counters against which you can tally events</li>
* <li>Metrics like count, average, rate, etc, which are computed from those
* counters</li>
* </ul>
* Meter can store multiple counters (ex: hits, errors) which you define in an
* Enum and pass to the constructor. Once you've instantiated a Meter, set up
* the metrics you want to track and start counting. Call peek() at any time to
* retrieve metrics or call mark() to reset the counters.
*
* @param <E> Enum type that defines a set of counters
*/
public class Meter<E extends Enum<E>> {
private Counter[] counters; // metrics counted since last mark()
private Metric[] metrics; // metric labels for each counter
/**
* @param counters initialize with enum.values()
*/
public Meter(E[] counters) {
this.counters = new Counter[counters.length];
for (int i = 0; i < counters.length; i++) {
this.counters[i] = new Counter();
}
metrics = new Metric[this.counters.length];
for (int i = 0; i < metrics.length; i++) {
metrics[i] = new Metric();
}
}
/**
* add a count metric for a counter
*
* @param which the counter
* @param label label to use for reporting
*/
public Meter<E> addCountMetric(E which, String label) {
metrics[which.ordinal()].setCountLabel(label);
return this;
}
/**
* add a count metric for a counter
*
* @param which the counter
*/
public Meter<E> addCountMetric(E which) {
return addCountMetric(which, which.toString());
}
/**
* add a max metric for a counter
*
* @param which the counter
* @param label label to use for reporting
*/
public Meter<E> addMaxMetric(E which, String label) {
metrics[which.ordinal()].setMaxLabel(label);
return this;
}
/**
* add a max metric for a counter
*
* @param which the counter
*/
public Meter<E> addMaxMetric(E which) {
return addMaxMetric(which, which.toString());
}
/**
* add an average metric for a counter
*
* @param which the counter
* @param label label to use for reporting
*/
public Meter<E> addAverageMetric(E which, String label) {
metrics[which.ordinal()].setAverageLabel(label);
return this;
}
/**
* add an average metric for a counter
*
* @param which the counter
*/
public Meter<E> addAverageMetric(E which) {
return addAverageMetric(which, which.toString());
}
/**
* add a rate metric for a counter
*
* @param which the counter
* @param label label to use for reporting
*/
public Meter<E> addRateMetric(E which, String label) {
metrics[which.ordinal()].setRateLabel(label);
return this;
}
/**
* add a rate metric for a counter
*
* @param which the counter
*/
public Meter<E> addRateMetric(E which) {
return addRateMetric(which, which.toString());
}
/**
* increment a counter (by one)
*/
public void inc(E which) {
counters[which.ordinal()].add(1);
}
/**
* add val to a counter
*/
public void inc(E which, long val) {
counters[which.ordinal()].add(val);
}
/**
* return the sum of all puts to a metric since last mark()
*/
public long getCount(E which) {
return counters[which.ordinal()].get().getCount();
}
/**
* return the average of all puts to a metric since last mark()
*/
public long getAverage(E which) {
return counters[which.ordinal()].get().getAverage();
}
/**
* return a metric's count divided by the time elapsed (in seconds) since
* last mark()
*/
public double getRate(E which) {
return counters[which.ordinal()].get().getRate();
}
/**
* reset a counter
*/
public void reset(E which) {
counters[which.ordinal()].mark();
}
/**
* return the values of metrics since last mark()
*/
public Map<String, Long> peek() {
// get all the metrics - must do this once so state is consistent
CounterState[] states = new CounterState[counters.length];
for (int i = 0; i < counters.length; i++) {
states[i] = counters[i].get();
}
return buildViews(states);
}
/**
* return reset and get the values of metrics since last mark()
*/
public Map<String, Long> mark() {
// mark all the metrics
CounterState[] states = new CounterState[counters.length];
for (int i = 0; i < counters.length; i++) {
states[i] = counters[i].mark();
}
return buildViews(states);
}
private Map<String, Long> buildViews(CounterState[] states) {
// fetch state for each label and build output
Map<String, Long> result = new LinkedHashMap<>();
for (int i = 0; i < metrics.length; i++) {
if (metrics[i] == null) {
continue;
}
if (!LessStrings.isEmpty(metrics[i].getCountLabel())) {
result.put(metrics[i].getCountLabel(), states[i].getCount());
}
if (!LessStrings.isEmpty(metrics[i].getMaxLabel())) {
result.put(metrics[i].getMaxLabel(), states[i].getMax());
}
if (!LessStrings.isEmpty(metrics[i].getAverageLabel())) {
result.put(metrics[i].getAverageLabel(), states[i].getAverage());
}
if (!LessStrings.isEmpty(metrics[i].getRateLabel())) {
result.put(metrics[i].getRateLabel(), (long) states[i].getRate());
}
}
return result;
}
/**
* A helper class for counting events and computing event totals, average
* event size and event rate.
* this class uses native atomic operations instead of synchronized methods
* to improve performance
* for heavily threaded applications - results may be slightly skewed if a
* put() spans a mark()
*/
private static class Counter {
private AtomicLong count;
private AtomicLong max;
private AtomicLong puts;
private AtomicLong timestamp;
public Counter() {
count = new AtomicLong();
max = new AtomicLong();
puts = new AtomicLong();
timestamp = new AtomicLong(System.currentTimeMillis());
}
/**
* add val to the internal counter
*/
public void add(long val) {
count.addAndGet(val);
if (val > max.get()) {
max.set(val); // can miss concurrent writes greater than max
}
puts.incrementAndGet();
}
/**
* return the counter states
*/
public CounterState get() {
return new CounterState(count.get(), max.get(), puts.get(), System.currentTimeMillis() - timestamp.get());
}
/**
* return the counter states and reset their values
*/
public CounterState mark() {
long now = System.currentTimeMillis();
return new CounterState(count.getAndSet(0), max.getAndSet(0), puts.getAndSet(0), now
- timestamp.getAndSet(now));
}
}
private static class CounterState {
private final long count;
private final long max;
private final long puts;
private final long intervalms;
public CounterState(long count, long max, long puts, long intervalms) {
this.count = count;
this.max = max;
this.puts = puts;
this.intervalms = intervalms;
}
/**
* return the sum of all puts since last reset
*/
public long getCount() {
return count;
}
/**
* return the largest put since last reset
*/
public long getMax() {
return max;
}
/**
* return the average of all puts since last reset
*/
public long getAverage() {
return puts == 0 ? 0 : count / puts;
}
/**
* return the sum of all puts divided by the time elapsed (in seconds)
* since last reset
*/
public double getRate() {
return intervalms == 0 ? 0. : ((double) count * 1000) / intervalms;
}
}
private static class Metric {
private String countLabel;
private String maxLabel;
private String averageLabel;
private String rateLabel;
public Metric() {
}
public Metric(String countLabel, String maxLabel, String averageLabel, String rateLabel) {
this.countLabel = countLabel;
this.maxLabel = maxLabel;
this.averageLabel = averageLabel;
this.rateLabel = rateLabel;
}
public String getCountLabel() {
return countLabel;
}
public String getMaxLabel() {
return maxLabel;
}
public String getAverageLabel() {
return averageLabel;
}
public String getRateLabel() {
return rateLabel;
}
public void setCountLabel(String countLabel) {
this.countLabel = countLabel;
}
public void setMaxLabel(String maxLabel) {
this.maxLabel = maxLabel;
}
public void setAverageLabel(String averageLabel) {
this.averageLabel = averageLabel;
}
public void setRateLabel(String rateLabel) {
this.rateLabel = rateLabel;
}
}
}