Micrometer Application Observability

Last update: 2021-03-16 06:20:21 +0800 +0800

Micrometer is a collection of Java libraries for instrumenting applications. It provides vendor-neutral APIs for metrics, observations, and distributed tracing, along with adapters that connect those APIs to monitoring systems such as Prometheus and tracing implementations such as OpenTelemetry.

Micrometer is not a monitoring server. It does not store telemetry, execute PromQL, render dashboards, or deliver alerts. It runs inside an application and produces telemetry for other systems to collect:

text
Java application
  -> Micrometer instrumentation
  -> Prometheus, OTLP, Datadog, or another registry
  -> Grafana or another observability interface

The relationship is similar to a database abstraction library and a database: Micrometer supplies the application-facing API, while Prometheus and other backends supply storage and querying.

Why Micrometer Exists #

Without an instrumentation facade, application code might depend directly on a vendor SDK:

java
prometheusCounter.inc();

Changing the monitoring system would then require changing instrumentation throughout the application and its libraries. Micrometer instead exposes a common model:

java
registry.counter("orders.created", "region", region).increment();

A registry implementation decides how that meter is exported. The same instrumentation can publish to Prometheus in one application and to an OTLP or commercial backend in another.

This separation is especially useful to library authors. A library can instrument its behavior with Micrometer without deciding which monitoring system its users must operate.

The Micrometer Project Family #

The Micrometer ecosystem has four central concepts and projects:

ComponentPurpose
Micrometer MetricsRecords dimensional counters, gauges, timers, and distributions
Micrometer ObservationInstruments an operation once and lets handlers produce multiple signals
Micrometer TracingProvides a facade over tracing implementations such as OpenTelemetry and Brave
Micrometer Context PropagationTransfers observation and tracing context across threads and reactive execution

The project also publishes registry adapters, tracing bridges, test utilities, dependency-management BOMs, and documentation-generation support.

Micrometer Metrics #

Micrometer Metrics is the foundation of the project. Its central interface is MeterRegistry, which creates and maintains meters.

The main meter types are:

  • Counter: A cumulative number of events.
  • Gauge: A sampled value that can increase or decrease.
  • Timer: A count and distribution of durations.
  • Distribution summary: A count and distribution of arbitrary values, such as response sizes.
  • Long task timer: The number and duration of operations that are still running.

Add the core API with a build dependency:

xml
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-core</artifactId>
</dependency>

Create and update a counter:

java
Counter counter = Counter.builder("orders.created")
    .description("Number of successfully created orders")
    .tag("region", "eu-west")
    .register(registry);

counter.increment();

Micrometer converts its dot-separated naming convention to the convention of the selected backend. A Prometheus registry may expose the preceding meter as:

text
orders_created_total{region="eu-west"} 42

Timers #

A timer records both how many operations completed and how long they took:

java
Timer timer = Timer.builder("checkout.duration")
    .description("Checkout processing time")
    .tag("result", "success")
    .publishPercentileHistogram()
    .register(registry);

timer.record(() -> checkout(order));

For Prometheus, a histogram-capable timer can produce counters, sums, and buckets that PromQL can aggregate:

promql
histogram_quantile(
  0.95,
  sum by (le) (
    rate(checkout_duration_seconds_bucket[5m])
  )
)

Prefer histogram buckets when percentiles must be aggregated across application instances. Client-calculated percentiles generally cannot be meaningfully combined after export.

Gauges #

A gauge observes an existing object’s current value:

java
AtomicInteger queueSize = registry.gauge(
    "checkout.queue.size",
    new AtomicInteger()
);

Micrometer normally keeps only a weak reference to the observed object. The application must retain a strong reference, as queueSize does here, or the object may be garbage-collected and the gauge may disappear.

A gauge should represent current state. Do not use a gauge for a value that should only increase; use a counter instead.

Registry Implementations #

MeterRegistry is the abstraction. A registry module adapts meters to a specific monitoring system.

For Prometheus:

xml
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

The Prometheus registry exposes a text representation that an HTTP endpoint can return for Prometheus to scrape:

java
PrometheusMeterRegistry registry =
    new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);

String responseBody = registry.scrape();

Micrometer also supplies registry modules for OTLP and numerous commercial and open-source monitoring systems. Registries fall broadly into two models:

  • Pull registries: A monitoring system, such as Prometheus, scrapes the application.
  • Push or step registries: The application periodically publishes aggregated measurements to a backend.

