Configuring with Micrometer Observation

Handler Configuration

For Micrometer Tracing to work with Micrometer Observation, you need to add a tracing-related ObservationHandler. The following example shows how to add and use a single DefaultTracingObservationHandler:

Tracer tracer = Tracer.NOOP; // The real tracer will come from your tracer
                             // implementation (Brave /
// OTel)
Propagator propagator = Propagator.NOOP; // The real propagator will come from
                                         // your tracer implementation (Brave /
                                         // OTel)
MeterRegistry meterRegistry = new SimpleMeterRegistry();

ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig()
    // assuming that micrometer-core is on the classpath
    .observationHandler(new DefaultMeterObservationHandler(meterRegistry))
    // we set up a first matching handler that creates spans - it comes from
    // Micrometer
    // Tracing. We set up spans for sending and receiving data over the wire
    // and a default one
    .observationHandler(new ObservationHandler.FirstMatchingCompositeObservationHandler(
            new PropagatingSenderTracingObservationHandler<>(tracer, propagator),
            new PropagatingReceiverTracingObservationHandler<>(tracer, propagator),
            new DefaultTracingObservationHandler(tracer)));

// Creating and starting a new observation
// via the `DefaultTracingObservationHandler` that will create a new Span and
// start it
Observation observation = Observation.start("my.operation", registry)
    .contextualName("This name is more readable - we can reuse it for e.g. spans")
    .lowCardinalityKeyValue("this.tag", "will end up as a meter tag and a span tag")
    .highCardinalityKeyValue("but.this.tag", "will end up as a span tag only");

// Put the observation in scope
// This will result in making the previously created Span, the current Span - it's
// in ThreadLocal
try (Observation.Scope scope = observation.openScope()) {
    // Run your code that you want to measure - still the attached Span is the
    // current one
    // This means that e.g. logging frameworks could inject to e.g. MDC tracing
    // information
    yourCodeToMeasure();
}
finally {
    // The corresponding Span will no longer be in ThreadLocal due to
    // try-with-resources block (Observation.Scope is an AutoCloseable)
    // Stop the Observation
    // The corresponding Span will be stopped and reported to an external system
    observation.stop();
}

You can also use a shorter version to perform measurements by using the observe method:

ObservationRegistry registry = ObservationRegistry.create();

Observation.createNotStarted("my.operation", registry)
    .contextualName("This name is more readable - we can reuse it for e.g. spans")
    .lowCardinalityKeyValue("this.tag", "will end up as a meter tag and a span tag")
    .highCardinalityKeyValue("but.this.tag", "will end up as a span tag only")
    .observe(this::yourCodeToMeasure);

This will result in the following Micrometer Metrics:

Gathered the following metrics
    Meter with name <my.operation> and type <TIMER> has the following measurements
        <[
            Measurement{statistic='COUNT', value=1.0},
            Measurement{statistic='TOTAL_TIME', value=1.011949454},
            Measurement{statistic='MAX', value=1.011949454}
        ]>
        and has the following tags <[tag(this.tag=will end up as a meter tag and a span tag)]>

It also results in the following trace view in (for example) Zipkin:

Trace Info propagation

Ordered Handler Configuration

Micrometer Tracing comes with multiple ObservationHandler implementations. To introduce ordering, you can use the ObservationHandler.AllMatchingCompositeObservationHandler to run logic for all ObservationHandler instances that match the given predicate and ObservationHandler. Use FirstMatchingCompositeObservationHandler to run logic only for the first ObservationHandler that matches the predicate. The former can group handlers and the latter can be chosen to (for example) run only one matching TracingObservationHandler.

Context Propagation with Micrometer Tracing

To make Context Propagation work with Micrometer Tracing, you need to manually register the proper ThreadLocalAccessor, as follows:

ContextRegistry.getInstance().registerThreadLocalAccessor(new ObservationAwareSpanThreadLocalAccessor(tracer));
ContextRegistry.getInstance()
    .registerThreadLocalAccessor(new ObservationAwareBaggageThreadLocalAccessor(registry, tracer));

The ObservationAwareSpanThreadLocalAccessor is required to propagate manually created spans (not the ones that are governed by Observations). The ObservationAwareBaggageThreadLocalAccessor is required to propagate baggage created by the user.

With Project Reactor one should set the values of Observation, Span or BaggageToPropagate in the Reactor Context as follows:

// Setup example
ContextRegistry contextRegistry = ContextRegistry.getInstance();

ObservationAwareSpanThreadLocalAccessor accessor;

ObservationAwareBaggageThreadLocalAccessor observationAwareBaggageThreadLocalAccessor;


accessor = new ObservationAwareSpanThreadLocalAccessor(observationRegistry, getTracer());
observationAwareBaggageThreadLocalAccessor = new ObservationAwareBaggageThreadLocalAccessor(observationRegistry,
        getTracer());
contextRegistry.loadThreadLocalAccessors()
    .registerThreadLocalAccessor(accessor)
    .registerThreadLocalAccessor(observationAwareBaggageThreadLocalAccessor);
Hooks.enableAutomaticContextPropagation();

// Usage example
Hooks.enableAutomaticContextPropagation();
Observation observation = Observation.start("parent", observationRegistry);

List<String> hello = Mono.just("hello")
    .subscribeOn(Schedulers.single())
    .flatMap(s -> {
        Mono<List<String>> mono = Mono.defer(() -> Mono.just(Arrays.asList(
            getTracer().getBaggage("tenant").get(),
            getTracer().getBaggage("tenant2").get())
        ));
    return mono.subscribeOn(Schedulers.parallel())
        .contextWrite(ReactorBaggage.append("tenant", s + ":baggage")); // Appends baggage to existing one (tenant2:baggage2)
})
    .contextWrite(Context.of(ObservationThreadLocalAccessor.KEY, observation, // Puts observation to Reactor Context
            ObservationAwareBaggageThreadLocalAccessor.KEY, new BaggageToPropagate("tenant2", "baggage2") // Puts baggage to Reactor Context
        ))
    .block();

Exemplars

To add support for exemplars instead of using the DefaultMeterObservationHandler you should use the TracingAwareMeterObservationHandler, as follows:

ObservationRegistry registry = ObservationRegistry.create();
registry.observationConfig()
    // Don't register the DefaultMeterObservationHandler...
    // .observationHandler(new DefaultMeterObservationHandler(meterRegistry))
    // ...instead register the tracing aware version
    .observationHandler(new TracingAwareMeterObservationHandler<>(
            new DefaultMeterObservationHandler(meterRegistry), tracer));