package org.stagemonitor.core.metrics.metrics2;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import com.codahale.metrics.Clock;
import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Histogram;
import com.codahale.metrics.Meter;
import com.codahale.metrics.ScheduledReporter;
import com.codahale.metrics.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.stagemonitor.core.util.ExecutorUtils;
/**
* A {@link ScheduledReporter} that works with a {@link Metric2Registry}
*/
public abstract class ScheduledMetrics2Reporter extends ScheduledReporter {
private static final Logger logger = LoggerFactory.getLogger(ScheduledMetrics2Reporter.class);
protected final Metric2Registry registry;
private final Metric2Filter filter;
private final ScheduledExecutorService executor;
protected Clock clock;
private boolean started;
protected ScheduledMetrics2Reporter(Builder builder) {
super(null, null, null, builder.getRateUnit(), builder.getDurationUnit(), builder.getExecutor());
this.registry = builder.getRegistry();
this.filter = builder.getFilter();
this.executor = builder.getExecutor();
this.clock = builder.getClock();
}
@Override
public void report() {
reportMetrics(
registry.getGauges(filter),
registry.getCounters(filter),
registry.getHistograms(filter),
registry.getMeters(filter),
registry.getTimers(filter)
);
}
/**
* Called periodically by the polling thread. Subclasses should report all the given metrics.
*
* @param gauges all of the gauges in the registry
* @param counters all of the counters in the registry
* @param histograms all of the histograms in the registry
* @param meters all of the meters in the registry
* @param timers all of the timers in the registry
*/
public abstract void reportMetrics(Map<MetricName, Gauge> gauges,
Map<MetricName, Counter> counters,
Map<MetricName, Histogram> histograms,
Map<MetricName, Meter> meters,
Map<MetricName, Timer> timers);
/**
* Don't use this method
*
* @deprecated use {@link #reportMetrics(Map, Map, Map, Map, Map)}
*/
@Override
@Deprecated
public final void report(SortedMap<String, Gauge> gauges,
SortedMap<String, Counter> counters,
SortedMap<String, Histogram> histograms,
SortedMap<String, Meter> meters,
SortedMap<String, Timer> timers) {
// intentionally left blank
}
/**
* Starts the reporter polling at the given period.
*
* @param period the amount of time between polls
* @param unit the unit for {@code period}
*/
public void start(long period, TimeUnit unit) {
synchronized (this) {
if (started) {
throw new IllegalStateException("This reporter has already been started");
}
final long periodInMS = unit.toMillis(period);
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
report();
} catch (RuntimeException ex) {
logger.error("RuntimeException thrown from {}#report. Exception was suppressed.", getClass().getSimpleName(), ex);
}
}
}, getOffsetUntilTimestampIsDivisableByPeriod(clock.getTime(), periodInMS), periodInMS, TimeUnit.MILLISECONDS);
this.clock = new QuantizedClock(clock, periodInMS);
this.started = true;
}
}
/*
* Makes sure that values are always submitted at the same time on each server no matter when they where started (aka. quantization)
*
* See https://blog.raintank.io/how-to-effectively-use-the-elasticsearch-data-source-and-solutions-to-common-pitfalls/#incomplete
* and also https://blog.raintank.io/25-graphite-grafana-and-statsd-gotchas/#graphite.quantization
*/
public static long getOffsetUntilTimestampIsDivisableByPeriod(long currentTimestamp, long periodInMS) {
return periodInMS - (currentTimestamp % periodInMS);
}
public abstract static class Builder<R extends ScheduledMetrics2Reporter, B extends Builder> {
private final Metric2Registry registry;
private final ScheduledExecutorService executor;
private Metric2Filter filter = Metric2Filter.ALL;
private TimeUnit rateUnit = TimeUnit.SECONDS;
private TimeUnit durationUnit = TimeUnit.MILLISECONDS;
private Clock clock = Clock.defaultClock();
private Map<String, String> globalTags = Collections.emptyMap();
protected Builder(Metric2Registry registry, String reporterName) {
this.executor = Executors.newSingleThreadScheduledExecutor(new ExecutorUtils.NamedThreadFactory(reporterName));
this.registry = registry;
}
protected Builder(Metric2Registry registry, ScheduledExecutorService executor) {
this.executor = executor;
this.registry = registry;
}
public Metric2Registry getRegistry() {
return registry;
}
public Metric2Filter getFilter() {
return filter;
}
public TimeUnit getRateUnit() {
return rateUnit;
}
public TimeUnit getDurationUnit() {
return durationUnit;
}
public ScheduledExecutorService getExecutor() {
return executor;
}
/**
* Only report metrics which match the given filter.
*
* @param filter a {@link com.codahale.metrics.MetricFilter}
* @return {@code this}
*/
public B filter(Metric2Filter filter) {
this.filter = filter;
return (B) this;
}
/**
* Convert rates to the given time unit.
*
* @param rateUnit a unit of time
* @return {@code this}
*/
public B convertRatesTo(TimeUnit rateUnit) {
this.rateUnit = rateUnit;
return (B) this;
}
/**
* Convert durations to the given time unit.
*
* @param durationUnit a unit of time
* @return {@code this}
*/
public B convertDurationsTo(TimeUnit durationUnit) {
this.durationUnit = durationUnit;
return (B) this;
}
public Clock getClock() {
return clock;
}
public B clock(Clock clock) {
this.clock = clock;
return (B) this;
}
public Map<String, String> getGlobalTags() {
return globalTags;
}
public B globalTags(Map<String, String> globalTags) {
this.globalTags = Collections.unmodifiableMap(new LinkedHashMap<String, String>(globalTags));
return (B) this;
}
/**
* Builds a reporter with the given properties.
*
* @return a reporter
*/
public abstract R build();
}
}