Introduction

For any Observation to happen, you need to register ObservationHandler objects through an ObservationRegistry. An ObservationHandler reacts only to supported implementations of an Observation.Context and can create timers, spans, and logs by reacting to the lifecycle events of an Observation, such as:

  • start - Observation has been started. Happens when the Observation#start() method gets called.

  • stop - Observation has been stopped. Happens when the Observation#stop() method gets called.

  • error - An error occurred while observing. Happens when the Observation#error(exception) method gets called.

  • event - An event happened when observing. Happens when the Observation#event(event) method gets called.

  • scope started - Observation opens a Scope. The Scope must be closed when no longer used. Handlers can create thread local variables on start that are cleared upon closing of the scope. Happens when the Observation#openScope() method gets called.

  • scope stopped - Observation stops a Scope. Happens when the Observation.Scope#close() method gets called.

Whenever any of these methods is called, an ObservationHandler method (such as onStart(T extends Observation.Context ctx), onStop(T extends Observation.Context ctx), and so on) is called. To pass state between the handler methods, you can use the Observation.Context.

This is how the Observation state diagram looks like:

        Observation           Observation
        Context               Context
Created ----------> Started ----------> Stopped

This is how the Observation Scope state diagram looks like:

              Observation
              Context
Scope Started ----------> Scope Finished

To make it possible to debug production problems an Observation needs additional metadata such as key-value pairs (also known as tags). You can then query your metrics or distributed tracing backend by those tags to find the required data. Tags can be either of high or low cardinality.

High cardinality means that a pair will have an unbounded number of possible values. An HTTP URL is a good example of such a key value (such as /example/user1234, /example/user2345 etc.). Low cardinality means that a key value will have a bounded number of possible values. A templated HTTP URL is a good example of such a key value (such as /example/{userId}).

To separate Observation lifecycle operations from an Observation configuration (such as names and low and high cardinality tags), you can use the ObservationConvention that provides an easy way of overriding the default naming conventions.

The following example uses the Observation API:

static class Example {

    private final ObservationRegistry registry;

    Example(ObservationRegistry registry) {
        this.registry = registry;
    }

    void run() {
        Observation.createNotStarted("foo", registry)
                .lowCardinalityKeyValue("lowTag", "lowTagValue")
                .highCardinalityKeyValue("highTag", "highTagValue")
                .observe(() -> System.out.println("Hello"));
    }

}
Calling observe(() → …​) leads to starting the Observation, putting it in scope, running the lambda, putting an error on the Observation if one took place, closing the scope and stopping the Observation.

Building Blocks

In this section we will describe a high overview of Micrometer Observation components. You can read more about those in this part of the documentation.

Glossary

  • ObservationRegistry - registry containing Observation related configuration (e.g. handlers, predicates, filters)

  • ObservationHandler - handles lifecycle of an Observation (e.g. create a timer on observation start, stop it on observation stop)

  • ObservationFilter - mutates the Context before the Observation gets stopped (e.g. add a high cardinality key-value with the cloud server region)

  • ObservationPredicate - condition to disable creation of an Observation (e.g. don’t create observations with given key-value)

  • Context (actually Observation.Context) - a mutable map attached to an Observation, passed between handlers (that way you can pass state without doing any thread locals)

  • ObservationConvention - mean to separate Observation lifecycle (starting, stopping, opening scopes) from adding meta-data (such as observation name, key-value pairs). That way the naming of observations and meta-data handling becomes a configuration problem (e.g. adding key-values do not require changing instrumentation code, but changing the convention)

Usage Flow

┌──────────────────┐┌─────────────────┐┌────────────────────┐┌───────┐┌─────────────────────┐
│ObservationHandler││ObservationFilter││ObservationPredicate││Context││ObservationConvention│
└┬─────────────────┘└┬────────────────┘└┬───────────────────┘└┬──────┘└┬────────────────────┘
┌▽───────────────────▽──────────────────▽┐                    │        │
│ObservationRegistry                     │                    │        │
└┬───────────────────────────────────────┘                    │        │
┌▽────────────────────────────────────────────────────────────▽────────▽┐
│Observation                                                            │
└───────────────────────────────────────────────────────────────────────┘
  • ObservationHandler, ObservationFilter, ObservationPredicate get registered on the ObservationRegistry

    • ObservationHandler can be composed together (e.g. via the FirstMatchingCompositeObservationHandler - that can be useful when you group multiple handlers of the same type, e.g. TracingHandler or MeterHandler)

  • ObservationRegistry, Context are mandatory to create an Observation

    • Either you create an Observation with name, ObservationRegistry and Context

    • Or with ObservationRegistry, ObservationConvention

  • When Observation calls its lifecycle methods, all ObservationHandlers that pass the supportsContext check with the corresponding Context will have their lifecycle methods executed (e.g. if a handler has an instanceof SenderContext check in supportsContext then it will be called only for such types of contexts)