CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-dropwizard-metrics--metrics-core

Comprehensive metrics collection and monitoring library providing counters, gauges, histograms, meters, and timers for Java applications.

Pending
Overview
Eval results
Files

utilities.mddocs/

Utilities and Extensions

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

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();
}

Usage Examples

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

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);
}

Usage Examples

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

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();
}

Usage Examples

// 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

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);
}

Metrics Created

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 size

Usage Examples

Basic 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

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) {}
    }
}

Usage Examples

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();
    }
}

Additional Utility Classes

NoopMetricRegistry

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 disabled

Clock Abstraction

Clock 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 nanos

Best Practices

Global Registry Management

  • Use SharedMetricRegistries judiciously - prefer dependency injection when possible
  • Set up default registries early in application lifecycle
  • Clean up shared registries during application shutdown
  • Use named registries to separate concerns in modular applications

Filtering Strategies

  • Create reusable filter instances rather than recreating them
  • Use filtering to reduce reporter overhead with large numbers of metrics
  • Combine filters logically to create complex selection criteria
  • Test filter logic with known metric sets to ensure correct behavior

Instrumentation Best Practices

  • Instrument executors close to their creation to capture all activity
  • Choose meaningful names for instrumented components
  • Monitor instrumented executor metrics for performance issues and capacity planning
  • Use custom thread factories with descriptive thread names for better debugging

Listener Implementation

  • Extend MetricRegistryListener.Base to avoid implementing unused methods
  • Keep listener implementations lightweight to avoid impacting metric registration performance
  • Use listeners for cross-cutting concerns like validation, alerting, and automatic configuration
  • Be careful with listener exception handling - exceptions can disrupt metric registration

Performance Considerations

  • Utility operations should be performed during application startup when possible
  • Monitor the performance impact of registry listeners, especially in high-metric-registration scenarios
  • Use filtering effectively to reduce processing overhead in reporters and other metric consumers
  • Consider the memory implications of keeping references to metrics in listener implementations

Install with Tessl CLI

npx tessl i tessl/maven-io-dropwizard-metrics--metrics-core

docs

advanced-gauges.md

core-metrics.md

index.md

reporting.md

reservoirs-sampling.md

utilities.md

tile.json