Micrometer Integration
The fun-micrometer module instruments Try and Result executions automatically,
recording success/failure counts and execution latency without requiring manual metric
tracking at call sites. It is an optional dependency — the core fun library has no
runtime dependency on Micrometer.
fun-micrometer declares micrometer-core as compileOnly. You bring your own version
and backend (Prometheus, Datadog, CloudWatch, etc.), following the same pattern as
fun-jackson and fun-resilience4j. Any version from
1.5.x through 1.16.x is supported (see version compatibility below).
Adding the dependency
// Gradleimplementation("codes.domix:fun-micrometer:LATEST_VERSION")// Micrometer — bring your own version (1.5.x – 1.16.x)implementation("io.micrometer:micrometer-core:1.16.5")// Add your Micrometer backend, e.g. Prometheus:// implementation("io.micrometer:micrometer-registry-prometheus:1.16.5")<!-- Maven --><dependency> <groupId>codes.domix</groupId> <artifactId>fun-micrometer</artifactId> <version>LATEST_VERSION</version></dependency><!-- Micrometer — bring your own version (1.5.x – 1.16.x) --><dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>1.16.5</version></dependency>
fun-micrometerdeclaresmicrometer-coreascompileOnly. Your Micrometer backend (Prometheus, Datadog, CloudWatch, etc.) is pulled in by your own dependency — not by this module.
Metrics recorded
For every instrumented call, these two metric families are updated:
| Metric | Type | Tags |
|---|---|---|
{name}.count | Counter | outcome=success|failure + your custom tags |
{name}.duration | Timer | outcome=success|failure + your custom tags |
Try-only metric:
| Metric | Type | Tags |
|---|---|---|
{name}.failure | Counter | exception=<label> + your custom tags (emitted on recordTry failure only) |
The outcome tag lets you calculate the error rate with a simple PromQL ratio.
The exception tag on {name}.failure pinpoints which failure category is causing failures
without needing to dig into logs.
Metric naming convention
Use stable, low-cardinality operation names for {name}. A practical convention is:
<bounded-context>.<operation>
Examples:
order.createpayment.chargeinventory.reserve
Avoid embedding IDs or user input in metric names.
Exception tag cardinality
Warning: The default
exceptiontag usesgetClass().getSimpleName(). When arbitrary third-party exceptions can appear at runtime, this produces an unbounded number of tag values — a violation of Micrometer’s low-cardinality contract that causes excessive series growth in Prometheus and similar backends.
In production, always supply an explicit classifier that maps every reachable exception type to one of a small, fixed set of labels:
DmxMicrometer dmx = DmxMicrometer.of(registry, cause -> switch (cause) { case IOException _ -> "io"; case TimeoutException _ -> "timeout"; case IllegalStateException _ -> "state"; default -> "other"; });The same option is available on the DmxMetered builder:
DmxMetered.of("payment.charge") .registry(registry) .exceptionClassifier(cause -> cause instanceof IOException ? "io" : "other") .recordTry(() -> stripe.charge(amount));A MeterFilter can also enforce cardinality globally across all meters, but an explicit
classifier is clearer and requires no global configuration.
DmxMicrometer
The primary entry point. Create one instance per MeterRegistry (typically application-scoped)
and call recordTry or recordResult at each instrumentation point.
import dmx.fun.Result;import dmx.fun.Try;import dmx.fun.micrometer.DmxMicrometer;import io.micrometer.core.instrument.MeterRegistry;import io.micrometer.core.instrument.Tags;
// One-time setup — typically injected or obtained from the application contextDmxMicrometer dmx = DmxMicrometer.of(registry);
// Instrument a Try operation — name + tags identify the metricTry<Response> result = dmx.recordTry( "http.client.get", Tags.of("endpoint", "/users"), () -> httpClient.get(url));
result .onSuccess(r -> render(r)) .onFailure(ex -> renderError(ex));
// Instrument a Result operationResult<User, Throwable> user = dmx.recordResult( "db.user.find", Tags.of("table", "users"), () -> userRepo.findById(id));Metrics recorded for each call:
| Metric | Type | Tags |
|---|---|---|
{name}.count | Counter | outcome=success|failure + custom tags |
{name}.duration | Timer | outcome=success|failure + custom tags |
{name}.failure | Counter | Try-only: exception=<ClassName> + custom tags (emitted on recordTry failure only) |
DmxMetered — fluent builder
When you prefer to configure name, tags, and registry in a single chain — for example
inside a factory method or when building one-off instrumented operations — use the
DmxMetered fluent builder:
import dmx.fun.Try;import dmx.fun.Result;import dmx.fun.micrometer.DmxMetered;import io.micrometer.core.instrument.Tags;
// Chain name → tags → registry → executeTry<Receipt> result = DmxMetered.of("payment.charge") .tags(Tags.of("provider", "stripe", "currency", "USD")) .registry(registry) .recordTry(() -> stripe.charge(amount));
// Same with ResultResult<User, Throwable> user = DmxMetered.of("db.user.find") .tags(Tags.of("table", "users")) .registry(registry) .recordResult(() -> userRepo.findById(id));
DmxMeteredis a mutable builder — callrecordTryorrecordResultonce at the end. For repeated instrumentation of the same operation, preferDmxMicrometer.of(registry)and callrecordTrydirectly.
Choosing DmxMicrometer vs DmxMetered
| Scenario | Recommendation |
|---|---|
| Multiple operations in the same class | DmxMicrometer.of(registry) — inject once, call many times |
| Single ad-hoc operation, tags built inline | DmxMetered.of(name).tags(...).registry(...).recordTry(...) |
| Spring Boot bean | Inject MeterRegistry, wrap with DmxMicrometer.of(registry) in the constructor |
Real-world example
An order service where every step — inventory reservation, payment charge, and persistence — is instrumented independently. No manual counter updates or stopwatch bookkeeping in the business logic.
import dmx.fun.Option;import dmx.fun.Result;import dmx.fun.Try;import dmx.fun.micrometer.DmxMicrometer;import io.micrometer.core.instrument.MeterRegistry;import io.micrometer.core.instrument.Tags;
/** * An order service that automatically records metrics for every Try/Result * operation without scattering manual counter.increment() calls throughout * the business logic. * * After startup, the following metrics appear in your Micrometer backend * (e.g. Prometheus /actuator/prometheus): * * order_place_count{outcome="success",...} * order_place_count{outcome="failure",...} * order_place_duration_seconds_count{outcome="success",...} * order_place_duration_seconds_sum{outcome="success",...} * order_place_failure_total{exception="PaymentException",...} * * inventory_reserve_count{outcome="success",...} * ... */public class OrderService {
private final DmxMicrometer dmx; private final PaymentGateway paymentGateway; private final InventoryClient inventoryClient; private final OrderRepository orderRepository;
public OrderService(MeterRegistry registry, PaymentGateway paymentGateway, InventoryClient inventoryClient, OrderRepository orderRepository) { this.dmx = DmxMicrometer.of(registry); this.paymentGateway = paymentGateway; this.inventoryClient = inventoryClient; this.orderRepository = orderRepository; }
/** * Places an order: reserves inventory, charges payment, and persists. * Each step is instrumented independently so failures are traceable * to the exact service call — no wrapping try/catch needed. * Keep metric tags bounded: put customerId/orderId in logs or traces, * not in Micrometer tags. */ public Result<Order, String> placeOrder(PlaceOrderCommand cmd) { Tags orderTags = Tags.of("channel", cmd.channel(), "region", cmd.region());
// 1. Reserve inventory — metrics: inventory.reserve.count / duration / failure Try<Reservation> reservation = dmx.recordTry( "inventory.reserve", orderTags, () -> inventoryClient.reserve(cmd.items()) ); if (reservation.isFailure()) { return Result.err("Inventory unavailable: " + reservation.getCause().getMessage()); }
// 2. Charge payment — metrics: payment.charge.count / duration / failure Try<Receipt> receipt = dmx.recordTry( "payment.charge", orderTags.and("provider", cmd.paymentProvider()), () -> paymentGateway.charge(cmd.amount(), cmd.paymentToken()) ); if (receipt.isFailure()) { inventoryClient.release(reservation.get()); return Result.err("Payment failed: " + receipt.getCause().getMessage()); }
// 3. Persist order — metrics: order.place.count / duration / failure return dmx.recordResult( "order.place", orderTags, () -> orderRepository.save(Order.from(cmd, reservation.get(), receipt.get())) ) .mapError(ex -> "Failed to persist order: " + ex.getMessage()); }
/** * Looks up an order by ID. Missing orders are modelled as Option.none() * rather than a failure — the metric still records outcome=success. */ public Option<Order> findOrder(String orderId) { return dmx.recordTry( "order.find", Tags.of("lookup", "by-id", "channel", "api"), () -> orderRepository.findById(orderId) ) .toOption() .flatMap(Option::fromOptional); }}Instrumentation cost guidance
Micrometer instrumentation has small but non-zero overhead. Recommended placement:
- Instrument network boundaries (HTTP clients, DB repositories, queue publishers/consumers).
- Instrument expensive or high-value business operations.
- Avoid instrumenting ultra-hot trivial paths where you do not use the metric operationally.
- Reuse a single
DmxMicrometerinstance perMeterRegistryand keep tag sets stable.
Version compatibility
The module is tested in CI against the following Micrometer versions on every pull
request that touches the micrometer/ module:
| Micrometer version | Spring Boot | Status |
|---|---|---|
| 1.16.x | 3.5.x | tested |
| 1.15.x | — | tested |
| 1.14.x | 3.4.x | tested |
| 1.13.x | 3.3.x | tested |
| 1.12.x | 3.2.x | tested |
| 1.11.x | 3.1.x | tested |
| 1.10.x | 3.0.x | tested |
| 1.9.x | 2.7.x | tested |
| 1.8.x | — | tested |
| 1.7.x | — | tested |
| 1.6.x | — | tested |
| 1.5.x | — | tested |
To test locally against a specific version:
./gradlew :micrometer:test -PmicrometerVersion=1.12.0