Introduction
Micrometer provides a vendor-neutral metrics facade for Java applications, but counters may not appear in the metrics endpoint when the meter is not registered with the correct registry, when tags cause cardinality explosion, or when the counter is incremented on an instance that is not the managed bean. Spring Boot auto-configures a MeterRegistry bean, but creating counters with new Counter() or using a different registry instance means the counter is not visible at /actuator/metrics. Additionally, using the wrong metric type (Counter vs Timer vs Gauge) produces misleading or missing data.
Symptoms
curl http://localhost:8080/actuator/metrics/orders.processed
# {"name":"orders.processed","measurements":[]}
# Empty measurements - counter not registered or not incrementedOr:
io.micrometer.core.instrument.config.InvalidConfigurationException: Meter registration failed
# Counter with same name but different tags already existsCommon Causes
- Meter not registered with Spring's MeterRegistry: Using wrong registry instance
- Counter created with wrong tags: Tags differ from query, meter not found
- Gauge used instead of Counter: Gauge shows current value, not cumulative
- Cardinality explosion: Too many unique tag combinations
- Meter removed or not reused: Creating new meter instance on each call
- Metrics endpoint not exposed: management.endpoints.web.exposure missing metrics
Step-by-Step Fix
Step 1: Inject and use MeterRegistry correctly
```java import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.stereotype.Service;
@Service public class OrderService {
private final Counter ordersProcessed; private final Counter ordersFailed;
// Inject MeterRegistry in constructor public OrderService(MeterRegistry registry) { // Counter.builder ensures idempotent registration this.ordersProcessed = Counter.builder("orders.processed") .description("Total orders processed") .tag("type", "all") .register(registry);
this.ordersFailed = Counter.builder("orders.failed") .description("Total orders that failed") .tag("type", "all") .register(registry); }
public void processOrder(Order order) { try { doProcess(order); ordersProcessed.increment(); // Increment the counter } catch (Exception e) { ordersFailed.increment(); throw e; } } } ```
Step 2: Configure metrics exposure
# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus # Include metrics
metrics:
export:
prometheus:
enabled: true
tags:
application: ${spring.application.name:myapp}Step 3: Use Timer for duration measurements
```java import io.micrometer.core.instrument.Timer;
@Service public class PaymentService {
private final Timer paymentTimer;
public PaymentService(MeterRegistry registry) { this.paymentTimer = Timer.builder("payment.processing.time") .description("Time to process a payment") .tag("method", "credit_card") .register(registry); }
public PaymentResult process(PaymentRequest request) { // Timer records execution time return paymentTimer.record(() -> { return paymentGateway.charge(request); }); } } ```
Prevention
- Always inject MeterRegistry through constructor -- never create counters directly
- Use Counter.builder().register(registry) for idempotent meter creation
- Keep tag cardinality low -- avoid tags with high unique values (user ID, request ID)
- Use Counter for incrementing values, Timer for durations, Gauge for current state
- Enable Prometheus export with micrometer-registry-prometheus dependency
- Verify metrics appear at /actuator/metrics after deployment
- Add metric assertions to integration tests to catch registration issues