Comprehensive metrics collection and monitoring library providing counters, gauges, histograms, meters, and timers for Java applications.
—
Metrics Core provides a comprehensive set of utility classes and extensions that enhance the core functionality with global registry management, filtering capabilities, automatic instrumentation, and event listening. These utilities simplify integration and provide advanced functionality for complex monitoring scenarios.
SharedMetricRegistries provides global registry management, allowing different parts of an application to access the same metrics without explicit registry passing. This is particularly useful in modular applications, frameworks, and scenarios where dependency injection isn't available.
public class SharedMetricRegistries {
// Global registry management
public static MetricRegistry getOrCreate(String name);
public static void setDefault(String name);
public static MetricRegistry getDefault();
// Named registry management
public static void add(String name, MetricRegistry registry);
public static void remove(String name);
public static Set<String> names();
public static void clear();
}Basic Global Registry:
// Set up a default registry early in application lifecycle
MetricRegistry mainRegistry = new MetricRegistry();
SharedMetricRegistries.add("main", mainRegistry);
SharedMetricRegistries.setDefault("main");
// Access from anywhere in the application
public class UserService {
private final Counter userCreations = SharedMetricRegistries.getDefault()
.counter("users.created");
public void createUser(User user) {
// ... create user logic ...
userCreations.inc();
}
}
public class OrderService {
private final Timer orderProcessing = SharedMetricRegistries.getDefault()
.timer("orders.processing.time");
public void processOrder(Order order) {
try (Timer.Context context = orderProcessing.time()) {
// ... process order logic ...
}
}
}Multiple Named Registries:
// Set up different registries for different subsystems
MetricRegistry webRegistry = new MetricRegistry();
MetricRegistry databaseRegistry = new MetricRegistry();
MetricRegistry cacheRegistry = new MetricRegistry();
SharedMetricRegistries.add("web", webRegistry);
SharedMetricRegistries.add("database", databaseRegistry);
SharedMetricRegistries.add("cache", cacheRegistry);
// Use specific registries in different components
public class WebController {
private final Counter requests = SharedMetricRegistries
.getOrCreate("web")
.counter("http.requests");
}
public class DatabaseService {
private final Histogram queryTimes = SharedMetricRegistries
.getOrCreate("database")
.histogram("query.execution.time");
}
public class CacheService {
private final Meter hitRate = SharedMetricRegistries
.getOrCreate("cache")
.meter("cache.hits");
}Registry Lifecycle Management:
// Application startup
public void initializeMetrics() {
MetricRegistry appRegistry = new MetricRegistry();
SharedMetricRegistries.add("application", appRegistry);
SharedMetricRegistries.setDefault("application");
// Set up reporters
ConsoleReporter.forRegistry(appRegistry)
.build()
.start(30, TimeUnit.SECONDS);
}
// Application shutdown
public void shutdownMetrics() {
// Clean up shared registries
SharedMetricRegistries.clear();
}
// Module-specific registries
public class ModuleInitializer {
public void initializeModule(String moduleName) {
MetricRegistry moduleRegistry = new MetricRegistry();
SharedMetricRegistries.add(moduleName, moduleRegistry);
// Configure module-specific reporting
Slf4jReporter.forRegistry(moduleRegistry)
.outputTo("metrics." + moduleName)
.build()
.start(1, TimeUnit.MINUTES);
}
}MetricFilter provides flexible filtering mechanisms for controlling which metrics are processed by reporters, registry operations, and other metric consumers.
@FunctionalInterface
public interface MetricFilter {
boolean matches(String name, Metric metric);
// Predefined filters
MetricFilter ALL = (name, metric) -> true;
// Static factory methods
static MetricFilter startsWith(String prefix);
static MetricFilter endsWith(String suffix);
static MetricFilter contains(String substring);
}Basic String-Based Filters:
// Predefined string filters
MetricFilter httpFilter = MetricFilter.startsWith("http");
MetricFilter errorFilter = MetricFilter.contains("error");
MetricFilter apiFilter = MetricFilter.endsWith("api");
// Use with reporters
ConsoleReporter httpReporter = ConsoleReporter.forRegistry(registry)
.filter(httpFilter)
.build();
CsvReporter errorReporter = CsvReporter.forRegistry(registry)
.filter(errorFilter)
.build(new File("/var/logs/errors"));Custom Filter Implementations:
// Filter by metric type
MetricFilter timersOnly = new MetricFilter() {
@Override
public boolean matches(String name, Metric metric) {
return metric instanceof Timer;
}
};
// Filter by activity level
MetricFilter activeMetrics = new MetricFilter() {
@Override
public boolean matches(String name, Metric metric) {
if (metric instanceof Counting) {
return ((Counting) metric).getCount() > 0;
}
return true; // Include non-counting metrics
}
};
// Filter by pattern matching
MetricFilter patternFilter = new MetricFilter() {
private final Pattern pattern = Pattern.compile(".*\\.(time|duration)$");
@Override
public boolean matches(String name, Metric metric) {
return pattern.matcher(name).matches();
}
};Composite Filters:
// Combine multiple filters with AND logic
public class AndFilter implements MetricFilter {
private final MetricFilter[] filters;
public AndFilter(MetricFilter... filters) {
this.filters = filters;
}
@Override
public boolean matches(String name, Metric metric) {
for (MetricFilter filter : filters) {
if (!filter.matches(name, metric)) {
return false;
}
}
return true;
}
}
// Combine multiple filters with OR logic
public class OrFilter implements MetricFilter {
private final MetricFilter[] filters;
public OrFilter(MetricFilter... filters) {
this.filters = filters;
}
@Override
public boolean matches(String name, Metric metric) {
for (MetricFilter filter : filters) {
if (filter.matches(name, metric)) {
return true;
}
}
return false;
}
}
// Usage
MetricFilter criticalMetrics = new OrFilter(
MetricFilter.contains("error"),
MetricFilter.contains("critical"),
MetricFilter.startsWith("system.health")
);
MetricFilter performanceTimers = new AndFilter(
timersOnly,
MetricFilter.contains("performance")
);Registry Filtering Operations:
// Remove metrics matching filter
registry.removeMatching(MetricFilter.startsWith("temp"));
// Get filtered metric maps
SortedMap<String, Counter> httpCounters = registry.getCounters(
MetricFilter.startsWith("http"));
SortedMap<String, Timer> activeTimers = registry.getTimers(activeMetrics);MetricAttribute represents the various attributes that can be reported for different metric types, allowing fine-grained control over which statistical values are included in reports.
public enum MetricAttribute {
// Histogram and Timer attributes
COUNT("count"),
MAX("max"),
MEAN("mean"),
MIN("min"),
STDDEV("stddev"),
P50("p50"),
P75("p75"),
P95("p95"),
P98("p98"),
P99("p99"),
P999("p999"),
// Meter and Timer rate attributes
M1_RATE("m1_rate"),
M5_RATE("m5_rate"),
M15_RATE("m15_rate"),
MEAN_RATE("mean_rate");
// Methods
public String getCode();
}// Exclude high percentiles from console output for readability
Set<MetricAttribute> excludeHighPercentiles = EnumSet.of(
MetricAttribute.P98,
MetricAttribute.P99,
MetricAttribute.P999
);
ConsoleReporter simpleReporter = ConsoleReporter.forRegistry(registry)
.disabledMetricAttributes(excludeHighPercentiles)
.build();
// Include only basic statistics for CSV export
Set<MetricAttribute> basicStats = EnumSet.of(
MetricAttribute.COUNT,
MetricAttribute.MIN,
MetricAttribute.MAX,
MetricAttribute.MEAN,
MetricAttribute.P50,
MetricAttribute.P95
);
Set<MetricAttribute> excludeAdvanced = EnumSet.complementOf(basicStats);
CsvReporter basicCsvReporter = CsvReporter.forRegistry(registry)
.disabledMetricAttributes(excludeAdvanced)
.build(new File("/var/metrics"));InstrumentedExecutorService wraps any ExecutorService implementation with comprehensive metrics, providing visibility into thread pool behavior, task execution times, and queue statistics.
public class InstrumentedExecutorService implements ExecutorService {
// Constructors
public InstrumentedExecutorService(ExecutorService delegate, MetricRegistry registry);
public InstrumentedExecutorService(ExecutorService delegate, MetricRegistry registry, String name);
// ExecutorService interface implementation
public void shutdown();
public List<Runnable> shutdownNow();
public boolean isShutdown();
public boolean isTerminated();
public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
public <T> Future<T> submit(Callable<T> task);
public <T> Future<T> submit(Runnable task, T result);
public Future<?> submit(Runnable task);
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
public void execute(Runnable command);
}When you wrap an ExecutorService, the following metrics are automatically created:
{name}.submitted - Counter: Total tasks submitted{name}.running - Counter: Currently executing tasks{name}.completed - Counter: Successfully completed tasks{name}.duration - Timer: Task execution duration{name}.rejected - Meter: Rejected task rate (if applicable)Additional metrics for ThreadPoolExecutor:
{name}.pool.size - Gauge: Current pool size{name}.pool.core - Gauge: Core pool size{name}.pool.max - Gauge: Maximum pool size{name}.pool.active - Gauge: Active thread count{name}.queue.size - Gauge: Queue sizeBasic Thread Pool Instrumentation:
// Create and instrument a fixed thread pool
ExecutorService originalExecutor = Executors.newFixedThreadPool(10);
ExecutorService instrumentedExecutor = new InstrumentedExecutorService(
originalExecutor, registry, "worker-pool");
// Use normally - metrics are collected automatically
instrumentedExecutor.submit(() -> performWork());
instrumentedExecutor.submit(() -> processTask());
// Metrics are available in registry:
// worker-pool.submitted, worker-pool.running, worker-pool.completed, etc.Multiple Instrumented Executors:
// Web request processing pool
ExecutorService webPool = new InstrumentedExecutorService(
Executors.newCachedThreadPool(), registry, "web-requests");
// Background task processing pool
ExecutorService backgroundPool = new InstrumentedExecutorService(
Executors.newFixedThreadPool(5), registry, "background-tasks");
// Database operation pool
ExecutorService dbPool = new InstrumentedExecutorService(
Executors.newFixedThreadPool(20), registry, "database-ops");
// Each pool gets its own set of metrics
webPool.submit(() -> handleHttpRequest());
backgroundPool.submit(() -> processBackgroundJob());
dbPool.submit(() -> executeQuery());Custom ThreadPoolExecutor Instrumentation:
// Create custom thread pool with detailed configuration
ThreadPoolExecutor customPool = new ThreadPoolExecutor(
5, // corePoolSize
20, // maximumPoolSize
60, TimeUnit.SECONDS, // keepAliveTime
new LinkedBlockingQueue<>(100), // workQueue
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "custom-worker-" + threadNumber.getAndIncrement());
t.setDaemon(true);
return t;
}
}
);
// Instrument the custom pool
ExecutorService instrumentedCustomPool = new InstrumentedExecutorService(
customPool, registry, "custom-processing");
// Monitor queue saturation and thread utilization
instrumentedCustomPool.submit(() -> heavyProcessingTask());Monitoring Executor Health:
public class ExecutorHealthChecker {
private final MetricRegistry registry;
public ExecutorHealthChecker(MetricRegistry registry) {
this.registry = registry;
}
public boolean isExecutorHealthy(String executorName) {
// Check queue size (assuming ThreadPoolExecutor)
Gauge<Integer> queueSize = registry.getGauges().get(executorName + ".queue.size");
if (queueSize != null && queueSize.getValue() > 50) {
return false; // Queue too full
}
// Check rejection rate
Meter rejectedRate = registry.getMeters().get(executorName + ".rejected");
if (rejectedRate != null && rejectedRate.getOneMinuteRate() > 10) {
return false; // Too many rejections
}
// Check task completion rate vs submission rate
Counter submitted = registry.getCounters().get(executorName + ".submitted");
Counter completed = registry.getCounters().get(executorName + ".completed");
if (submitted != null && completed != null) {
long backlog = submitted.getCount() - completed.getCount();
if (backlog > 1000) {
return false; // Too much backlog
}
}
return true;
}
}MetricRegistryListener provides an event-driven mechanism for responding to metric registration and removal events, enabling advanced monitoring scenarios, metric validation, and automatic configuration.
public interface MetricRegistryListener extends EventListener {
// Metric addition events
void onGaugeAdded(String name, Gauge<?> gauge);
void onCounterAdded(String name, Counter counter);
void onHistogramAdded(String name, Histogram histogram);
void onMeterAdded(String name, Meter meter);
void onTimerAdded(String name, Timer timer);
// Metric removal events
void onGaugeRemoved(String name);
void onCounterRemoved(String name);
void onHistogramRemoved(String name);
void onMeterRemoved(String name);
void onTimerRemoved(String name);
// Convenience base class with no-op implementations
abstract class Base implements MetricRegistryListener {
@Override public void onGaugeAdded(String name, Gauge<?> gauge) {}
@Override public void onCounterAdded(String name, Counter counter) {}
@Override public void onHistogramAdded(String name, Histogram histogram) {}
@Override public void onMeterAdded(String name, Meter meter) {}
@Override public void onTimerAdded(String name, Timer timer) {}
@Override public void onGaugeRemoved(String name) {}
@Override public void onCounterRemoved(String name) {}
@Override public void onHistogramRemoved(String name) {}
@Override public void onMeterRemoved(String name) {}
@Override public void onTimerRemoved(String name) {}
}
}Automatic Reporter Configuration:
public class AutoReporterListener extends MetricRegistryListener.Base {
private final CsvReporter csvReporter;
private final Slf4jReporter slf4jReporter;
public AutoReporterListener(MetricRegistry registry) {
this.csvReporter = CsvReporter.forRegistry(registry)
.build(new File("/var/metrics"));
this.slf4jReporter = Slf4jReporter.forRegistry(registry)
.outputTo("metrics")
.build();
}
@Override
public void onTimerAdded(String name, Timer timer) {
// Start CSV reporting when first timer is added
if (!csvReporter.isStarted()) {
csvReporter.start(5, TimeUnit.MINUTES);
}
// Log important timer additions
if (name.contains("critical")) {
System.out.println("Critical timer added: " + name);
}
}
@Override
public void onMeterAdded(String name, Meter meter) {
// Start SLF4J reporting when first meter is added
if (!slf4jReporter.isStarted()) {
slf4jReporter.start(1, TimeUnit.MINUTES);
}
}
}
// Register the listener
registry.addListener(new AutoReporterListener(registry));Metric Validation and Alerting:
public class MetricValidationListener extends MetricRegistryListener.Base {
private final Set<String> allowedPrefixes = Set.of("http", "db", "cache", "business");
private final Logger logger = LoggerFactory.getLogger(MetricValidationListener.class);
@Override
public void onCounterAdded(String name, Counter counter) {
validateMetricName(name);
setupAlertingForCounter(name, counter);
}
@Override
public void onHistogramAdded(String name, Histogram histogram) {
validateMetricName(name);
validateHistogramConfiguration(name, histogram);
}
private void validateMetricName(String name) {
boolean validPrefix = allowedPrefixes.stream()
.anyMatch(name::startsWith);
if (!validPrefix) {
logger.warn("Metric '{}' uses non-standard prefix", name);
}
if (name.contains(" ") || name.contains("/")) {
logger.error("Metric '{}' contains invalid characters", name);
}
}
private void validateHistogramConfiguration(String name, Histogram histogram) {
Snapshot snapshot = histogram.getSnapshot();
if (snapshot.size() == 0) {
// New histogram, check reservoir type by examining behavior
histogram.update(1);
if (histogram.getSnapshot().size() > 0) {
logger.info("Histogram '{}' configured and ready", name);
}
}
}
private void setupAlertingForCounter(String name, Counter counter) {
if (name.contains("error") || name.contains("failure")) {
// Schedule periodic error count checking
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(() -> {
long count = counter.getCount();
if (count > 100) {
logger.error("High error count detected: {} = {}", name, count);
}
}, 1, 1, TimeUnit.MINUTES);
}
}
}Dynamic Metric Grouping:
public class MetricGroupingListener extends MetricRegistryListener.Base {
private final Map<String, List<String>> metricGroups = new ConcurrentHashMap<>();
@Override
public void onCounterAdded(String name, Counter counter) {
addToGroup(name, "counters");
}
@Override
public void onTimerAdded(String name, Timer timer) {
addToGroup(name, "timers");
// Group by subsystem
String subsystem = extractSubsystem(name);
if (subsystem != null) {
addToGroup(name, "subsystem." + subsystem);
}
}
private void addToGroup(String metricName, String groupName) {
metricGroups.computeIfAbsent(groupName, k -> new ArrayList<>())
.add(metricName);
}
private String extractSubsystem(String name) {
String[] parts = name.split("\\.");
return parts.length > 0 ? parts[0] : null;
}
public List<String> getMetricsInGroup(String groupName) {
return metricGroups.getOrDefault(groupName, Collections.emptyList());
}
public Set<String> getAllGroups() {
return metricGroups.keySet();
}
}NoopMetricRegistry implements the null object pattern for scenarios where metrics should be disabled:
public class NoopMetricRegistry extends MetricRegistry {
// All operations are no-ops
// Useful for testing or when metrics are disabled
}Usage:
// Conditional metric registry
MetricRegistry registry;
if (metricsEnabled) {
registry = new MetricRegistry();
} else {
registry = new NoopMetricRegistry();
}
// Code works the same way regardless
Counter requests = registry.counter("requests");
requests.inc(); // No-op if metrics disabledClock provides time abstraction for testing and alternative time sources:
public abstract class Clock {
public abstract long getTick(); // High-resolution time for durations
public abstract long getTime(); // Wall-clock time in milliseconds
public static Clock defaultClock(); // System clock implementation
// Built-in implementation
public static class UserTimeClock extends Clock {
@Override public long getTick() { return System.nanoTime(); }
@Override public long getTime() { return System.currentTimeMillis(); }
}
}Testing with Custom Clock:
public class TestClock extends Clock {
private long currentTime = 0;
@Override
public long getTick() {
return currentTime * 1_000_000; // Convert millis to nanos
}
@Override
public long getTime() {
return currentTime;
}
public void advance(long millis) {
currentTime += millis;
}
}
// Use in tests
TestClock testClock = new TestClock();
Timer timer = new Timer(new ExponentiallyDecayingReservoir(1000, 0.015, testClock), testClock);
Timer.Context context = timer.time();
testClock.advance(100); // Simulate 100ms elapsed
context.stop();
assertEquals(1, timer.getCount());
assertTrue(timer.getSnapshot().getMean() > 90_000_000); // ~100ms in nanosSharedMetricRegistries judiciously - prefer dependency injection when possibleMetricRegistryListener.Base to avoid implementing unused methodsInstall with Tessl CLI
npx tessl i tessl/maven-io-dropwizard-metrics--metrics-core