Metrics with Micrometer

Built-in Metrics

Actuator uses Micrometer, a "SLF4J for metrics": you record metrics once and export them to Prometheus, Datadog, New Relic or CloudWatch. Many metrics are automatic: http.server.requests, jvm.memory.used, hikaricp.connections.active, cache.gets.

Example

curl -s "localhost:8081/actuator/metrics/http.server.requests?tag=uri:/api/products/{id}"

Example output

{"name":"http.server.requests","baseUnit":"seconds","measurements":[{"statistic":"COUNT","value":15234.0},{"statistic":"TOTAL_TIME","value":182.4},{"statistic":"MAX","value":0.41}],"availableTags":[{"tag":"status","values":["200","404"]},{"tag":"method","values":["GET"]}]}

Custom Business Metrics

@Service
public class CheckoutService {
    private final Counter ordersPlaced;
    private final Timer paymentTimer;
    private final DistributionSummary orderValue;

    public CheckoutService(MeterRegistry registry, CartRepository carts) {
        this.ordersPlaced = Counter.builder("shopease.orders.placed")
                .description("Orders placed").tag("channel", "web").register(registry);
        this.paymentTimer = Timer.builder("shopease.payment.duration")
                .publishPercentiles(0.5, 0.95, 0.99).register(registry);
        this.orderValue = DistributionSummary.builder("shopease.order.value")
                .baseUnit("inr").register(registry);
        Gauge.builder("shopease.carts.active", carts, CartRepository::countActive).register(registry);
    }

    public OrderResponse checkout(Cart cart) {
        PaymentResult result = paymentTimer.record(() -> paymentGateway.charge(cart));
        ordersPlaced.increment();
        orderValue.record(cart.total().doubleValue());
        ...
    }
}
MeterUse
Counteronly goes up: orders placed, failed payments
Gaugea current value: active carts, queue size
Timerhow long something takes, plus count
DistributionSummarydistribution of values: order amount, file size

A Mini Meter Registry

Example

Counters and timers are simple ideas. This plain-Java registry records them with tags and prints Prometheus-style lines:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Registry registry = new Registry();
        registry.increment("shopease_orders_placed_total", "channel=web");
        registry.increment("shopease_orders_placed_total", "channel=web");
        registry.increment("shopease_orders_placed_total", "channel=app");
        for (long ms : new long[] { 120, 340, 95, 410 }) registry.time("shopease_payment_seconds", ms);
        registry.print();
    }
}

class Registry {
    private final Map<String, Double> counters = new TreeMap<>();
    private final Map<String, List<Long>> timers = new TreeMap<>();

    void increment(String name, String tags) { counters.merge(name + "{" + tags + "}", 1.0, Double::sum); }
    void time(String name, long millis) { timers.computeIfAbsent(name, k -> new ArrayList<>()).add(millis); }

    void print() {
        counters.forEach((k, v) -> System.out.println(k + " " + v));
        timers.forEach((name, list) -> {
            long total = list.stream().mapToLong(Long::longValue).sum();
            long max = Collections.max(list);
            System.out.println(name + "_count " + list.size());
            System.out.println(name + "_sum " + total / 1000.0);
            System.out.println(name + "_max " + max / 1000.0);
        });
    }
}

Output

shopease_orders_placed_total{channel=app} 1.0
shopease_orders_placed_total{channel=web} 2.0
shopease_payment_seconds_count 4
shopease_payment_seconds_sum 0.965
shopease_payment_seconds_max 0.41