Introduction
Micrometer counters that do not increment are a silent failure -- the code calls counter.increment() but the metric value remains zero in the monitoring dashboard. This is typically caused by using different MeterRegistry instances, recreating counters on every call (which resets the count), or the metrics endpoint not being properly exposed. Unlike exceptions, missing metrics do not cause errors, making them difficult to detect until production dashboards show flat lines during active usage.
Symptoms
Prometheus endpoint shows zero:
$ curl http://localhost:8080/actuator/prometheus
# HELP orders_processed_total
# TYPE orders_processed_total counter
orders_processed_total 0.0Despite application processing orders:
10:23:45 INFO OrderService - Processed order ORD-12345
10:23:46 INFO OrderService - Processed order ORD-12346
10:23:47 INFO OrderService - Processed order ORD-12347Or the counter is missing entirely from the output:
$ curl http://localhost:8080/actuator/prometheus | grep orders
# No output - counter not registeredCommon Causes
- Creating counter on every call:
registry.counter("orders").increment()creates a new counter each time - Wrong MeterRegistry injected: Using
SimpleMeterRegistryinstead of the composite registry - Counter name mismatch: Incrementing
order_countbut queryingorders_processed_total - Tags causing multiple counters: Different tag values create separate counters, splitting the count
- Actuator metrics endpoint not enabled:
/actuator/prometheusnot exposed - Counter incremented in a bean created before MeterRegistry: Bean initialization order issue
Step-by-Step Fix
Step 1: Register counter once as a bean
```java @Configuration public class MetricsConfig {
@Bean public Counter ordersProcessedCounter(MeterRegistry registry) { return Counter.builder("orders.processed") .description("Total number of orders processed") .tag("application", "order-service") .register(registry); } } ```
Then inject and use:
```java @Service public class OrderService {
private final Counter ordersProcessedCounter;
public OrderService(Counter ordersProcessedCounter) { this.ordersProcessedCounter = ordersProcessedCounter; }
public void processOrder(Order order) { // Process the order // ...
// Increment the pre-registered counter ordersProcessedCounter.increment(); } } ```
Step 2: Enable metrics export in application.yml
management:
endpoints:
web:
exposure:
include: health,info,prometheus,metrics
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name:myapp}Step 3: Use MeterRegistry.counter() with consistent naming
```java @Service public class OrderService {
private final MeterRegistry meterRegistry;
public OrderService(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; }
public void processOrder(Order order) { // This is safe - counter() returns existing or creates new meterRegistry.counter("orders.processed", "status", order.getStatus(), "type", order.getType()) .increment(); } } ```
The counter() method is idempotent -- it returns the existing counter if one with the same name and tags already exists.
Step 4: Verify metrics with tests
```java @Test void shouldIncrementOrderCounter() { SimpleMeterRegistry registry = new SimpleMeterRegistry(); Counter counter = Counter.builder("orders.processed").register(registry);
OrderService service = new OrderService(registry); service.processOrder(new Order("ORD-001", "COMPLETED"));
assertThat(counter.count()).isEqualTo(1.0); } ```
Prevention
- Register counters as Spring beans to ensure single registration
- Inject
MeterRegistry(notCompositeMeterRegistryorSimpleMeterRegistry) - Use dot-separated naming convention:
orders.processed,orders.failed - Enable
/actuator/prometheusendpoint and verify output in staging - Add integration tests that assert counter values after operations
- Use
@Timedand@Countedannotations for method-level metrics with Spring AOP - Monitor counter registration via
meterRegistry.getMeters()in startup diagnostics