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:

bash
$ curl http://localhost:8080/actuator/prometheus
# HELP orders_processed_total
# TYPE orders_processed_total counter
orders_processed_total 0.0

Despite application processing orders:

bash
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-12347

Or the counter is missing entirely from the output:

bash
$ curl http://localhost:8080/actuator/prometheus | grep orders
# No output - counter not registered

Common Causes

  • Creating counter on every call: registry.counter("orders").increment() creates a new counter each time
  • Wrong MeterRegistry injected: Using SimpleMeterRegistry instead of the composite registry
  • Counter name mismatch: Incrementing order_count but querying orders_processed_total
  • Tags causing multiple counters: Different tag values create separate counters, splitting the count
  • Actuator metrics endpoint not enabled: /actuator/prometheus not 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

yaml
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 (not CompositeMeterRegistry or SimpleMeterRegistry)
  • Use dot-separated naming convention: orders.processed, orders.failed
  • Enable /actuator/prometheus endpoint and verify output in staging
  • Add integration tests that assert counter values after operations
  • Use @Timed and @Counted annotations for method-level metrics with Spring AOP
  • Monitor counter registration via meterRegistry.getMeters() in startup diagnostics