Metric Tagging in Micrometer
Micrometer is the instrumentation facade in Spring Boot’s observability ecosystem, providing a unified API to collect, tag, and publish metrics to systems like Prometheus, Datadog, and New Relic. Tags, which are key value pairs linked to metrics, allow detailed filtering and aggregation across dimensions such as service or region. However, too many unique tag values can generate excessive metric combinations and impact performance. This article explores tagging patterns for Micrometer metrics using a Spring Boot application.
1. Project Setup
To get started, we will create a Spring Boot project with Micrometer and Prometheus support. The project will expose an endpoint to simulate operations that will be instrumented with tagged metrics.
<!-- Spring Boot Actuator for metrics exposure -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
This configuration sets up a Spring Boot project that uses Micrometer and Prometheus to collect and expose metrics. The spring-boot-starter-actuator dependency enables metric endpoints, while the micrometer-registry-prometheus dependency allows the metrics to be scraped by Prometheus.
2. Application Configuration
We then configure our application to expose Prometheus metrics and enable management endpoints.
application.yml
server:
port: 8080
management:
endpoints:
web:
exposure:
include: ["prometheus", "metrics"]
endpoint:
prometheus:
enabled: true
This configuration exposes the /actuator/prometheus endpoint, which Prometheus can scrape for metrics. It also allows us to view available metrics at /actuator/metrics.
3. Creating Metrics with the Builder API
Micrometer’s Builder API provides a flexible way to create meters with tags dynamically. Below is a service that records counters and timers with variable tag values.
@Service
public class PaymentMetricsService {
private final MeterRegistry meterRegistry;
public PaymentMetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
public void recordPayment(String channel, boolean success, long processingTimeMs) {
Counter counter = Counter.builder("app.payments.processed")
.description("Number of processed payments")
.tag("channel", channel)
.tag("result", success ? "success" : "failure")
.register(meterRegistry);
counter.increment();
Timer timer = Timer.builder("app.payments.processing.time")
.description("Time taken to process a payment")
.tag("channel", channel)
.register(meterRegistry);
timer.record(Duration.ofMillis(processingTimeMs));
}
}
This class demonstrates how to use Micrometer’s Builder API to create counters and timers dynamically. The recordPayment() method registers a counter to track successful and failed payments and a timer to measure processing duration. Each meter includes tags that capture the payment channel (e.g., “web”, “mobile”) and the result (“success” or “failure”).
Although this pattern is straightforward, using builders in hot code paths can create garbage and reduce performance when the tag combinations vary widely. In such cases, it’s better to reuse meters.
4. Using MeterProvider to Reuse Meters Efficiently
When developing metric-heavy applications, creating new meter instances repeatedly (such as counters, timers, or gauges) can be inefficient and lead to unnecessary memory overhead. Instead, we can use Micrometer’s MeterProvider API, introduced in newer versions, enables the efficient obtaining and reuse of existing meters. The MeterProvider ensures that the same meter is reused if it already exists, rather than registering a duplicate one.
@Service
public class PaymentMetricsService2 {
private final MeterRegistry meterRegistry;
private final Meter.MeterProvider<Counter> counterProvider;
private final Meter.MeterProvider<Timer> timerProvider;
public PaymentMetricsService2(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// Create reusable providers for Counter and Timer
this.counterProvider = Counter.builder("app.payments.processed")
.description("Number of payments processed")
.withRegistry(meterRegistry);
this.timerProvider = Timer.builder("app.payments.processing.time")
.description("Time taken to process payments")
.withRegistry(meterRegistry);
}
public void recordPayment(String paymentMethod, boolean success, long durationMs) {
// Retrieve or create a Counter from the provider
Counter paymentCounter = counterProvider.withTags(
"method", paymentMethod,
"status", success ? "success" : "failure"
);
// Retrieve or create a Timer from the provider
Timer paymentTimer = timerProvider.withTags("method", paymentMethod);
// Record the metrics
paymentCounter.increment();
paymentTimer.record(durationMs, TimeUnit.MILLISECONDS);
}
}
In this implementation, reusable Meter.MeterProvider<T> instances for Counter and Timer are defined during class initialization, each bound to a MeterRegistry and configured with base metadata such as name and description. When withTags(...) is invoked, Micrometer checks for an existing meter matching the specified tag set and returns it if available; otherwise, it creates and registers a new one internally.
5. Running the Application
Next, a controller is created to invoke the PaymentMetricsService method when a request is made, simulating payment processing and recording the corresponding metrics.
@RestController
public class PaymentController {
private final PaymentMetricsService paymentMetricsService;
public PaymentController(PaymentMetricsService paymentMetricsService) {
this.paymentMetricsService = paymentMetricsService;
}
@GetMapping("/process-payment")
public String processPayment(
@RequestParam(defaultValue = "web") String channel,
@RequestParam(defaultValue = "true") boolean success) {
// Simulate random processing time
long processingTime = ThreadLocalRandom.current().nextLong(100, 500);
// Record payment metrics
paymentMetricsService.recordPayment(channel, success, processingTime);
return String.format("Payment processed via %s (success=%s) in %d ms",
channel, success, processingTime);
}
}
This controller provides a /process-payment endpoint. When invoked, it simulates payment processing by generating a random delay time, then calls PaymentMetricsService to record counter and timer metrics with appropriate tags for the channel and result.
Build and run the Spring Boot application using mvn spring-boot:run, then trigger the endpoint multiple times with different query parameters to generate and record various metric combinations.
curl "http://localhost:8080/process-payment?channel=web&success=true" curl "http://localhost:8080/process-payment?channel=mobile&success=false" curl "http://localhost:8080/process-payment?channel=web&success=true"
Each request records a counter increment and a timer observation in Micrometer.
Viewing the Counter and Timer Metrics
Available metrics can be viewed through the /actuator/metrics endpoint.
http://localhost:8080/actuator/metrics
You should see entries like:
{
"names": [
"app.payments.processed",
"app.payments.processing.time",
...
]
}
To view details for each metric, access them individually:
Counter metric
http://localhost:8080/actuator/metrics/app.payments.processed
Example output:
{
"name": "app.payments.processed",
"description": "Number of processed payments",
"measurements": [
{
"statistic": "COUNT",
"value": 3
}
],
"availableTags": [
{
"tag": "result",
"values": [
"failure",
"success"
]
},
{
"tag": "channel",
"values": [
"mobile",
"web"
]
}
]
}
Timer metric
http://localhost:8080/actuator/metrics/app.payments.processing.time
Example output:
{
"name": "app.payments.processing.time",
"description": "Time taken to process a payment",
"baseUnit": "seconds",
"measurements": [
{
"statistic": "COUNT",
"value": 3
},
{
"statistic": "TOTAL_TIME",
"value": 0.752
},
{
"statistic": "MAX",
"value": 0
}
],
"availableTags": [
{
"tag": "channel",
"values": [
"mobile",
"web"
]
}
]
}
This confirms that the counter is tracking the number of payments processed and the timer is recording the duration of each payment.
6. Conclusion
In this article, we explored some Micrometre tagging patterns for capturing metrics in Spring Boot applications. We began with the Builder API to create dynamic counters and timers with variable tags, then advanced to the MeterProvider API, which improves efficiency by reusing meters automatically and reducing registration overhead. These techniques help maintain low-latency instrumentation while ensuring consistent metric tagging across services. By applying these patterns, we can achieve high-performance observability that scales effectively in modern distributed systems.
7. Download the Source Code
This article explored tagging patterns in Micrometer metrics.
You can download the full source code of this example here: micrometer tagging patterns