Application instrumentation should not assume one model. The registry handles backend-specific naming, temporality, aggregation, and transport behavior.

Composite Registries #

A CompositeMeterRegistry can publish the same meters through multiple registries:

java
CompositeMeterRegistry composite = new CompositeMeterRegistry();
composite.add(prometheusRegistry);
composite.add(anotherRegistry);

This is useful during a backend migration, but it also increases resource use and can expose differences in how backends represent the same meter.

Filters and Common Tags #

MeterFilter can rename meters, add tags, configure distributions, or deny undesired meters:

java
registry.config()
    .commonTags("service", "checkout", "environment", "production");

Common tags identify the application consistently, but they should remain bounded. A request ID, user ID, email address, or raw URL must not become a metric tag. Each unique tag combination creates another time series.

Micrometer Observation #

Metrics answer aggregate questions, while tracing describes individual operations. Instrumenting the same operation independently for both signals can duplicate lifecycle and metadata code.

The Observation API represents one observable operation:

java
Observation.createNotStarted("checkout", observationRegistry)
    .lowCardinalityKeyValue("result", "success")
    .highCardinalityKeyValue("order.id", orderId)
    .observe(() -> checkout(order));

An observation has a lifecycle:

text
create -> start -> open scope -> event/error -> close scope -> stop

Registered ObservationHandler implementations react to that lifecycle. One handler can record a timer, another can create a trace span, and another can place correlation identifiers into logging context. Application code describes the operation once instead of calling every telemetry API separately.

Low- and High-Cardinality Key Values #

The Observation API distinguishes two kinds of metadata:

  • Low-cardinality key values have a small, bounded set of values. They may be used as metric tags and trace attributes.
  • High-cardinality key values may have many unique values. They may be attached to individual trace spans but must not become metric tags.

For example:

text
Low cardinality:  method=GET, route=/orders/{id}, result=success
High cardinality: order.id=8f27..., request.id=90ac...

This distinction lets one observation safely produce both metrics and traces.

Observation Conventions #

An ObservationConvention centralizes an observation’s name and key values. Frameworks and libraries can provide a default convention while applications replace it with a custom one.

Conventions keep instrumentation stable and prevent names or tags from being assembled differently at every call site. They can also be documented and tested as part of an application’s observability contract.

Micrometer Tracing #

Micrometer Tracing is a vendor-neutral facade for distributed tracing. It was created from the tracing functionality of Spring Cloud Sleuth and is not tied to Spring.

Its API covers concepts such as:

  • Tracers and spans
  • Current trace context
  • Baggage
  • Context injection and extraction
  • Sampling and span reporting integration

Micrometer Tracing does not implement a complete tracing system by itself. Bridge modules connect it to a tracer implementation:

  • OpenTelemetry bridge: Uses the OpenTelemetry SDK and ecosystem.
  • Brave bridge: Uses the Brave tracing library.

An application normally chooses one bridge, not both:

xml
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>

or:

xml
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>

Reporter and exporter configuration then sends completed spans to a backend such as Tempo, Zipkin, Jaeger through a compatible protocol, or another OpenTelemetry-compatible service.

Observation and Tracing Together #

Micrometer provides an observation handler that creates a span for each observation:

text
Observation starts -> span starts
Observation receives an error -> span records the error
Observation stops -> span stops and is reported

This is the normal application-level integration. Direct use of the tracing API is still useful when explicit child spans or tracing-specific behavior is needed.

Context Propagation #

Telemetry context must cross execution boundaries. A trace started on an HTTP request thread is otherwise lost when work moves to an executor, a callback, or a reactive pipeline.

The Micrometer Context Propagation library provides abstractions that transfer values among context mechanisms, including:

  • Java ThreadLocal values
  • Reactor Context
  • Other context carriers connected through accessors

Its principal concepts include:

  • Context accessor: Reads and writes values in a particular context type.
  • Thread-local accessor: Reads, writes, and clears a ThreadLocal value.
  • Context registry: Keeps the registered accessors.
  • Context snapshot: Captures values from one execution context and restores them in another.

A simplified executor example looks like:

java
ContextSnapshot snapshot = ContextSnapshot.captureAll();

executor.execute(() -> {
    try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
        performWork();
    }
});

Framework integrations can perform this work automatically. Manual propagation is needed when an application creates an execution boundary that the framework does not instrument.

Context propagation should be scoped carefully. Failing to restore previous thread-local values can leak one request’s trace or security context into another task executed by the same pooled thread.

