google-site-verification: google61fe8ba583a51912.html
Metrics Collection Library in Java

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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper