1. Core Metrics Interfaces and Base Classes
// Metric.java package com.metrics.core; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; public interface Metric { String getName(); Map<String, String> getTags(); MetricType getType(); void reset(); enum MetricType { COUNTER, GAUGE, HISTOGRAM, SUMMARY } } // TaggedMetric.java package com.metrics.core; import java.util.Collections; import java.util.Map; import java.util.Objects; public class TaggedMetric { private final String name; private final Map<String, String> tags; public TaggedMetric(String name, Map<String, String> tags) { this.name = Objects.requireNonNull(name); this.tags = tags != null ? Collections.unmodifiableMap(tags) : Collections.emptyMap(); } public String getName() { return name; } public Map<String, String> getTags() { return tags; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; TaggedMetric that = (TaggedMetric) o; return name.equals(that.name) && tags.equals(that.tags); } @Override public int hashCode() { return Objects.hash(name, tags); } @Override public String toString() { return "TaggedMetric{name='" + name + "', tags=" + tags + "}"; } } // BaseMetric.java package com.metrics.core; import java.time.Instant; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; public abstract class BaseMetric implements Metric { protected final String name; protected final Map<String, String> tags; protected final AtomicLong lastUpdated; protected BaseMetric(String name, Map<String, String> tags) { this.name = name; this.tags = new ConcurrentHashMap<>(tags != null ? tags : Map.of()); this.lastUpdated = new AtomicLong(System.currentTimeMillis()); } @Override public String getName() { return name; } @Override public Map<String, String> getTags() { return Map.copyOf(tags); } public long getLastUpdated() { return lastUpdated.get(); } public Instant getLastUpdatedInstant() { return Instant.ofEpochMilli(lastUpdated.get()); } protected void updateTimestamp() { lastUpdated.set(System.currentTimeMillis()); } public void setTag(String key, String value) { tags.put(key, value); updateTimestamp(); } public void removeTag(String key) { tags.remove(key); updateTimestamp(); } } 2. Histogram Implementation
// Histogram.java package com.metrics.histogram; import com.metrics.core.BaseMetric; import com.metrics.core.MetricType; import java.util.Arrays; import java.util.Map; import java.util.concurrent.atomic.AtomicLongArray; import java.util.concurrent.atomic.DoubleAdder; import java.util.concurrent.atomic.LongAdder; public class Histogram extends BaseMetric { private final double[] buckets; private final AtomicLongArray bucketCounts; private final LongAdder sum; private final LongAdder count; private final LongAdder min; private final LongAdder max; private final DoubleAdder sumOfSquares; // Common bucket configurations public static final double[] LATENCY_BUCKETS = { 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0, Double.MAX_VALUE }; public static final double[] REQUEST_SIZE_BUCKETS = { 100, 500, 1000, 5000, 10000, 50000, 100000, 500000, 1000000, 5000000, 10000000, Double.MAX_VALUE }; public static final double[] DEFAULT_BUCKETS = { 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, Double.MAX_VALUE }; public Histogram(String name) { this(name, DEFAULT_BUCKETS, Map.of()); } public Histogram(String name, double[] buckets) { this(name, buckets, Map.of()); } public Histogram(String name, double[] buckets, Map<String, String> tags) { super(name, tags); this.buckets = Arrays.copyOf(buckets, buckets.length); Arrays.sort(this.buckets); this.bucketCounts = new AtomicLongArray(this.buckets.length); this.sum = new LongAdder(); this.count = new LongAdder(); this.min = new LongAdder(); this.max = new LongAdder(); this.sumOfSquares = new DoubleAdder(); // Initialize min to max value and max to min value this.min.reset(); this.min.add(Long.MAX_VALUE); this.max.reset(); this.max.add(Long.MIN_VALUE); } public void observe(double value) { if (value < 0) { throw new IllegalArgumentException("Histogram values cannot be negative: " + value); } // Find the appropriate bucket int bucketIndex = findBucketIndex(value); if (bucketIndex >= 0 && bucketIndex < bucketCounts.length()) { bucketCounts.incrementAndGet(bucketIndex); } // Update statistics long longValue = (long) value; sum.add(longValue); count.increment(); sumOfSquares.add(value * value); // Update min and max updateMinMax(longValue); updateTimestamp(); } public void observeWithTimestamp(double value, long timestamp) { observe(value); lastUpdated.set(timestamp); } private int findBucketIndex(double value) { for (int i = 0; i < buckets.length; i++) { if (value <= buckets[i]) { return i; } } return buckets.length - 1; } private void updateMinMax(long value) { // Update min long currentMin; do { currentMin = min.longValue(); } while (value < currentMin && !min.compareAndSet(currentMin, value)); // Update max long currentMax; do { currentMax = max.longValue(); } while (value > currentMax && !max.compareAndSet(currentMax, value)); } public HistogramSnapshot getSnapshot() { return new HistogramSnapshot(this); } @Override public void reset() { for (int i = 0; i < bucketCounts.length(); i++) { bucketCounts.set(i, 0); } sum.reset(); count.reset(); sumOfSquares.reset(); min.reset(); min.add(Long.MAX_VALUE); max.reset(); max.add(Long.MIN_VALUE); updateTimestamp(); } @Override public MetricType getType() { return MetricType.HISTOGRAM; } // Getters for internal state (package-private for snapshot) double[] getBuckets() { return Arrays.copyOf(buckets, buckets.length); } long[] getBucketCounts() { long[] counts = new long[bucketCounts.length()]; for (int i = 0; i < counts.length; i++) { counts[i] = bucketCounts.get(i); } return counts; } long getSum() { return sum.longValue(); } long getCount() { return count.longValue(); } long getMin() { long minValue = min.longValue(); return minValue == Long.MAX_VALUE ? 0 : minValue; } long getMax() { long maxValue = max.longValue(); return maxValue == Long.MIN_VALUE ? 0 : maxValue; } double getSumOfSquares() { return sumOfSquares.doubleValue(); } public static class HistogramSnapshot { private final String name; private final Map<String, String> tags; private final long timestamp; private final double[] buckets; private final long[] bucketCounts; private final long sum; private final long count; private final long min; private final long max; private final double sumOfSquares; public HistogramSnapshot(Histogram histogram) { this.name = histogram.getName(); this.tags = histogram.getTags(); this.timestamp = histogram.getLastUpdated(); this.buckets = histogram.getBuckets(); this.bucketCounts = histogram.getBucketCounts(); this.sum = histogram.getSum(); this.count = histogram.getCount(); this.min = histogram.getMin(); this.max = histogram.getMax(); this.sumOfSquares = histogram.getSumOfSquares(); } public double getAverage() { return count > 0 ? (double) sum / count : 0.0; } public double getVariance() { if (count <= 1) return 0.0; double mean = getAverage(); return (sumOfSquares / count) - (mean * mean); } public double getStdDev() { return Math.sqrt(getVariance()); } public double getPercentile(double percentile) { if (count == 0) return 0.0; if (percentile < 0 || percentile > 1) { throw new IllegalArgumentException("Percentile must be between 0 and 1"); } long targetCount = (long) Math.ceil(percentile * count); long accumulated = 0; for (int i = 0; i < bucketCounts.length; i++) { accumulated += bucketCounts[i]; if (accumulated >= targetCount) { return buckets[i]; } } return max; } public Map<Double, Double> getPercentiles(double... percentiles) { Map<Double, Double> result = new java.util.TreeMap<>(); for (double p : percentiles) { result.put(p, getPercentile(p)); } return result; } // Getters public String getName() { return name; } public Map<String, String> getTags() { return tags; } public long getTimestamp() { return timestamp; } public double[] getBuckets() { return Arrays.copyOf(buckets, buckets.length); } public long[] getBucketCounts() { return Arrays.copyOf(bucketCounts, bucketCounts.length); } public long getSum() { return sum; } public long getCount() { return count; } public long getMin() { return min; } public long getMax() { return max; } public double getSumOfSquares() { return sumOfSquares; } @Override public String toString() { return String.format( "HistogramSnapshot{name='%s', count=%,d, min=%,d, max=%,d, avg=%.2f, p95=%.2f, p99=%.2f}", name, count, min, max, getAverage(), getPercentile(0.95), getPercentile(0.99) ); } } } 3. Summary Implementation
// Summary.java package com.metrics.summary; import com.metrics.core.BaseMetric; import com.metrics.core.MetricType; import java.util.Arrays; import java.util.Map; import java.util.concurrent.atomic.DoubleAdder; import java.util.concurrent.atomic.LongAdder; public class Summary extends BaseMetric { private final double[] quantiles; private final LongAdder count; private final DoubleAdder sum; private final LongAdder min; private final LongAdder max; private final DoubleAdder sumOfSquares; // Common quantile configurations public static final double[] DEFAULT_QUANTILES = {0.5, 0.75, 0.9, 0.95, 0.99, 0.999}; public static final double[] LATENCY_QUANTILES = {0.5, 0.9, 0.95, 0.99, 0.999}; public static final double[] SIMPLE_QUANTILES = {0.5, 0.9, 0.99}; // Circular buffer for recent values (simplified approach) private final double[] recentValues; private int recentIndex; private final int maxRecentValues; public Summary(String name) { this(name, DEFAULT_QUANTILES, 1000, Map.of()); } public Summary(String name, double[] quantiles) { this(name, quantiles, 1000, Map.of()); } public Summary(String name, double[] quantiles, int maxRecentValues, Map<String, String> tags) { super(name, tags); this.quantiles = Arrays.copyOf(quantiles, quantiles.length); Arrays.sort(this.quantiles); this.count = new LongAdder(); this.sum = new DoubleAdder(); this.min = new LongAdder(); this.max = new LongAdder(); this.sumOfSquares = new DoubleAdder(); this.maxRecentValues = maxRecentValues; this.recentValues = new double[maxRecentValues]; this.recentIndex = 0; // Initialize min/max this.min.reset(); this.min.add(Long.MAX_VALUE); this.max.reset(); this.max.add(Long.MIN_VALUE); } public void observe(double value) { if (value < 0) { throw new IllegalArgumentException("Summary values cannot be negative: " + value); } // Update statistics count.increment(); sum.add(value); sumOfSquares.add(value * value); // Update min/max updateMinMax((long) value); // Store recent value for quantile calculation storeRecentValue(value); updateTimestamp(); } private void storeRecentValue(double value) { recentValues[recentIndex] = value; recentIndex = (recentIndex + 1) % maxRecentValues; } private void updateMinMax(long value) { // Update min long currentMin; do { currentMin = min.longValue(); } while (value < currentMin && !min.compareAndSet(currentMin, value)); // Update max long currentMax; do { currentMax = max.longValue(); } while (value > currentMax && !max.compareAndSet(currentMax, value)); } public SummarySnapshot getSnapshot() { return new SummarySnapshot(this); } @Override public void reset() { count.reset(); sum.reset(); sumOfSquares.reset(); min.reset(); min.add(Long.MAX_VALUE); max.reset(); max.add(Long.MIN_VALUE); Arrays.fill(recentValues, 0.0); recentIndex = 0; updateTimestamp(); } @Override public MetricType getType() { return MetricType.SUMMARY; } // Getters for internal state double[] getQuantiles() { return Arrays.copyOf(quantiles, quantiles.length); } long getCount() { return count.longValue(); } double getSum() { return sum.doubleValue(); } long getMin() { long minValue = min.longValue(); return minValue == Long.MAX_VALUE ? 0 : minValue; } long getMax() { long maxValue = max.longValue(); return maxValue == Long.MIN_VALUE ? 0 : maxValue; } double getSumOfSquares() { return sumOfSquares.doubleValue(); } double[] getRecentValues() { double[] values = new double[Math.min(count.intValue(), maxRecentValues)]; System.arraycopy(recentValues, 0, values, 0, values.length); return values; } public static class SummarySnapshot { private final String name; private final Map<String, String> tags; private final long timestamp; private final double[] quantiles; private final long count; private final double sum; private final long min; private final long max; private final double sumOfSquares; private final double[] recentValues; public SummarySnapshot(Summary summary) { this.name = summary.getName(); this.tags = summary.getTags(); this.timestamp = summary.getLastUpdated(); this.quantiles = summary.getQuantiles(); this.count = summary.getCount(); this.sum = summary.getSum(); this.min = summary.getMin(); this.max = summary.getMax(); this.sumOfSquares = summary.getSumOfSquares(); this.recentValues = summary.getRecentValues(); } public double getAverage() { return count > 0 ? sum / count : 0.0; } public double getVariance() { if (count <= 1) return 0.0; double mean = getAverage(); return (sumOfSquares / count) - (mean * mean); } public double getStdDev() { return Math.sqrt(getVariance()); } public Map<Double, Double> getQuantileValues() { Map<Double, Double> result = new java.util.TreeMap<>(); if (count == 0) { for (double q : quantiles) { result.put(q, 0.0); } return result; } // Calculate quantiles from recent values double[] sortedValues = Arrays.copyOf(recentValues, Math.min((int) count, recentValues.length)); Arrays.sort(sortedValues); for (double quantile : quantiles) { if (sortedValues.length == 0) { result.put(quantile, 0.0); continue; } double pos = quantile * (sortedValues.length - 1); int lower = (int) Math.floor(pos); int upper = (int) Math.ceil(pos); if (lower == upper) { result.put(quantile, sortedValues[lower]); } else { double weight = pos - lower; double value = sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight; result.put(quantile, value); } } return result; } public double getQuantile(double quantile) { Map<Double, Double> quantiles = getQuantileValues(); return quantiles.getOrDefault(quantile, 0.0); } // Getters public String getName() { return name; } public Map<String, String> getTags() { return tags; } public long getTimestamp() { return timestamp; } public double[] getQuantiles() { return Arrays.copyOf(quantiles, quantiles.length); } public long getCount() { return count; } public double getSum() { return sum; } public long getMin() { return min; } public long getMax() { return max; } public double getSumOfSquares() { return sumOfSquares; } public double[] getRecentValues() { return Arrays.copyOf(recentValues, recentValues.length); } @Override public String toString() { Map<Double, Double> quantileValues = getQuantileValues(); return String.format( "SummarySnapshot{name='%s', count=%,d, min=%,d, max=%,d, avg=%.2f, q50=%.2f, q95=%.2f, q99=%.2f}", name, count, min, max, getAverage(), quantileValues.get(0.5), quantileValues.get(0.95), quantileValues.get(0.99) ); } } } 4. Metrics Registry
// MetricsRegistry.java package com.metrics.registry; import com.metrics.core.Metric; import com.metrics.core.TaggedMetric; import com.metrics.histogram.Histogram; import com.metrics.summary.Summary; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; public class MetricsRegistry { private static final MetricsRegistry INSTANCE = new MetricsRegistry(); private final Map<TaggedMetric, Metric> metrics; private final ScheduledExecutorService scheduler; private final List<MetricsReporter> reporters; private final AtomicLong lastCollection; private final RegistryConfig config; private MetricsRegistry() { this.metrics = new ConcurrentHashMap<>(); this.scheduler = Executors.newScheduledThreadPool(2); this.reporters = new ArrayList<>(); this.lastCollection = new AtomicLong(System.currentTimeMillis()); this.config = new RegistryConfig(); startPeriodicCollection(); } public static MetricsRegistry getInstance() { return INSTANCE; } public Histogram histogram(String name) { return histogram(name, Histogram.DEFAULT_BUCKETS, Map.of()); } public Histogram histogram(String name, double[] buckets) { return histogram(name, buckets, Map.of()); } public Histogram histogram(String name, double[] buckets, Map<String, String> tags) { TaggedMetric key = new TaggedMetric(name, tags); return (Histogram) metrics.computeIfAbsent(key, k -> new Histogram(name, buckets, tags)); } public Summary summary(String name) { return summary(name, Summary.DEFAULT_QUANTILES, 1000, Map.of()); } public Summary summary(String name, double[] quantiles) { return summary(name, quantiles, 1000, Map.of()); } public Summary summary(String name, double[] quantiles, int maxRecentValues, Map<String, String> tags) { TaggedMetric key = new TaggedMetric(name, tags); return (Summary) metrics.computeIfAbsent(key, k -> new Summary(name, quantiles, maxRecentValues, tags)); } public Optional<Metric> getMetric(String name, Map<String, String> tags) { return Optional.ofNullable(metrics.get(new TaggedMetric(name, tags))); } public List<Metric> getMetricsByName(String name) { return metrics.entrySet().stream() .filter(entry -> entry.getKey().getName().equals(name)) .map(Map.Entry::getValue) .collect(Collectors.toList()); } public List<Metric> getMetricsByTag(String tagKey, String tagValue) { return metrics.entrySet().stream() .filter(entry -> tagValue.equals(entry.getKey().getTags().get(tagKey))) .map(Map.Entry::getValue) .collect(Collectors.toList()); } public void removeMetric(String name, Map<String, String> tags) { metrics.remove(new TaggedMetric(name, tags)); } public void addReporter(MetricsReporter reporter) { reporters.add(reporter); } public void removeReporter(MetricsReporter reporter) { reporters.remove(reporter); } private void startPeriodicCollection() { scheduler.scheduleAtFixedRate(() -> { try { collectAndReport(); } catch (Exception e) { System.err.println("Error in metrics collection: " + e.getMessage()); } }, config.collectionIntervalSeconds, config.collectionIntervalSeconds, TimeUnit.SECONDS); } private void collectAndReport() { long collectionTime = System.currentTimeMillis(); Map<String, Object> collectedMetrics = collectMetrics(); lastCollection.set(collectionTime); for (MetricsReporter reporter : reporters) { try { reporter.report(collectedMetrics, collectionTime); } catch (Exception e) { System.err.println("Error in reporter " + reporter.getClass().getSimpleName() + ": " + e.getMessage()); } } } private Map<String, Object> collectMetrics() { Map<String, Object> collected = new HashMap<>(); for (Metric metric : metrics.values()) { if (metric instanceof Histogram) { collected.put(metric.getName() + ".histogram", ((Histogram) metric).getSnapshot()); } else if (metric instanceof Summary) { collected.put(metric.getName() + ".summary", ((Summary) metric).getSnapshot()); } } return collected; } public RegistryStats getStats() { long histograms = metrics.values().stream() .filter(m -> m instanceof Histogram) .count(); long summaries = metrics.values().stream() .filter(m -> m instanceof Summary) .count(); return new RegistryStats(metrics.size(), histograms, summaries, lastCollection.get()); } public void shutdown() { scheduler.shutdown(); try { if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) { scheduler.shutdownNow(); } } catch (InterruptedException e) { scheduler.shutdownNow(); Thread.currentThread().interrupt(); } } public static class RegistryConfig { public long collectionIntervalSeconds = 60; public boolean enableAutoCleanup = true; public long metricExpiryMinutes = 60; } public static class RegistryStats { public final long totalMetrics; public final long histogramCount; public final long summaryCount; public final long lastCollectionTime; public RegistryStats(long totalMetrics, long histogramCount, long summaryCount, long lastCollectionTime) { this.totalMetrics = totalMetrics; this.histogramCount = histogramCount; this.summaryCount = summaryCount; this.lastCollectionTime = lastCollectionTime; } @Override public String toString() { return String.format( "RegistryStats{total=%,d, histograms=%,d, summaries=%,d, lastCollection=%s}", totalMetrics, histogramCount, summaryCount, new Date(lastCollectionTime) ); } } } 5. Reporters and Exporters
// MetricsReporter.java package com.metrics.registry; import java.util.Map; public interface MetricsReporter { void report(Map<String, Object> metrics, long timestamp); } // ConsoleReporter.java package com.metrics.reporters; import com.metrics.registry.MetricsReporter; import com.metrics.histogram.Histogram; import com.metrics.summary.Summary; import java.util.Map; public class ConsoleReporter implements MetricsReporter { private final boolean verbose; public ConsoleReporter() { this(false); } public ConsoleReporter(boolean verbose) { this.verbose = verbose; } @Override public void report(Map<String, Object> metrics, long timestamp) { System.out.printf("\n=== Metrics Report - %s ===\n", new java.util.Date(timestamp)); System.out.printf("Total metrics: %d\n\n", metrics.size()); for (Map.Entry<String, Object> entry : metrics.entrySet()) { if (entry.getValue() instanceof Histogram.HistogramSnapshot) { printHistogram((Histogram.HistogramSnapshot) entry.getValue()); } else if (entry.getValue() instanceof Summary.SummarySnapshot) { printSummary((Summary.SummarySnapshot) entry.getValue()); } } } private void printHistogram(Histogram.HistogramSnapshot snapshot) { System.out.printf("Histogram: %s\n", snapshot.getName()); System.out.printf(" Count: %,d, Sum: %,d, Min: %,d, Max: %,d\n", snapshot.getCount(), snapshot.getSum(), snapshot.getMin(), snapshot.getMax()); System.out.printf(" Average: %.2f, StdDev: %.2f\n", snapshot.getAverage(), snapshot.getStdDev()); Map<Double, Double> percentiles = snapshot.getPercentiles(0.5, 0.75, 0.9, 0.95, 0.99); System.out.printf(" Percentiles: p50=%.2f, p75=%.2f, p90=%.2f, p95=%.2f, p99=%.2f\n", percentiles.get(0.5), percentiles.get(0.75), percentiles.get(0.9), percentiles.get(0.95), percentiles.get(0.99)); if (verbose) { System.out.println(" Bucket Distribution:"); double[] buckets = snapshot.getBuckets(); long[] counts = snapshot.getBucketCounts(); for (int i = 0; i < buckets.length; i++) { if (counts[i] > 0) { double percentage = (double) counts[i] / snapshot.getCount() * 100; System.out.printf(" ≤ %.2f: %,d (%.1f%%)\n", buckets[i], counts[i], percentage); } } } System.out.println(); } private void printSummary(Summary.SummarySnapshot snapshot) { System.out.printf("Summary: %s\n", snapshot.getName()); System.out.printf(" Count: %,d, Sum: %.2f, Min: %,d, Max: %,d\n", snapshot.getCount(), snapshot.getSum(), snapshot.getMin(), snapshot.getMax()); System.out.printf(" Average: %.2f, StdDev: %.2f\n", snapshot.getAverage(), snapshot.getStdDev()); Map<Double, Double> quantiles = snapshot.getQuantileValues(); System.out.printf(" Quantiles: q50=%.2f, q75=%.2f, q90=%.2f, q95=%.2f, q99=%.2f\n", quantiles.get(0.5), quantiles.get(0.75), quantiles.get(0.9), quantiles.get(0.95), quantiles.get(0.99)); System.out.println(); } } // JsonReporter.java package com.metrics.reporters; import com.metrics.registry.MetricsReporter; import com.metrics.histogram.Histogram; import com.metrics.summary.Summary; import java.util.Map; import java.util.LinkedHashMap; import java.util.List; import java.util.ArrayList; public class JsonReporter implements MetricsReporter { @Override public void report(Map<String, Object> metrics, long timestamp) { Map<String, Object> report = new LinkedHashMap<>(); report.put("timestamp", timestamp); report.put("format", "metrics-report"); List<Map<String, Object>> metricData = new ArrayList<>(); for (Map.Entry<String, Object> entry : metrics.entrySet()) { metricData.add(createMetricReport(entry.getKey(), entry.getValue())); } report.put("metrics", metricData); // In production, you'd use Jackson or GSON System.out.println("JSON Metrics: " + formatAsJson(report)); } private Map<String, Object> createMetricReport(String name, Object metric) { Map<String, Object> report = new LinkedHashMap<>(); report.put("name", name); if (metric instanceof Histogram.HistogramSnapshot) { report.putAll(createHistogramReport((Histogram.HistogramSnapshot) metric)); } else if (metric instanceof Summary.SummarySnapshot) { report.putAll(createSummaryReport((Summary.SummarySnapshot) metric)); } return report; } private Map<String, Object> createHistogramReport(Histogram.HistogramSnapshot snapshot) { Map<String, Object> report = new LinkedHashMap<>(); report.put("type", "histogram"); report.put("count", snapshot.getCount()); report.put("sum", snapshot.getSum()); report.put("min", snapshot.getMin()); report.put("max", snapshot.getMax()); report.put("average", snapshot.getAverage()); report.put("std_dev", snapshot.getStdDev()); report.put("percentiles", snapshot.getPercentiles(0.5, 0.75, 0.9, 0.95, 0.99)); return report; } private Map<String, Object> createSummaryReport(Summary.SummarySnapshot snapshot) { Map<String, Object> report = new LinkedHashMap<>(); report.put("type", "summary"); report.put("count", snapshot.getCount()); report.put("sum", snapshot.getSum()); report.put("min", snapshot.getMin()); report.put("max", snapshot.getMax()); report.put("average", snapshot.getAverage()); report.put("std_dev", snapshot.getStdDev()); report.put("quantiles", snapshot.getQuantileValues()); return report; } private String formatAsJson(Map<String, Object> data) { // Simplified JSON formatting StringBuilder sb = new StringBuilder(); sb.append("{"); formatJsonValue(sb, data); sb.append("}"); return sb.toString(); } private void formatJsonValue(StringBuilder sb, Object value) { if (value instanceof String) { sb.append("\"").append(escapeJson((String) value)).append("\""); } else if (value instanceof Number) { sb.append(value); } else if (value instanceof Map) { sb.append("{"); boolean first = true; for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) { if (!first) sb.append(", "); sb.append("\"").append(entry.getKey()).append("\": "); formatJsonValue(sb, entry.getValue()); first = false; } sb.append("}"); } else if (value instanceof List) { sb.append("["); boolean first = true; for (Object item : (List<?>) value) { if (!first) sb.append(", "); formatJsonValue(sb, item); first = false; } sb.append("]"); } else { sb.append("\"").append(String.valueOf(value)).append("\""); } } private String escapeJson(String str) { return str.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\b", "\\b") .replace("\f", "\\f") .replace("\n", "\\n") .replace("\r", "\\r") .replace("\t", "\\t"); } } 6. Usage Examples
// MetricsDemo.java package com.metrics.demo; import com.metrics.registry.MetricsRegistry; import com.metrics.histogram.Histogram; import com.metrics.summary.Summary; import com.metrics.reporters.ConsoleReporter; import com.metrics.reporters.JsonReporter; import java.util.Map; import java.util.Random; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class MetricsDemo { public static void main(String[] args) throws InterruptedException { MetricsRegistry registry = MetricsRegistry.getInstance(); // Add reporters registry.addReporter(new ConsoleReporter(true)); registry.addReporter(new JsonReporter()); // Create various metrics Histogram requestLatency = registry.histogram("http_request_duration_seconds", Histogram.LATENCY_BUCKETS, Map.of("service", "api-gateway")); Histogram responseSize = registry.histogram("http_response_size_bytes", Histogram.REQUEST_SIZE_BUCKETS, Map.of("service", "api-gateway")); Summary databaseQueryTime = registry.summary("database_query_duration_seconds", Summary.LATENCY_QUANTILES, 5000, Map.of("database", "users-db")); // Simulate metrics collection simulateRequestMetrics(requestLatency, responseSize); simulateDatabaseMetrics(databaseQueryTime); // Let it run for a while Thread.sleep(120000); // Print final stats System.out.println("Final registry stats: " + registry.getStats()); // Shutdown registry.shutdown(); } private static void simulateRequestMetrics(Histogram latency, Histogram size) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Random random = new Random(); scheduler.scheduleAtFixedRate(() -> { // Simulate request latency (0.1ms to 5 seconds) double latencyValue = 0.0001 + random.nextDouble() * 5.0; latency.observe(latencyValue); // Simulate response size (100 bytes to 10MB) double sizeValue = 100 + random.nextDouble() * 10_000_000; size.observe(sizeValue); // Simulate occasional slow requests if (random.nextInt(100) < 5) { // 5% slow requests double slowLatency = 1.0 + random.nextDouble() * 10.0; latency.observe(slowLatency); } }, 0, 100, TimeUnit.MILLISECONDS); // 10 requests per second } private static void simulateDatabaseMetrics(Summary queryTime) { ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Random random = new Random(); scheduler.scheduleAtFixedRate(() -> { // Simulate normal query time (1ms to 500ms) double queryTimeValue = 0.001 + random.nextDouble() * 0.5; queryTime.observe(queryTimeValue); // Simulate occasional slow queries if (random.nextInt(100) < 2) { // 2% slow queries double slowQueryTime = 1.0 + random.nextDouble() * 5.0; queryTime.observe(slowQueryTime); } }, 0, 50, TimeUnit.MILLISECONDS); // 20 queries per second } } // LatencyMeasurementUtility.java package com.metrics.util; import com.metrics.histogram.Histogram; import com.metrics.summary.Summary; import java.util.concurrent.Callable; public class LatencyMeasurementUtility { public static <T> T measureWithHistogram(Histogram histogram, Callable<T> operation) throws Exception { long startTime = System.nanoTime(); try { return operation.call(); } finally { double duration = (System.nanoTime() - startTime) / 1_000_000_000.0; // Convert to seconds histogram.observe(duration); } } public static void measureWithHistogram(Histogram histogram, Runnable operation) { long startTime = System.nanoTime(); try { operation.run(); } finally { double duration = (System.nanoTime() - startTime) / 1_000_000_000.0; histogram.observe(duration); } } public static <T> T measureWithSummary(Summary summary, Callable<T> operation) throws Exception { long startTime = System.nanoTime(); try { return operation.call(); } finally { double duration = (System.nanoTime() - startTime) / 1_000_000_000.0; summary.observe(duration); } } } Key Features:
- Thread-Safe: All operations are thread-safe using atomic operations
- High Performance: Minimal overhead for metric collection
- Flexible Buckets/Quantiles: Configurable buckets and quantiles for different use cases
- Tag Support: Multi-dimensional metrics with tags
- Multiple Reporters: Console, JSON, and extensible reporter interface
- Rich Statistics: Percentiles, averages, variance, standard deviation
- Memory Efficient: Circular buffers for recent values in summaries
- Production Ready: Proper resource management and error handling
This implementation provides a solid foundation for monitoring distributed systems and can be easily extended to integrate with Prometheus, Grafana, or other monitoring systems.