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

// Gradle
implementation("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-micrometer declares micrometer-core as compileOnly. 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:

MetricTypeTags
{name}.countCounteroutcome=success|failure + your custom tags
{name}.durationTimeroutcome=success|failure + your custom tags

Try-only metric:

MetricTypeTags
{name}.failureCounterexception=<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.create
  • payment.charge
  • inventory.reserve

Avoid embedding IDs or user input in metric names.

Exception tag cardinality

Warning: The default exception tag uses getClass().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 context
DmxMicrometer dmx = DmxMicrometer.of(registry);
// Instrument a Try operation — name + tags identify the metric
Try<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 operation
Result<User, Throwable> user = dmx.recordResult(
"db.user.find",
Tags.of("table", "users"),
() -> userRepo.findById(id)
);

Metrics recorded for each call:

MetricTypeTags
{name}.countCounteroutcome=success|failure + custom tags
{name}.durationTimeroutcome=success|failure + custom tags
{name}.failureCounterTry-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 → execute
Try<Receipt> result = DmxMetered.of("payment.charge")
.tags(Tags.of("provider", "stripe", "currency", "USD"))
.registry(registry)
.recordTry(() -> stripe.charge(amount));
// Same with Result
Result<User, Throwable> user = DmxMetered.of("db.user.find")
.tags(Tags.of("table", "users"))
.registry(registry)
.recordResult(() -> userRepo.findById(id));

DmxMetered is a mutable builder — call recordTry or recordResult once at the end. For repeated instrumentation of the same operation, prefer DmxMicrometer.of(registry) and call recordTry directly.

Choosing DmxMicrometer vs DmxMetered

ScenarioRecommendation
Multiple operations in the same classDmxMicrometer.of(registry) — inject once, call many times
Single ad-hoc operation, tags built inlineDmxMetered.of(name).tags(...).registry(...).recordTry(...)
Spring Boot beanInject 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 DmxMicrometer instance per MeterRegistry and 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 versionSpring BootStatus
1.16.x3.5.xtested
1.15.xtested
1.14.x3.4.xtested
1.13.x3.3.xtested
1.12.x3.2.xtested
1.11.x3.1.xtested
1.10.x3.0.xtested
1.9.x2.7.xtested
1.8.xtested
1.7.xtested
1.6.xtested
1.5.xtested

To test locally against a specific version:

Terminal window
./gradlew :micrometer:test -PmicrometerVersion=1.12.0