Enterprise Java

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.

Download
You can download the full source code of this example here: micrometer tagging patterns

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button