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:
Java application
-> Micrometer instrumentation
-> Prometheus, OTLP, Datadog, or another registry
-> Grafana or another observability interfaceThe 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:
prometheusCounter.inc();Changing the monitoring system would then require changing instrumentation throughout the application and its libraries. Micrometer instead exposes a common model:
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:
| Component | Purpose |
|---|---|
| Micrometer Metrics | Records dimensional counters, gauges, timers, and distributions |
| Micrometer Observation | Instruments an operation once and lets handlers produce multiple signals |
| Micrometer Tracing | Provides a facade over tracing implementations such as OpenTelemetry and Brave |
| Micrometer Context Propagation | Transfers 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:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>Create and update a counter:
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:
orders_created_total{region="eu-west"} 42Timers #
A timer records both how many operations completed and how long they took:
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:
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:
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:
<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:
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:
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:
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:
Observation.createNotStarted("checkout", observationRegistry)
.lowCardinalityKeyValue("result", "success")
.highCardinalityKeyValue("order.id", orderId)
.observe(() -> checkout(order));An observation has a lifecycle:
create -> start -> open scope -> event/error -> close scope -> stopRegistered 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:
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:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>or:
<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:
Observation starts -> span starts
Observation receives an error -> span records the error
Observation stops -> span stops and is reportedThis 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
ThreadLocalvalues - 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
ThreadLocalvalue. - 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:
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:
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:
<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:
/actuator/prometheusPrometheus 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:
| Area | Micrometer | OpenTelemetry |
|---|---|---|
| Primary scope | JVM application instrumentation facade | Cross-language telemetry APIs, SDKs, protocols, and collector |
| Metrics model | Mature Java meter abstractions and registry integrations | OpenTelemetry metrics API and SDK |
| Tracing | Facade over tracer implementations | Native tracing API and SDK |
| Unified operation API | Observation API | Separate signal APIs with shared context |
| Export | Registry modules and tracing bridges | OTLP and exporter ecosystem |
| Collector process | None | OpenTelemetry 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:
- Instrument application-specific behavior with Micrometer meters or observations.
- Add
micrometer-registry-prometheus. - Expose the registry’s scrape output through an HTTP endpoint.
- Configure Prometheus to scrape that endpoint.
- Query the resulting series with PromQL.
- Build Grafana dashboards and Prometheus alerting rules.
The responsibilities remain distinct:
Micrometer -> creates and exposes measurements
Prometheus -> scrapes, stores, and queries measurements
Grafana -> visualizes queries
Alertmanager -> routes notificationsFor distributed tracing, add one Micrometer Tracing bridge and configure its exporter separately. Prometheus stores metrics, not trace spans.
Instrumentation Guidelines #
- Measure business operations, not merely low-level implementation details.
- Give every meter a stable name, unit, and description.
- Keep metric tags low-cardinality and bounded.
- Use normalized routes such as
/orders/{id}, not raw paths. - Use timers for duration and counters for event totals.
- Prefer aggregatable histograms when fleet-wide percentiles are required.
- Use observations when one operation should produce both metrics and spans.
- Propagate context at every asynchronous boundary.
- Test meter names and observation conventions as public interfaces.
- 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)