Micrometer Tracing Integration

The fun-tracing module instruments Try and Result executions with distributed tracing spans automatically, opening a named child span for each call and recording outcome signals without requiring manual Span.start() / Span.end() management at call sites. It is an optional dependency — the core fun library has no runtime dependency on Micrometer Tracing.

fun-tracing declares micrometer-tracing as compileOnly. You bring your own version and bridge (OpenTelemetry or Brave), following the same pattern as fun-micrometer and fun-resilience4j. Any version from 1.2.x through 1.6.x is supported (see version compatibility below).

Adding the dependency

// Gradle
implementation("codes.domix:fun-tracing:LATEST_VERSION")
// Micrometer Tracing — bring your own version (1.2.x – 1.6.x)
implementation("io.micrometer:micrometer-tracing:1.6.5")
// Add a bridge — OTel (recommended) or Brave:
implementation("io.micrometer:micrometer-tracing-bridge-otel:1.6.5")
// implementation("io.micrometer:micrometer-tracing-bridge-brave:1.6.5")
<!-- Maven -->
<dependency>
<groupId>codes.domix</groupId>
<artifactId>fun-tracing</artifactId>
<version>LATEST_VERSION</version>
</dependency>
<!-- Micrometer Tracing — bring your own version (1.2.x – 1.6.x) -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing</artifactId>
<version>1.6.5</version>
</dependency>
<!-- Add a bridge — OTel (recommended) or Brave -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
<version>1.6.5</version>
</dependency>

fun-tracing declares micrometer-tracing as compileOnly. You bring your own version and bridge — Spring Boot auto-configures a Tracer bean when either bridge is on the classpath.

Signals recorded

For every instrumented call, the following signals are written to the active span:

SignalWhen
Span named {name}Always — the span name is the name argument
outcome=success tagSupplier returned normally
outcome=failure tagSupplier threw any exception
exception=<label> tagSupplier threw — classifier label of the cause
Span marked as errorSupplier threw — recorded via span.error(cause)

The span is always ended before the method returns, even when the supplier throws.

Span naming convention

Use stable, low-cardinality operation names. A practical convention is:

<bounded-context>.<operation>

Examples:

  • payment.charge
  • inventory.reserve
  • order.place
  • http.client.get

Avoid embedding IDs or user input in span names — those belong in log fields or baggage, not in span names, as high-cardinality names generate excessive trace index entries in Zipkin, Jaeger, and similar backends.

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 distinct tag values. High-cardinality span tags generate excessive index entries in Zipkin, Jaeger, and similar backends — and bloat sampling metadata in OpenTelemetry exporters.

In production, always supply an explicit classifier that maps every reachable exception type to one of a small, fixed set of labels:

DmxTracing dmx = DmxTracing.of(tracer, cause ->
switch (cause) {
case IOException __ -> "io";
case TimeoutException __ -> "timeout";
case IllegalStateException __-> "state";
default -> "other";
}
);

The same option is available on the DmxTraced builder:

DmxTraced.of("payment.charge")
.tracer(tracer)
.exceptionClassifier(cause -> cause instanceof IOException ? "io" : "other")
.traceTry(() -> stripe.charge(amount));

DmxTracing

The primary entry point. Create one instance per Tracer (typically application-scoped) and call traceTry or traceResult at each instrumentation point.

import dmx.fun.Result;
import dmx.fun.Try;
import dmx.fun.tracing.DmxTracing;
import io.micrometer.tracing.Tracer;
// One-time setup — typically injected from the application context
DmxTracing dmx = DmxTracing.of(tracer); // io.micrometer.tracing.Tracer
// Instrument a Try operation — name becomes the span name
Try<Response> result = dmx.traceTry("http.client.get",
() -> httpClient.get(url)
);
result
.onSuccess(r -> render(r))
.onFailure(ex -> renderError(ex));
// Instrument a Result operation
Result<User, Throwable> user = dmx.traceResult("db.user.find",
() -> userRepo.findById(id)
);

Signals recorded for each call:

SignalWhen
Span named {name}Always
outcome=success tagSupplier returned normally
outcome=failure tagSupplier threw
exception=<SimpleClassName> tagSupplier threw
Span marked as errorSupplier threw

DmxTraced — fluent builder

When you prefer to configure name and tracer in a single chain — for example inside a factory method or when wiring the span name and tracer at different call sites — use the DmxTraced fluent builder:

import dmx.fun.Try;
import dmx.fun.Result;
import dmx.fun.tracing.DmxTraced;
// Chain span name → tracer → execute
Try<Receipt> result = DmxTraced.of("payment.charge")
.tracer(tracer)
.traceTry(() -> stripe.charge(amount));
// Same with Result
Result<User, Throwable> user = DmxTraced.of("db.user.find")
.tracer(tracer)
.traceResult(() -> userRepo.findById(id));

DmxTraced is a mutable builder — call traceTry or traceResult once at the end. For repeated instrumentation of the same operation, prefer DmxTracing.of(tracer) and call traceTry directly.

