Every Spring Boot developer knows this moment. You need a simple gauge metric. You open the Micrometer docs. You write ten lines of code. You inject MeterRegistry into another class. Then you repeat it for the next metric.

Meanwhile @Timed just works. Why can't everything else?

I built Metrify to fix this.

The Problem

Micrometer is great. But the moment you need anything beyond @Timed , you are back to writing boilerplate.

Want a gauge that tracks a method's return value? Here is what you probably write today:

@Service
public class OrderService {

    private final AtomicInteger activeOrders = new AtomicInteger(0);

    public OrderService(MeterRegistry registry) {
        Gauge.builder("orders.active", activeOrders, AtomicInteger::get)
             .description("Number of active orders")
             .register(registry);
    }

    public int getActiveOrderCount() {
        return activeOrders.get();
    }
}

Here is what you should write instead:

@Service
public class OrderService {

    @MetricGauge(name = "orders.active", description = "Number of active orders")
    public int getActiveOrderCount() {
        return activeOrders.get();
    }
}

One annotation. Zero boilerplate.

Why Doesnโ€™t Micrometer Have this Functionality Already?

The Micrometer team made a conscious decision. In GitHub issue #451, the community asked for Dropwizard-style annotations. The answer was: Micrometer is a low-level facade. Not an annotation framework.

Fair. But that left a gap nobody filled for modern Spring Boot 3.x. The old metrics-spring library had @Gauge and more. But it was built on Dropwizard Metrics and has not been updated since 2016.

Metrify fills that gap.

What Metrify Does

Add one dependency. All annotations work. No manual bean registration. No TimedAspect setup. No MeterRegistry everywhere.

@MetricGauge

@MetricGauge(name = "queue.depth")
public int getQueueDepth() {
    return processingQueue.size();
}

For fields, you can annotate AtomicInteger or AtomicLong directly:

@MetricGauge(name = "connections.active")
private final AtomicInteger activeConnections = new AtomicInteger(0);

@MetricCounter

@MetricCounter(name = "orders.created", tags = {"channel", "web"})
public Order createOrder(OrderRequest request) { ... }

Dynamic Tags via SpEL

@MetricCounter(
    name = "orders.processed",
    dynamicTags = {
        @MetricTag(key = "region", expression = "#order.region"),
        @MetricTag(key = "tier", expression = "#order.customer.tier")
    }
)
public void processOrder(Order order) { ... }

@CachedGauge

For expensive calls, you do not want to run on every Prometheus scrape:

@CachedGauge(name = "db.connections", ttl = 30, ttlUnit = TimeUnit.SECONDS)
public int getDatabaseConnectionCount() {
    return dataSource.getActiveConnections();
}

Startup Tag Validation

Micrometer silently fails when you register the same metric name with different tag keys in different places. Metrify catches this at startup:

ERROR: Metric 'orders.processed' has inconsistent tag keys.
  OrderService.processOrder()   โ†’ [region, tier]
  LegacyService.handleOrder()   โ†’ [region]

Reactive Support

Mono and Flux are handled natively. The counter increments when the stream completes. Not when the method is called.

Getting Started

<dependency>
    <groupId>io.github.wtk-ns</groupId>
    <artifactId>metrify-spring-boot-starter</artifactId>
    <version>0.1.0</version>
</dependency>

No @Enable annotation. No config class. It just works.

One Honest Caveat

Method-level @MetricGauge caches the last returned value. It does not give Prometheus a live view between method calls.

If you need a true real-time gauge, use a field annotation instead:

@MetricGauge(name = "connections.active")
private final AtomicInteger connectionCount = new AtomicInteger(0);

This is a live reference. Always reflects the current value.


Try It!

  • GitHub: https://github.com/wtk-ns/metrify-spring-boot-starter

If you have ever copy-pasted the same Gauge.builder() block from one service to another. This is for you.