Explain the 'one instrumentation, many signals' model: how does a single Observation produce metrics, traces, and logs together?
answer
- ObservationHandler = the fan-out point
- onStart/onStop/onError/onScope callbacks
- MeterHandler→Timer, TracingHandler→span, MDC→logs
- same name+KeyValues → correlated signals
- missing signal = missing handler/dependency
basics
~20 sYou instrument code once with an Observation. The registry's handlers each react to its lifecycle events — one handler records a metric, another creates a trace span, another logs — so one instrumentation feeds all three pillars.
solid answer
~40 sThe Observation API decouples *instrumenting* code from *what signals ship*. When an Observation starts, stops, opens a scope, or records an error, the ObservationRegistry notifies every registered `ObservationHandler` for that event. Each handler translates lifecycle callbacks into a specific signal: `DefaultMeterObservationHandler` records a Micrometer Timer (metrics); the tracing handlers (from Micrometer Tracing) open/close a span propagated to Brave or OpenTelemetry (traces); and you can add handlers that emit structured logs or events. Because they share one Observation, the timer, span, and logs carry the **same name and KeyValues**, and the span/trace IDs line up with logs via MDC. So a single `observe(...)` call yields a correlated timer, span, and log line without you wiring three libraries by hand. Adding or removing a signal is a handler/config change, not a code change.
code
java · 26 linesimport io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationHandler;
// A custom handler that logs the lifecycle — showing the fan-out contract.
class LoggingObservationHandler implements ObservationHandler<Observation.Context> {
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(LoggingObservationHandler.class);
@Override
public void onStart(Observation.Context context) {
log.info("START {}", context.getName());
}
@Override
public void onStop(Observation.Context context) {
log.info("STOP {} tags={}", context.getName(), context.getLowCardinalityKeyValues());
}
@Override
public boolean supportsContext(Observation.Context context) {
return true; // handle every Observation
}
}
// Register alongside the auto-configured meter + tracing handlers:
// registry.observationConfig().observationHandler(new LoggingObservationHandler());go deeper
Knows the slogan and that handlers produce the signals.
Names the handler lifecycle callbacks and the meter/tracing handlers by role.
Can debug a missing pillar as a missing handler/dependency and explain MDC correlation.
Treats the handler list as an extensibility seam and reasons about ordering, cardinality routing, and framework-provided instrumentation.
## The core idea **One instrumentation, many signals** means: you mark a unit of work as an **Observation** exactly once, and the observability *outputs* (the "pillars": **metrics**, **traces/distributed tracing**, **logs**) are produced by pluggable components rather than by your business code. Your code says *"this is an interesting operation"*; the platform decides *what to emit*. ## How the fan-out actually works: ObservationHandler The registry holds a list of **`ObservationHandler<T extends Observation.Context>`**. An Observation has a **lifecycle** and, at each step, the registry calls matching handlers: - `onStart(context)` — when the Observation starts. - `onScopeOpened` / `onScopeClosed` — when a scope (thread-local binding) opens/closes. - `onError(context)` — when an error is recorded. - `onEvent(event, context)` — for point-in-time events. - `onStop(context)` — when the Observation stops. Each handler implements `supportsContext(Context)` to decide whether it cares. On each callback it does its own thing: - **Metrics**: `DefaultMeterObservationHandler` starts a Timer.Sample on start and, on stop, records the duration into a Micrometer **Timer** named after the Observation, tagged with its **low-cardinality KeyValues**. - **Traces**: Micrometer Tracing ships `PropagatingSenderTracingObservationHandler`, `PropagatingReceiverTracingObservationHandler`, and `DefaultTracingObservationHandler`. On start they create/continue a **span**; on stop they end it. Span tags come from KeyValues (both low and high cardinality). The underlying tracer is **Brave** (Zipkin) or **OpenTelemetry**. - **Logs/correlation**: tracing handlers push **trace/span IDs into the MDC**, so your normal log lines become correlated with the trace. You can also register custom handlers that emit an event/log per Observation. Because every handler reads the *same* `Observation.Context` (same name, same KeyValues), the emitted signals are **consistent and correlated** by construction. ## What Spring Boot wires for you Spring Boot auto-configures the `ObservationRegistry` and registers: - the meter handler whenever a `MeterRegistry` bean exists, - the tracing handlers whenever **Micrometer Tracing** + a bridge (`micrometer-tracing-bridge-brave` or `-otel`) are on the classpath, - plus framework instrumentation: HTTP server (`observationRegistry` filter), `RestClient`/`WebClient`, `@Observed` AOP (needs `ObservedAspect`), scheduled tasks, messaging, etc. So an incoming HTTP request already becomes one Observation producing an `http.server.requests` timer **and** a server span **and** correlated logs. ## Concrete flow ``` observe(work) → registry.start() → MeterHandler: start Timer.Sample → TracingHandler: start span → run work (scope open: MDC has traceId/spanId → logs correlated) → on exception: error() → handlers tag error / span status ERROR → registry.stop() → MeterHandler: record Timer → TracingHandler: end span ``` ## Gotchas - **Missing a signal usually means a missing handler/dependency**, not a code bug. No `MeterRegistry` → no timer; no tracing bridge → no spans. The code is identical either way. - **High-cardinality KeyValues** reach spans but are **excluded from metric tags** to avoid cardinality blow-up (see the KeyValues question). - **`@Observed`** annotation needs the `ObservedAspect` bean registered, or nothing happens. - Handlers run in the **order registered**; ordering matters when one handler depends on another's context mutation. ## When to lean on it Use it whenever you want a custom operation to appear uniformly across dashboards, traces, and logs with one line, and when you want the freedom to turn a signal on/off by changing dependencies/config rather than editing instrumentation.
- You added the Observation code but no spans appear in your tracing backend — where do you look first?Check dependencies/config, not the code. Spans need Micrometer Tracing plus a bridge (brave or otel) on the classpath and a reporter/exporter configured. Without the tracing handlers registered, the same Observation still records metrics but produces no span.
- How do logs end up correlated with the trace from the same Observation?The tracing ObservationHandlers push traceId/spanId into SLF4J's MDC while the Observation's scope is open, and your logging pattern includes those MDC keys, so every log line during the operation carries the same IDs.
saying these in an interview costs you the question
- Believing your business code must call the tracer and MeterRegistry separately.
- Thinking each signal needs its own instrumentation call.
- Assuming turning on tracing requires editing the Observation code rather than adding a bridge dependency.
- Not knowing ObservationHandler is what produces the signals.