Test and Documentation Support #

Micrometer publishes utilities that help libraries treat instrumentation as a testable API rather than incidental behavior.

A SimpleMeterRegistry stores measurements in memory and is useful in unit tests:

java
SimpleMeterRegistry registry = new SimpleMeterRegistry();
CheckoutService service = new CheckoutService(registry);

service.checkout(order);

assertThat(registry.counter("orders.created").count()).isEqualTo(1.0);

Observation test support can assert that observations started, stopped, and carried the expected key values. Tracing test support provides test-oriented tracer and span-handler implementations.

The Micrometer documentation generator can produce reference documentation from observation conventions and meter definitions. This helps framework and library authors publish the names, tags, and meanings of their instrumentation without maintaining a separate list by hand.

Micrometer also publishes BOMs so related module versions can be managed together:

xml
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>io.micrometer</groupId>
      <artifactId>micrometer-bom</artifactId>
      <version>${micrometer.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Framework dependency-management platforms may already import an appropriate Micrometer BOM. In that case, applications should normally let the framework select compatible versions.

Micrometer in Spring Boot #

Micrometer is independent of Spring, but Spring Boot provides its most familiar integration. Spring Boot Actuator configures registries and instruments many framework components automatically.

With the Actuator and Prometheus registry dependencies, an application can expose metrics at:

text
/actuator/prometheus

Prometheus scrapes that endpoint. Grafana then queries Prometheus; it does not query Micrometer directly.

Spring Boot can also configure observation handlers, tracing bridges, propagation formats, sampling, and exporters. Application code should first use the framework’s existing observations and only add custom instrumentation for business operations that the framework cannot understand.

Micrometer is also used outside Spring. Other JVM frameworks and libraries can integrate MeterRegistry, ObservationRegistry, and the tracing APIs without depending on the Spring container.

Micrometer and OpenTelemetry #

Micrometer and OpenTelemetry overlap, but they are not identical:

AreaMicrometerOpenTelemetry
Primary scopeJVM application instrumentation facadeCross-language telemetry APIs, SDKs, protocols, and collector
Metrics modelMature Java meter abstractions and registry integrationsOpenTelemetry metrics API and SDK
TracingFacade over tracer implementationsNative tracing API and SDK
Unified operation APIObservation APISeparate signal APIs with shared context
ExportRegistry modules and tracing bridgesOTLP and exporter ecosystem
Collector processNoneOpenTelemetry Collector

They can be used together. Micrometer metrics can publish through an OTLP registry, and Micrometer Tracing can bridge to the OpenTelemetry SDK. This lets an application retain Micrometer’s Java-facing APIs while using OpenTelemetry for propagation and export.

Choose APIs deliberately rather than instrumenting the same operation through both systems independently, which can create duplicate metrics and spans.

A Practical Prometheus Setup #

For a Java service monitored by Prometheus:

  1. Instrument application-specific behavior with Micrometer meters or observations.
  2. Add micrometer-registry-prometheus.
  3. Expose the registry’s scrape output through an HTTP endpoint.
  4. Configure Prometheus to scrape that endpoint.
  5. Query the resulting series with PromQL.
  6. Build Grafana dashboards and Prometheus alerting rules.

The responsibilities remain distinct:

text
Micrometer  -> creates and exposes measurements
Prometheus  -> scrapes, stores, and queries measurements
Grafana     -> visualizes queries
Alertmanager -> routes notifications

For distributed tracing, add one Micrometer Tracing bridge and configure its exporter separately. Prometheus stores metrics, not trace spans.

Instrumentation Guidelines #

  1. Measure business operations, not merely low-level implementation details.
  2. Give every meter a stable name, unit, and description.
  3. Keep metric tags low-cardinality and bounded.
  4. Use normalized routes such as /orders/{id}, not raw paths.
  5. Use timers for duration and counters for event totals.
  6. Prefer aggregatable histograms when fleet-wide percentiles are required.
  7. Use observations when one operation should produce both metrics and spans.
  8. Propagate context at every asynchronous boundary.
  9. Test meter names and observation conventions as public interfaces.
  10. Avoid registering new meters dynamically on every request.

Micrometer’s main value is separation of concerns. Application and library code describe what should be observed; registry adapters and tracing bridges decide where that telemetry goes. This keeps instrumentation portable while allowing Prometheus, OpenTelemetry, and commercial backends to evolve independently from the code being measured.

Comments

Read or comment on GitHub (sign-in required to comment)