Choosing DmxTracing vs DmxTraced

ScenarioRecommendation
Spring Boot with fun-spring-bootInject DmxTracing directly — auto-configured, no setup needed
Multiple operations in the same class (plain Spring)DmxTracing.of(tracer) — inject Tracer, wrap once in the constructor
Single ad-hoc operation or factory methodDmxTraced.of(name).tracer(tracer).traceTry(...)

Real-world example

An order service where every step — inventory reservation, payment charge, and persistence — is instrumented as an independent child span. No manual span bookkeeping in the business logic; every step appears in the trace timeline automatically.

import dmx.fun.Result;
import dmx.fun.Try;
import dmx.fun.tracing.DmxTracing;
import io.micrometer.tracing.Tracer;
/**
* An order service where each step — inventory reservation, payment charge,
* and persistence — is wrapped in its own named span.
*
* After a request completes, the trace backend (Zipkin, Jaeger, etc.) shows
* a timeline of child spans, each tagged with outcome=success|failure.
* Failed steps also carry exception=<ClassName> and are marked as errors,
* making the root cause visible at a glance without digging into logs.
*/
public class OrderService {
private final DmxTracing dmx;
private final PaymentGateway paymentGateway;
private final InventoryClient inventoryClient;
private final OrderRepository orderRepository;
public OrderService(Tracer tracer,
PaymentGateway paymentGateway,
InventoryClient inventoryClient,
OrderRepository orderRepository) {
this.dmx = DmxTracing.of(tracer);
this.paymentGateway = paymentGateway;
this.inventoryClient = inventoryClient;
this.orderRepository = orderRepository;
}
/**
* Places an order: reserves inventory, charges payment, and persists.
* Each step is a child span of the active trace context — no manual
* Span.start() / Span.end() calls needed.
*/
public Result<Order, String> placeOrder(PlaceOrderCommand cmd) {
// 1. Reserve inventory — span: inventory.reserve
Try<Reservation> reservation = dmx.traceTry("inventory.reserve",
() -> inventoryClient.reserve(cmd.items())
);
if (reservation.isFailure()) {
return Result.err("Inventory unavailable: " + reservation.getCause().getMessage());
}
// 2. Charge payment — span: payment.charge
Try<Receipt> receipt = dmx.traceTry("payment.charge",
() -> paymentGateway.charge(cmd.amount(), cmd.paymentToken())
);
if (receipt.isFailure()) {
inventoryClient.release(reservation.get());
return Result.err("Payment failed: " + receipt.getCause().getMessage());
}
// 3. Persist order — span: order.place
return dmx.traceResult("order.place",
() -> orderRepository.save(Order.from(cmd, reservation.get(), receipt.get()))
)
.mapError(ex -> "Failed to persist order: " + ex.getMessage());
}
}

Spring Boot integration

When fun-spring-boot, fun-tracing, and a Micrometer Tracing bridge are all on the classpath, Spring Boot registers a DmxTracing bean automatically — no @Bean declaration or manual wiring required. Inject it directly into your services:

import dmx.fun.Try;
import dmx.fun.tracing.DmxTracing;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final DmxTracing dmx; // injected — auto-configured by fun-spring-boot
public PaymentService(DmxTracing dmx) {
this.dmx = dmx;
}
public Try<Receipt> charge(Amount amount, PaymentToken token) {
return dmx.traceTry("payment.charge",
() -> gateway.charge(amount, token)
);
}
}

How it works

DmxFunTracingAutoConfiguration activates when:

  1. micrometer-tracing and fun-tracing are on the classpath (@ConditionalOnClass).
  2. A Tracer bean is present in the context (@ConditionalOnBean) — Spring Boot registers one automatically when a bridge (micrometer-tracing-bridge-otel or micrometer-tracing-bridge-brave) is on the classpath.

The DmxTracing bean is backed by that Tracer, so all spans it opens inherit the active trace context (W3C TraceContext, B3, etc.) without any extra wiring.

Opting out

dmx.fun.tracing.enabled=false # disable DmxTracing auto-configuration

Replacing the bean

The bean is guarded by @ConditionalOnMissingBean. Declare your own @Bean of type DmxTracing and the auto-configuration backs off:

@Bean
DmxTracing dmxTracing(Tracer tracer) {
return DmxTracing.of(tracer); // or any custom Tracer
}

Without fun-spring-boot

If you use plain Spring (no fun-spring-boot), construct DmxTracing manually in your @Bean or constructor:

@Service
public class PaymentService {
private final DmxTracing dmx;
public PaymentService(Tracer tracer) {
this.dmx = DmxTracing.of(tracer);
}
}

Version compatibility

The module is tested in CI against the following Micrometer Tracing versions on every pull request that touches the tracing/ module:

Micrometer TracingSpring BootStatus
1.6.x3.5.xtested
1.5.x3.4.xtested
1.4.x3.3.xtested
1.3.x3.2.xtested
1.2.x3.1.xtested

To test locally against a specific version:

Terminal window
./gradlew :tracing:test -PmicrometerTracingVersion=1.5.11