A comprehensive metrics collection library supporting Gauges, Counters, Timers, Histograms, and Meters with multiple reporting backends and advanced features.
Complete Implementation
1. Core Metrics Interfaces and Base Classes
package com.metrics.core;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.time.Instant;
import java.io.Closeable;
/**
* Core metrics interfaces and base implementation
*/
public interface Metric {
String getName();
Map<String, String> getTags();
MetricType getType();
Instant getCreatedTime();
}
public enum MetricType {
COUNTER, GAUGE, TIMER, HISTOGRAM, METER, SET
}
public interface Countable {
long getCount();
}
/**
* Base metric class
*/
public abstract class AbstractMetric implements Metric {
protected final String name;
protected final Map<String, String> tags;
protected final Instant createdTime;
protected final MetricType type;
public AbstractMetric(String name, Map<String, String> tags, MetricType type) {
this.name = name;
this.tags = tags != null ? new ConcurrentHashMap<>(tags) : new ConcurrentHashMap<>();
this.createdTime = Instant.now();
this.type = type;
}
@Override
public String getName() { return name; }
@Override
public Map<String, String> getTags() { return new HashMap<>(tags); }
@Override
public MetricType getType() { return type; }
@Override
public Instant getCreatedTime() { return createdTime; }
public void addTag(String key, String value) {
tags.put(key, value);
}
public void removeTag(String key) {
tags.remove(key);
}
}
2. Counter Implementation
/**
* Counter metric - monotonically increasing value
*/
public class Counter extends AbstractMetric implements Countable {
private final AtomicLong count;
private final AtomicLong lastUpdated;
public Counter(String name) {
this(name, null);
}
public Counter(String name, Map<String, String> tags) {
super(name, tags, MetricType.COUNTER);
this.count = new AtomicLong(0);
this.lastUpdated = new AtomicLong(System.currentTimeMillis());
}
public void increment() {
increment(1);
}
public void increment(long delta) {
if (delta < 0) {
throw new IllegalArgumentException("Counter can only be incremented by non-negative values");
}
count.addAndGet(delta);
lastUpdated.set(System.currentTimeMillis());
}
public void decrement() {
decrement(1);
}
public void decrement(long delta) {
if (delta < 0) {
throw new IllegalArgumentException("Counter can only be decremented by non-negative values");
}
count.addAndGet(-delta);
lastUpdated.set(System.currentTimeMillis());
}
public void reset() {
count.set(0);
lastUpdated.set(System.currentTimeMillis());
}
@Override
public long getCount() {
return count.get();
}
public long getLastUpdated() {
return lastUpdated.get();
}
public CounterSnapshot getSnapshot() {
return new CounterSnapshot(getCount(), Instant.ofEpochMilli(lastUpdated.get()));
}
/**
* Immutable counter snapshot
*/
public static class CounterSnapshot {
private final long count;
private final Instant timestamp;
public CounterSnapshot(long count, Instant timestamp) {
this.count = count;
this.timestamp = timestamp;
}
public long getCount() { return count; }
public Instant getTimestamp() { return timestamp; }
}
}
3. Gauge Implementation
/**
* Gauge metric - instantaneous measurement of a value
*/
public class Gauge extends AbstractMetric {
private final Callable<Double> valueSupplier;
private volatile double lastValue;
private final AtomicLong lastUpdated;
public Gauge(String name, Callable<Double> valueSupplier) {
this(name, valueSupplier, null);
}
public Gauge(String name, Callable<Double> valueSupplier, Map<String, String> tags) {
super(name, tags, MetricType.GAUGE);
this.valueSupplier = valueSupplier;
this.lastUpdated = new AtomicLong(System.currentTimeMillis());
this.lastValue = Double.NaN;
}
public double getValue() {
try {
double value = valueSupplier.call();
lastValue = value;
lastUpdated.set(System.currentTimeMillis());
return value;
} catch (Exception e) {
System.err.println("Error getting gauge value for " + name + ": " + e.getMessage());
return lastValue;
}
}
public long getLastUpdated() {
return lastUpdated.get();
}
public GaugeSnapshot getSnapshot() {
return new GaugeSnapshot(getValue(), Instant.ofEpochMilli(lastUpdated.get()));
}
/**
* Simple number gauge
*/
public static class NumberGauge extends Gauge {
private final AtomicDouble value;
public NumberGauge(String name) {
this(name, null);
}
public NumberGauge(String name, Map<String, String> tags) {
super(name, () -> 0.0, tags);
this.value = new AtomicDouble(0.0);
// Override the value supplier to use our atomic double
try {
var field = Gauge.class.getDeclaredField("valueSupplier");
field.setAccessible(true);
field.set(this, (Callable<Double>) value::get);
} catch (Exception e) {
throw new RuntimeException("Failed to create NumberGauge", e);
}
}
public void setValue(double newValue) {
value.set(newValue);
}
public double getCurrentValue() {
return value.get();
}
}
/**
* Cached gauge that updates value on demand but caches for a period
*/
public static class CachedGauge extends Gauge {
private final long cacheTimeoutMs;
private volatile double cachedValue;
private volatile long lastCacheUpdate;
public CachedGauge(String name, Callable<Double> valueSupplier, long cacheTimeoutMs) {
this(name, valueSupplier, cacheTimeoutMs, null);
}
public CachedGauge(String name, Callable<Double> valueSupplier,
long cacheTimeoutMs, Map<String, String> tags) {
super(name, valueSupplier, tags);
this.cacheTimeoutMs = cacheTimeoutMs;
this.cachedValue = Double.NaN;
this.lastCacheUpdate = 0;
}
@Override
public double getValue() {
long now = System.currentTimeMillis();
if (now - lastCacheUpdate > cacheTimeoutMs) {
try {
cachedValue = valueSupplier.call();
lastCacheUpdate = now;
} catch (Exception e) {
System.err.println("Error updating cached gauge " + getName() + ": " + e.getMessage());
}
}
return cachedValue;
}
}
/**
* Immutable gauge snapshot
*/
public static class GaugeSnapshot {
private final double value;
private final Instant timestamp;
public GaugeSnapshot(double value, Instant timestamp) {
this.value = value;
this.timestamp = timestamp;
}
public double getValue() { return value; }
public Instant getTimestamp() { return timestamp; }
}
}
4. Timer Implementation
/**
* Timer metric - measures duration and rate of events
*/
public class Timer extends AbstractMetric implements Countable {
private final Histogram histogram;
private final Meter meter;
private final Clock clock;
public Timer(String name) {
this(name, null, new ExponentiallyDecayingReservoir());
}
public Timer(String name, Map<String, String> tags) {
this(name, tags, new ExponentiallyDecayingReservoir());
}
public Timer(String name, Map<String, String> tags, Reservoir reservoir) {
super(name, tags, MetricType.TIMER);
this.histogram = new Histogram(name + ".histogram", tags, reservoir);
this.meter = new Meter(name + ".meter", tags);
this.clock = Clock.DEFAULT;
}
public TimerContext start() {
return new TimerContext(this, clock.getTick());
}
public long time(Runnable operation) {
long startTime = clock.getTick();
try {
operation.run();
} finally {
update(clock.getTick() - startTime);
}
return clock.getTick() - startTime;
}
public <T> T time(Callable<T> operation) {
long startTime = clock.getTick();
try {
return operation.call();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
update(clock.getTick() - startTime);
}
}
public void update(long duration, TimeUnit unit) {
update(unit.toNanos(duration));
}
public void update(long durationNanos) {
if (durationNanos >= 0) {
histogram.update(durationNanos);
meter.mark();
}
}
public double getFifteenMinuteRate() {
return meter.getFifteenMinuteRate();
}
public double getFiveMinuteRate() {
return meter.getFiveMinuteRate();
}
public double getOneMinuteRate() {
return meter.getOneMinuteRate();
}
public double getMeanRate() {
return meter.getMeanRate();
}
public Snapshot getSnapshot() {
return histogram.getSnapshot();
}
@Override
public long getCount() {
return histogram.getCount();
}
public long getSum() {
return histogram.getSum();
}
public double getMean() {
return histogram.getMean();
}
public double getStdDev() {
return histogram.getStdDev();
}
public double getMax() {
return histogram.getMax();
}
public double getMin() {
return histogram.getMin();
}
/**
* Timer context for timing blocks of code
*/
public static class TimerContext implements Closeable {
private final Timer timer;
private final long startTime;
private volatile boolean stopped;
public TimerContext(Timer timer, long startTime) {
this.timer = timer;
this.startTime = startTime;
this.stopped = false;
}
public long stop() {
if (!stopped) {
stopped = true;
long elapsed = timer.clock.getTick() - startTime;
timer.update(elapsed);
return elapsed;
}
return 0;
}
@Override
public void close() {
stop();
}
}
public TimerSnapshot getTimerSnapshot() {
return new TimerSnapshot(
getCount(),
getSum(),
getMean(),
getStdDev(),
getMin(),
getMax(),
getSnapshot(),
getMeanRate(),
getOneMinuteRate(),
getFiveMinuteRate(),
getFifteenMinuteRate(),
Instant.now()
);
}
/**
* Immutable timer snapshot
*/
public static class TimerSnapshot {
private final long count;
private final long sum;
private final double mean;
private final double stdDev;
private final double min;
private final double max;
private final Snapshot snapshot;
private final double meanRate;
private final double oneMinuteRate;
private final double fiveMinuteRate;
private final double fifteenMinuteRate;
private final Instant timestamp;
public TimerSnapshot(long count, long sum, double mean, double stdDev,
double min, double max, Snapshot snapshot,
double meanRate, double oneMinuteRate,
double fiveMinuteRate, double fifteenMinuteRate,
Instant timestamp) {
this.count = count;
this.sum = sum;
this.mean = mean;
this.stdDev = stdDev;
this.min = min;
this.max = max;
this.snapshot = snapshot;
this.meanRate = meanRate;
this.oneMinuteRate = oneMinuteRate;
this.fiveMinuteRate = fiveMinuteRate;
this.fifteenMinuteRate = fifteenMinuteRate;
this.timestamp = timestamp;
}
// Getters
public long getCount() { return count; }
public long getSum() { return sum; }
public double getMean() { return mean; }
public double getStdDev() { return stdDev; }
public double getMin() { return min; }
public double getMax() { return max; }
public Snapshot getSnapshot() { return snapshot; }
public double getMeanRate() { return meanRate; }
public double getOneMinuteRate() { return oneMinuteRate; }
public double getFiveMinuteRate() { return fiveMinuteRate; }
public double getFifteenMinuteRate() { return fifteenMinuteRate; }
public Instant getTimestamp() { return timestamp; }
}
}
5. Histogram Implementation
/**
* Histogram metric - statistical distribution of values
*/
public class Histogram extends AbstractMetric implements Countable {
private final Reservoir reservoir;
private final AtomicLong count;
private final AtomicLong sum;
public Histogram(String name, Reservoir reservoir) {
this(name, null, reservoir);
}
public Histogram(String name, Map<String, String> tags, Reservoir reservoir) {
super(name, tags, MetricType.HISTOGRAM);
this.reservoir = reservoir;
this.count = new AtomicLong(0);
this.sum = new AtomicLong(0);
}
public void update(int value) {
update((long) value);
}
public void update(long value) {
count.incrementAndGet();
sum.addAndGet(value);
reservoir.update(value);
}
public void update(double value) {
count.incrementAndGet();
sum.addAndGet((long) value);
reservoir.update((long) value);
}
@Override
public long getCount() {
return count.get();
}
public long getSum() {
return sum.get();
}
public double getMean() {
long currentCount = count.get();
if (currentCount == 0) {
return 0.0;
}
return (double) sum.get() / currentCount;
}
public double getStdDev() {
Snapshot snapshot = getSnapshot();
return snapshot.getStdDev();
}
public double getMax() {
Snapshot snapshot = getSnapshot();
return snapshot.getMax();
}
public double getMin() {
Snapshot snapshot = getSnapshot();
return snapshot.getMin();
}
public Snapshot getSnapshot() {
return reservoir.getSnapshot();
}
public HistogramSnapshot getHistogramSnapshot() {
return new HistogramSnapshot(
getCount(),
getSum(),
getMean(),
getStdDev(),
getMin(),
getMax(),
getSnapshot(),
Instant.now()
);
}
/**
* Immutable histogram snapshot
*/
public static class HistogramSnapshot {
private final long count;
private final long sum;
private final double mean;
private final double stdDev;
private final double min;
private final double max;
private final Snapshot snapshot;
private final Instant timestamp;
public HistogramSnapshot(long count, long sum, double mean, double stdDev,
double min, double max, Snapshot snapshot, Instant timestamp) {
this.count = count;
this.sum = sum;
this.mean = mean;
this.stdDev = stdDev;
this.min = min;
this.max = max;
this.snapshot = snapshot;
this.timestamp = timestamp;
}
// Getters
public long getCount() { return count; }
public long getSum() { return sum; }
public double getMean() { return mean; }
public double getStdDev() { return stdDev; }
public double getMin() { return min; }
public double getMax() { return max; }
public Snapshot getSnapshot() { return snapshot; }
public Instant getTimestamp() { return timestamp; }
}
}
6. Meter Implementation
/**
* Meter metric - measures rate of events
*/
public class Meter extends AbstractMetric implements Countable {
private static final long TICK_INTERVAL = TimeUnit.SECONDS.toNanos(5);
private static final int MAX_AGE = 15; // 15 minutes in ticks
private final AtomicLong count;
private final long startTime;
private final AtomicLong lastTick;
private final EWMA m1Rate; // 1-minute rate
private final EWMA m5Rate; // 5-minute rate
private final EWMA m15Rate; // 15-minute rate
private final Clock clock;
public Meter(String name) {
this(name, null);
}
public Meter(String name, Map<String, String> tags) {
super(name, tags, MetricType.METER);
this.count = new AtomicLong(0);
this.startTime = this.clock.getTick();
this.lastTick = new AtomicLong(startTime);
this.m1Rate = EWMA.oneMinuteEWMA();
this.m5Rate = EWMA.fiveMinuteEWMA();
this.m15Rate = EWMA.fifteenMinuteEWMA();
this.clock = Clock.DEFAULT;
}
public void mark() {
mark(1);
}
public void mark(long n) {
tickIfNecessary();
count.addAndGet(n);
m1Rate.update(n);
m5Rate.update(n);
m15Rate.update(n);
}
private void tickIfNecessary() {
long oldTick = lastTick.get();
long newTick = clock.getTick();
long age = newTick - oldTick;
if (age > TICK_INTERVAL) {
long newIntervalStartTick = newTick - age % TICK_INTERVAL;
if (lastTick.compareAndSet(oldTick, newIntervalStartTick)) {
long requiredTicks = age / TICK_INTERVAL;
for (long i = 0; i < requiredTicks; i++) {
m1Rate.tick();
m5Rate.tick();
m15Rate.tick();
}
}
}
}
@Override
public long getCount() {
return count.get();
}
public double getFifteenMinuteRate() {
tickIfNecessary();
return m15Rate.getRate(TimeUnit.SECONDS);
}
public double getFiveMinuteRate() {
tickIfNecessary();
return m5Rate.getRate(TimeUnit.SECONDS);
}
public double getOneMinuteRate() {
tickIfNecessary();
return m1Rate.getRate(TimeUnit.SECONDS);
}
public double getMeanRate() {
if (getCount() == 0) {
return 0.0;
}
long elapsed = clock.getTick() - startTime;
return convertNsRate(getCount() / (double) elapsed);
}
private double convertNsRate(double ratePerNs) {
return ratePerNs * (double) TimeUnit.SECONDS.toNanos(1);
}
public MeterSnapshot getMeterSnapshot() {
return new MeterSnapshot(
getCount(),
getMeanRate(),
getOneMinuteRate(),
getFiveMinuteRate(),
getFifteenMinuteRate(),
Instant.now()
);
}
/**
* Immutable meter snapshot
*/
public static class MeterSnapshot {
private final long count;
private final double meanRate;
private final double oneMinuteRate;
private final double fiveMinuteRate;
private final double fifteenMinuteRate;
private final Instant timestamp;
public MeterSnapshot(long count, double meanRate, double oneMinuteRate,
double fiveMinuteRate, double fifteenMinuteRate, Instant timestamp) {
this.count = count;
this.meanRate = meanRate;
this.oneMinuteRate = oneMinuteRate;
this.fiveMinuteRate = fiveMinuteRate;
this.fifteenMinuteRate = fifteenMinuteRate;
this.timestamp = timestamp;
}
// Getters
public long getCount() { return count; }
public double getMeanRate() { return meanRate; }
public double getOneMinuteRate() { return oneMinuteRate; }
public double getFiveMinuteRate() { return fiveMinuteRate; }
public double getFifteenMinuteRate() { return fifteenMinuteRate; }
public Instant getTimestamp() { return timestamp; }
}
}
7. Reservoir and Sampling Implementations
/**
* Reservoir interface for sampling data
*/
public interface Reservoir {
int size();
void update(long value);
Snapshot getSnapshot();
}
/**
* Statistical snapshot
*/
public interface Snapshot {
double getValue(double quantile);
long[] getValues();
int size();
long getMax();
long getMin();
double getMean();
double getStdDev();
double getMedian();
double get75thPercentile();
double get95thPercentile();
double get98thPercentile();
double get99thPercentile();
double get999thPercentile();
}
/**
* Exponentially decaying reservoir
*/
public class ExponentiallyDecayingReservoir implements Reservoir {
private static final int DEFAULT_SIZE = 1028;
private static final double DEFAULT_ALPHA = 0.015;
private static final long RESCALE_THRESHOLD = TimeUnit.HOURS.toNanos(1);
private final ConcurrentSkipListMap<Double, Long> values;
private final AtomicLong count;
private final double alpha;
private final int size;
private final AtomicLong startTime;
private final AtomicLong nextScaleTime;
private final Clock clock;
public ExponentiallyDecayingReservoir() {
this(DEFAULT_SIZE, DEFAULT_ALPHA);
}
public ExponentiallyDecayingReservoir(int size, double alpha) {
this.values = new ConcurrentSkipListMap<>();
this.count = new AtomicLong(0);
this.alpha = alpha;
this.size = size;
this.startTime = new AtomicLong(Clock.DEFAULT.getTick());
this.nextScaleTime = new AtomicLong(startTime.get() + RESCALE_THRESHOLD);
this.clock = Clock.DEFAULT;
}
@Override
public int size() {
return Math.min(size, (int) count.get());
}
@Override
public void update(long value) {
update(value, clock.getTick());
}
public void update(long value, long timestamp) {
rescaleIfNeeded();
double weight = weight(timestamp - startTime.get());
double priority = weight / ThreadLocalRandom.current().nextDouble();
long newCount = count.incrementAndGet();
if (newCount <= size) {
values.put(priority, value);
} else {
Double first = values.firstKey();
if (first < priority && values.putIfAbsent(priority, value) == null) {
while (values.remove(first) == null) {
first = values.firstKey();
}
}
}
}
private double weight(long time) {
return Math.exp(alpha * time);
}
private void rescaleIfNeeded() {
long now = clock.getTick();
long next = nextScaleTime.get();
if (now >= next) {
rescale(now, next);
}
}
private void rescale(long now, long next) {
if (nextScaleTime.compareAndSet(next, now + RESCALE_THRESHOLD)) {
long oldStartTime = startTime.get();
startTime.set(now);
double scalingFactor = Math.exp(-alpha * (now - oldStartTime));
if (scalingFactor != 0) {
ArrayList<Double> keys = new ArrayList<>(values.keySet());
for (Double key : keys) {
Long value = values.remove(key);
if (value != null) {
values.put(key * scalingFactor, value);
}
}
}
}
}
@Override
public Snapshot getSnapshot() {
rescaleIfNeeded();
return new WeightedSnapshot(values);
}
}
/**
* Weighted snapshot implementation
*/
public class WeightedSnapshot implements Snapshot {
private final long[] values;
private final double[] weights;
private final double[] quantiles;
public WeightedSnapshot(ConcurrentSkipListMap<Double, Long> values) {
List<Long> valueList = new ArrayList<>(values.values());
List<Double> weightList = new ArrayList<>(values.keySet());
this.values = new long[valueList.size()];
this.weights = new double[weightList.size()];
this.quantiles = new double[valueList.size()];
// Sort by value
List<Pair<Long, Double>> pairs = new ArrayList<>();
for (int i = 0; i < valueList.size(); i++) {
pairs.add(new Pair<>(valueList.get(i), weightList.get(i)));
}
pairs.sort(Comparator.comparing(Pair::getKey));
double sum = 0.0;
for (int i = 0; i < pairs.size(); i++) {
Pair<Long, Double> pair = pairs.get(i);
this.values[i] = pair.getKey();
this.weights[i] = pair.getValue();
sum += pair.getValue();
}
// Normalize weights and calculate quantiles
double runningSum = 0.0;
for (int i = 0; i < weights.length; i++) {
weights[i] /= sum;
runningSum += weights[i];
quantiles[i] = runningSum;
}
}
@Override
public double getValue(double quantile) {
if (quantile < 0.0 || quantile > 1.0 || values.length == 0) {
return 0.0;
}
if (values.length == 1) {
return values[0];
}
for (int i = 0; i < quantiles.length; i++) {
if (quantiles[i] >= quantile) {
return values[i];
}
}
return values[values.length - 1];
}
@Override
public long[] getValues() {
return Arrays.copyOf(values, values.length);
}
@Override
public int size() {
return values.length;
}
@Override
public long getMax() {
if (values.length == 0) return 0;
long max = values[0];
for (long value : values) {
if (value > max) max = value;
}
return max;
}
@Override
public long getMin() {
if (values.length == 0) return 0;
long min = values[0];
for (long value : values) {
if (value < min) min = value;
}
return min;
}
@Override
public double getMean() {
if (values.length == 0) return 0.0;
double sum = 0.0;
double totalWeight = 0.0;
for (int i = 0; i < values.length; i++) {
sum += values[i] * weights[i];
totalWeight += weights[i];
}
return sum / totalWeight;
}
@Override
public double getStdDev() {
if (values.length <= 1) return 0.0;
double mean = getMean();
double variance = 0.0;
double totalWeight = 0.0;
for (int i = 0; i < values.length; i++) {
double diff = values[i] - mean;
variance += weights[i] * diff * diff;
totalWeight += weights[i];
}
return Math.sqrt(variance / totalWeight);
}
@Override
public double getMedian() {
return getValue(0.5);
}
@Override
public double get75thPercentile() {
return getValue(0.75);
}
@Override
public double get95thPercentile() {
return getValue(0.95);
}
@Override
public double get98thPercentile() {
return getValue(0.98);
}
@Override
public double get99thPercentile() {
return getValue(0.99);
}
@Override
public double get999thPercentile() {
return getValue(0.999);
}
private static class Pair<K, V> {
private final K key;
private final V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
}
/**
* EWMA (Exponentially Weighted Moving Average)
*/
public class EWMA {
private static final int INTERVAL = 5;
private static final double SECONDS_PER_MINUTE = 60.0;
private static final int ONE_MINUTE = 1;
private static final int FIVE_MINUTES = 5;
private static final int FIFTEEN_MINUTES = 15;
private static final double M1_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / ONE_MINUTE);
private static final double M5_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIVE_MINUTES);
private static final double M15_ALPHA = 1 - Math.exp(-INTERVAL / SECONDS_PER_MINUTE / FIFTEEN_MINUTES);
private volatile boolean initialized = false;
private volatile double rate = 0.0;
private final AtomicLong uncounted = new AtomicLong(0);
private final double alpha;
private final double interval;
public static EWMA oneMinuteEWMA() {
return new EWMA(M1_ALPHA, INTERVAL);
}
public static EWMA fiveMinuteEWMA() {
return new EWMA(M5_ALPHA, INTERVAL);
}
public static EWMA fifteenMinuteEWMA() {
return new EWMA(M15_ALPHA, INTERVAL);
}
public EWMA(double alpha, long interval) {
this.alpha = alpha;
this.interval = interval;
}
public void update(long n) {
uncounted.addAndGet(n);
}
public void tick() {
long count = uncounted.getAndSet(0);
double instantRate = count / interval;
if (initialized) {
rate += (alpha * (instantRate - rate));
} else {
rate = instantRate;
initialized = true;
}
}
public double getRate(TimeUnit rateUnit) {
return rate * (double) rateUnit.toNanos(1);
}
}
/**
* Clock abstraction for testing
*/
public interface Clock {
long getTick();
Clock DEFAULT = new Clock() {
@Override
public long getTick() {
return System.nanoTime();
}
};
}
/**
* Atomic double implementation
*/
class AtomicDouble extends Number {
private final AtomicLong value;
public AtomicDouble() {
this(0.0);
}
public AtomicDouble(double initialValue) {
value = new AtomicLong(Double.doubleToLongBits(initialValue));
}
public final void set(double newValue) {
value.set(Double.doubleToLongBits(newValue));
}
public final double get() {
return Double.longBitsToDouble(value.get());
}
public final double getAndSet(double newValue) {
return Double.longBitsToDouble(value.getAndSet(Double.doubleToLongBits(newValue)));
}
public final boolean compareAndSet(double expect, double update) {
return value.compareAndSet(Double.doubleToLongBits(expect),
Double.doubleToLongBits(update));
}
@Override
public int intValue() {
return (int) get();
}
@Override
public long longValue() {
return (long) get();
}
@Override
public float floatValue() {
return (float) get();
}
@Override
public double doubleValue() {
return get();
}
}
8. Metrics Registry and Management
/**
* Central metrics registry
*/
public class MetricsRegistry {
private final ConcurrentMap<String, Metric> metrics;
private final List<MetricsReporter> reporters;
private final ScheduledExecutorService scheduler;
private static volatile MetricsRegistry instance;
private MetricsRegistry() {
this.metrics = new ConcurrentHashMap<>();
this.reporters = new CopyOnWriteArrayList<>();
this.scheduler = Executors.newScheduledThreadPool(2);
}
public static MetricsRegistry getInstance() {
if (instance == null) {
synchronized (MetricsRegistry.class) {
if (instance == null) {
instance = new MetricsRegistry();
}
}
}
return instance;
}
public Counter counter(String name) {
return counter(name, null);
}
public Counter counter(String name, Map<String, String> tags) {
return (Counter) metrics.computeIfAbsent(
getKey(name, tags),
k -> new Counter(name, tags)
);
}
public Gauge gauge(String name, Callable<Double> valueSupplier) {
return gauge(name, valueSupplier, null);
}
public Gauge gauge(String name, Callable<Double> valueSupplier, Map<String, String> tags) {
return (Gauge) metrics.computeIfAbsent(
getKey(name, tags),
k -> new Gauge(name, valueSupplier, tags)
);
}
public Gauge.NumberGauge numberGauge(String name) {
return numberGauge(name, null);
}
public Gauge.NumberGauge numberGauge(String name, Map<String, String> tags) {
return (Gauge.NumberGauge) metrics.computeIfAbsent(
getKey(name, tags),
k -> new Gauge.NumberGauge(name, tags)
);
}
public Timer timer(String name) {
return timer(name, null);
}
public Timer timer(String name, Map<String, String> tags) {
return (Timer) metrics.computeIfAbsent(
getKey(name, tags),
k -> new Timer(name, tags)
);
}
public Histogram histogram(String name) {
return histogram(name, null);
}
public Histogram histogram(String name, Map<String, String> tags) {
return (Histogram) metrics.computeIfAbsent(
getKey(name, tags),
k -> new Histogram(name, tags, new ExponentiallyDecayingReservoir())
);
}
public Meter meter(String name) {
return meter(name, null);
}
public Meter meter(String name, Map<String, String> tags) {
return (Meter) metrics.computeIfAbsent(
getKey(name, tags),
k -> new Meter(name, tags)
);
}
public void remove(String name) {
metrics.remove(name);
}
public void remove(String name, Map<String, String> tags) {
metrics.remove(getKey(name, tags));
}
public Map<String, Metric> getMetrics() {
return new HashMap<>(metrics);
}
public <T extends Metric> T getMetric(String name, Class<T> metricClass) {
return getMetric(name, null, metricClass);
}
@SuppressWarnings("unchecked")
public <T extends Metric> T getMetric(String name, Map<String, String> tags, Class<T> metricClass) {
Metric metric = metrics.get(getKey(name, tags));
if (metric != null && metricClass.isInstance(metric)) {
return (T) metric;
}
return null;
}
public void addReporter(MetricsReporter reporter) {
reporters.add(reporter);
}
public void startReporters(long interval, TimeUnit unit) {
for (MetricsReporter reporter : reporters) {
scheduler.scheduleAtFixedRate(() -> {
try {
reporter.report(getMetrics());
} catch (Exception e) {
System.err.println("Error in metrics reporter: " + e.getMessage());
}
}, 0, interval, unit);
}
}
public void stopReporters() {
scheduler.shutdown();
}
private String getKey(String name, Map<String, String> tags) {
if (tags == null || tags.isEmpty()) {
return name;
}
StringBuilder key = new StringBuilder(name);
tags.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> key.append(";").append(entry.getKey()).append("=").append(entry.getValue()));
return key.toString();
}
}
/**
* Metrics reporter interface
*/
public interface MetricsReporter {
void report(Map<String, Metric> metrics);
String getName();
}
/**
* Console metrics reporter
*/
public class ConsoleReporter implements MetricsReporter {
private final PrintStream output;
public ConsoleReporter() {
this(System.out);
}
public ConsoleReporter(PrintStream output) {
this.output = output;
}
@Override
public void report(Map<String, Metric> metrics) {
output.println("=== Metrics Report ===");
output.printf("%-40s %-10s %-20s%n", "Name", "Type", "Value");
output.println("----------------------------------------");
for (Map.Entry<String, Metric> entry : metrics.entrySet()) {
Metric metric = entry.getValue();
String value = getMetricValue(metric);
output.printf("%-40s %-10s %-20s%n",
metric.getName(), metric.getType(), value);
}
output.println();
}
private String getMetricValue(Metric metric) {
switch (metric.getType()) {
case COUNTER:
return String.valueOf(((Counter) metric).getCount());
case GAUGE:
return String.format("%.2f", ((Gauge) metric).getValue());
case TIMER:
Timer timer = (Timer) metric;
return String.format("count=%d, mean=%.2fms",
timer.getCount(), timer.getMean() / 1_000_000.0);
case HISTOGRAM:
Histogram histogram = (Histogram) metric;
return String.format("count=%d, mean=%.2f",
histogram.getCount(), histogram.getMean());
case METER:
Meter meter = (Meter) metric;
return String.format("count=%d, rate=%.2f/s",
meter.getCount(), meter.getOneMinuteRate());
default:
return "N/A";
}
}
@Override
public String getName() {
return "ConsoleReporter";
}
}
/**
* JMX metrics reporter
*/
public class JmxReporter implements MetricsReporter {
private final MBeanServer mBeanServer;
private final Map<String, ObjectName> registeredBeans;
public JmxReporter() {
this.mBeanServer = ManagementFactory.getPlatformMBeanServer();
this.registeredBeans = new ConcurrentHashMap<>();
}
@Override
public void report(Map<String, Metric> metrics) {
for (Map.Entry<String, Metric> entry : metrics.entrySet()) {
String key = entry.getKey();
Metric metric = entry.getValue();
try {
ObjectName objectName = getObjectName(metric);
if (!registeredBeans.containsKey(key)) {
mBeanServer.registerMBean(createMBean(metric), objectName);
registeredBeans.put(key, objectName);
}
} catch (Exception e) {
System.err.println("Error registering JMX bean for metric " + key + ": " + e.getMessage());
}
}
// Clean up removed metrics
Set<String> currentKeys = new HashSet<>(metrics.keySet());
for (String key : registeredBeans.keySet()) {
if (!currentKeys.contains(key)) {
try {
mBeanServer.unregisterMBean(registeredBeans.get(key));
registeredBeans.remove(key);
} catch (Exception e) {
System.err.println("Error unregistering JMX bean: " + e.getMessage());
}
}
}
}
private ObjectName getObjectName(Metric metric) throws Exception {
String domain = "com.metrics";
String name = metric.getName().replaceAll("[^a-zA-Z0-9-_]", ".");
Map<String, String> tags = metric.getTags();
StringBuilder objectName = new StringBuilder(domain).append(":type=").append(metric.getType());
objectName.append(",name=").append(name);
for (Map.Entry<String, String> tag : tags.entrySet()) {
objectName.append(",").append(tag.getKey()).append("=").append(tag.getValue());
}
return new ObjectName(objectName.toString());
}
private Object createMBean(Metric metric) {
// Create appropriate MBean based on metric type
// This is a simplified implementation
return new StandardMBean(metric, Metric.class);
}
@Override
public String getName() {
return "JmxReporter";
}
}
9. Usage Examples and Demo
/**
* Demo and usage examples
*/
public class MetricsDemo {
public static void main(String[] args) throws Exception {
MetricsRegistry registry = MetricsRegistry.getInstance();
// Add reporters
registry.addReporter(new ConsoleReporter());
registry.addReporter(new JmxReporter());
// Start reporting every 10 seconds
registry.startReporters(10, TimeUnit.SECONDS);
// Create different types of metrics
Counter requestCounter = registry.counter("http.requests",
Map.of("method", "GET", "status", "200"));
Gauge memoryGauge = registry.gauge("jvm.memory.used", () ->
(double) (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
Timer requestTimer = registry.timer("http.request.duration");
Meter errorMeter = registry.meter("http.errors");
Histogram responseSizeHistogram = registry.histogram("http.response.size");
// Simulate some activity
simulateWebTraffic(requestCounter, requestTimer, errorMeter, responseSizeHistogram);
// Let it run for a while
Thread.sleep(60000);
// Stop reporters
registry.stopReporters();
}
private static void simulateWebTraffic(Counter counter, Timer timer, Meter errorMeter,
Histogram responseSizeHistogram) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);
// Simulate successful requests
executor.scheduleAtFixedRate(() -> {
try (Timer.TimerContext context = timer.start()) {
counter.increment();
responseSizeHistogram.update(ThreadLocalRandom.current().nextInt(100, 5000));
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 200));
} catch (Exception e) {
errorMeter.mark();
}
}, 0, 100, TimeUnit.MILLISECONDS);
// Simulate occasional errors
executor.scheduleAtFixedRate(() -> {
if (ThreadLocalRandom.current().nextDouble() < 0.1) {
errorMeter.mark();
}
}, 0, 1, TimeUnit.SECONDS);
}
}
/**
* Builder for metric names with tags
*/
public class MetricBuilder {
private final String name;
private final Map<String, String> tags;
private MetricBuilder(String name) {
this.name = name;
this.tags = new HashMap<>();
}
public static MetricBuilder name(String name) {
return new MetricBuilder(name);
}
public MetricBuilder tag(String key, String value) {
tags.put(key, value);
return this;
}
public Counter counter() {
return MetricsRegistry.getInstance().counter(name, tags);
}
public Timer timer() {
return MetricsRegistry.getInstance().timer(name, tags);
}
public Meter meter() {
return MetricsRegistry.getInstance().meter(name, tags);
}
public Histogram histogram() {
return MetricsRegistry.getInstance().histogram(name, tags);
}
}
// Example usage of MetricBuilder
class ApplicationMetrics {
private static final Counter USER_LOGINS = MetricBuilder.name("user.logins")
.tag("component", "auth")
.counter();
private static final Timer DB_QUERY_TIMER = MetricBuilder.name("db.query.duration")
.tag("component", "database")
.timer();
public void userLogin(String username) {
USER_LOGINS.increment();
try (Timer.TimerContext context = DB_QUERY_TIMER.start()) {
// Perform database query
Thread.sleep(50);
} catch (Exception e) {
// Handle error
}
}
}
Features
- Multiple Metric Types: Counters, Gauges, Timers, Histograms, Meters
- High Performance: Atomic operations, efficient data structures
- Statistical Sampling: Exponentially decaying reservoirs for percentiles
- Tag Support: Key-value tags for dimensional metrics
- Multiple Reporting: Console, JMX, and extensible reporter interface
- Thread-Safe: All operations are thread-safe
- Memory Efficient: Reservoir sampling for high-cardinality data
- EWMA Rates: Exponentially weighted moving averages for meter rates
Usage Examples
Basic Counter
Counter counter = MetricsRegistry.getInstance().counter("requests");
counter.increment();
Timer for Code Blocks
Timer timer = MetricsRegistry.getInstance().timer("operation.duration");
try (Timer.TimerContext context = timer.start()) {
// Your code here
}
Gauge for Dynamic Values
Gauge gauge = MetricsRegistry.getInstance().gauge("queue.size",
() -> (double) queue.size());
Meter for Rate Measurement
Meter meter = MetricsRegistry.getInstance().meter("errors");
meter.mark(); // When error occurs
This comprehensive metrics library provides production-ready instrumentation for Java applications with support for all standard metric types and multiple reporting backends.