Implementing Java Custom Metrics with Micrometer for Fine-Grained Application Monitoring

Intent, Audience, and Outcome

This guide is written for Java developers and DevOps engineers who want to implement fine-grained, custom metrics for JVM applications using Micrometer, enhancing observability beyond default metrics. By the end, you will understand how to define custom counters, gauges, and timers; integrate them into a sample business service; expose and verify metrics via Prometheus; and manage production considerations such as security, performance, and troubleshooting. We assume Java 11+, Micrometer 1.10.x, and familiarity with building Java applications using Maven or Gradle.

When to Use Custom Metrics with Micrometer

Micrometer provides a flexible abstraction over many monitoring backends (e.g., Prometheus, Datadog), suitable for capturing JVM standard metrics and custom, domain-specific data. Use custom metrics when:

  • You need business or domain-level visibility (e.g., active users, order counts).
  • You want to measure operation durations or frequencies specific to your application logic.
  • Default JVM or server metrics are insufficient for diagnosing application performance.

Avoid or minimize custom metrics if you don't require granular observability or if you lack a monitoring stack to consume and act on the metrics. Creating unnecessary or overly fine-grained metrics can add overhead and noise.

Alternatives and Trade-offs

  • Manual Logging: Simple but less structured and higher latency; costly to parse and analyze.
  • Distributed Tracing (OpenTelemetry): Focuses on request flows rather than aggregated metric data.
  • Other Metrics Libraries (Dropwizard, Micrometer alternatives): May lack modern backend integrations or flexible tagging.

Micrometer's tag-based, vendor-neutral approach provides flexibility, but it requires careful metric design to avoid high-cardinality issues.

Prerequisites and Environment

  • Java Development Kit (JDK) 11 or later.
  • Maven or Gradle for build and dependency management.
  • Micrometer core and registry dependencies (example uses Prometheus).
  • Optional but recommended: Spring Boot 2.x or 3.x for easier dependency management and auto-configuration.

Setting Up Micrometer in a Java Application

Adding Micrometer to your project involves including core and registry dependencies and configuring the registry instance.

Dependencies

For Maven in pom.xml:

<dependencies>
  <dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
    <version>1.10.4</version>
  </dependency>
  <dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <version>1.10.4</version>
  </dependency>
</dependencies>

For Gradle:

dependencies {
    implementation 'io.micrometer:micrometer-core:1.10.4'
    implementation 'io.micrometer:micrometer-registry-prometheus:1.10.4'
}

Initializing the Prometheus Meter Registry

In standalone Java (non-Spring), you can create and expose the Prometheus registry as follows:

import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.micrometer.prometheus.PrometheusConfig;
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class MetricsServer {
    private final PrometheusMeterRegistry registry;

    public MetricsServer() {
        registry = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
    }

    public PrometheusMeterRegistry getRegistry() {
        return registry;
    }

    public void startHttpServer(int port) throws Exception {
        HttpServer server = HttpServer.create(new InetSocketAddress(port), 0);
        server.createContext("/metrics", httpExchange -> {
            String response = registry.scrape();
            httpExchange.getResponseHeaders().add("Content-Type", "text/plain; version=0.0.4; charset=utf-8");
            httpExchange.sendResponseHeaders(200, response.getBytes().length);
            try (OutputStream os = httpExchange.getResponseBody()) {
                os.write(response.getBytes());
            }
        });
        server.start();
        System.out.println("Prometheus metrics server started at http://localhost:" + port + "/metrics");
    }
}

This code instantiates a Prometheus meter registry and exposes metrics on a simple built-in HTTP server, which Prometheus can scrape.

End-to-End Example: Instrumenting a User Management Service

We will build a simple UserService class that tracks:

  • The total number of user creation requests (Counter).
  • The current number of active user sessions (Gauge).
  • The duration of password reset operations (Timer).

Step 1: Create Metrics Wrapper

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.MeterRegistry;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.TimeUnit;

public class UserServiceMetrics {

    private final Counter createUserRequests;
    private final AtomicInteger activeSessions;
    private final Timer passwordResetTimer;

    public UserServiceMetrics(MeterRegistry registry) {
        createUserRequests = registry.counter("user_service_create_requests_total", "endpoint", "/users/create");

        activeSessions = new AtomicInteger(0);
        Gauge.builder("user_service_active_sessions", activeSessions, AtomicInteger::get)
             .description("Number of active user sessions")
             .register(registry);

        passwordResetTimer = Timer.builder("user_service_password_reset_duration_seconds")
                .description("Duration of password reset operations")
                .tag("operation", "password_reset")
                .publishPercentiles(0.5, 0.95, 0.99) // Optional: percentiles for latency
                .register(registry);
    }

    public void incrementCreateUserRequests() {
        createUserRequests.increment();
    }

    public void sessionStarted() {
        activeSessions.incrementAndGet();
    }

    public void sessionEnded() {
        activeSessions.decrementAndGet();
    }

    public void recordPasswordResetDuration(Runnable task) {
        passwordResetTimer.record(task);
    }

    // For callable tasks with a result and checked exceptions
    public <T> T recordPasswordResetDuration(java.util.concurrent.Callable<T> task) throws Exception {
        return passwordResetTimer.recordCallable(task);
    }
}

Step 2: Integrate Metrics in Business Logic

public class UserService {
    private final UserServiceMetrics metrics;

    public UserService(MeterRegistry registry) {
        metrics = new UserServiceMetrics(registry);
    }

    public void createUser(String username) {
        // Simulated business logic: e.g. save user to DB
        simulateWork(200);

        metrics.incrementCreateUserRequests();
    }

    public void userLoggedIn() {
        metrics.sessionStarted();
    }

    public void userLoggedOut() {
        metrics.sessionEnded();
    }

    public void resetPassword(String username) {
        metrics.recordPasswordResetDuration(() -> {
            // Simulate password reset process
            simulateWork(500);
        });
    }

    private void simulateWork(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException ignored) {}
    }
}

Step 3: Run and Expose Metrics

Example main method using the MetricsServer to run and expose metrics:

public class Application {
    public static void main(String[] args) throws Exception {
        MetricsServer metricsServer = new MetricsServer();
        UserService userService = new UserService(metricsServer.getRegistry());

        metricsServer.startHttpServer(8081);

        // Simulate application activity
        userService.createUser("alice");
        userService.userLoggedIn();
        userService.resetPassword("alice");
        userService.userLoggedOut();

        // Keep application running so metrics can be scraped
        Thread.currentThread().join();
    }
}

This setup exposes /metrics on http://localhost:8081/metrics where Prometheus can scrape.

Verification Steps

  1. Run the application.
  2. Navigate to http://localhost:8081/metrics.
  3. Look for output containing:
# HELP user_service_create_requests_total Total number of create user requests
# TYPE user_service_create_requests_total counter
user_service_create_requests_total{endpoint="/users/create"} 1.0

# HELP user_service_active_sessions Number of active user sessions
# TYPE user_service_active_sessions gauge
user_service_active_sessions 0.0

# HELP user_service_password_reset_duration_seconds Duration of password reset operations
# TYPE user_service_password_reset_duration_seconds summary
user_service_password_reset_duration_seconds_count {operation="password_reset"} 1.0
user_service_password_reset_duration_seconds_sum {operation="password_reset"} 0.5

The exact counts may differ based on usage, but all three custom metrics should be present.

Production Failure Modes and Troubleshooting

  • Metrics Not Visible:
  • Confirm the HTTP server exposing /metrics is running and accessible.
  • Validate Prometheus scrape config matches the correct endpoint.
  • Check that Micrometer registry is properly instantiated and used consistently.
  • Unexpected Metric Values:
  • Ensure metrics update calls are executed at correct lifecycle points.
  • Prevent counter duplication by reusing metric instances—not creating new counters repeatedly.
  • High Cardinality and Performance Issues:
  • Avoid using unbounded tags (e.g., user IDs) which cause high cardinality.
  • Monitor memory use on metrics backend.
  • Thread Safety:
  • Use atomic types (e.g., AtomicInteger for Gauges) and built-in thread-safe Micrometer API.

Security Considerations

  • Restrict access to /metrics endpoint via network policies, firewalls, or authentication middleware.
  • Metrics may expose sensitive system details; avoid including sensitive data in labels or metric names.
  • Use HTTPS to protect telemetry data in transit.

Performance Considerations

  • Use asynchronous non-blocking metrics where possible.
  • Minimize additional work inside metric recording (no heavy computations or blocking calls).
  • Leverage batch or sampled metrics collection for high-frequency events.

Operational Safeguards

  • Document custom metrics clearly, including metric names, purpose, and units.
  • Configure monitoring alerts on key metrics thresholds.
  • Use tagging consistently to enable meaningful aggregation.
  • Review and prune obsolete metrics regularly to avoid metric growth.

Limitations

  • Micrometer focuses on metrics, not distributed tracing or logs—use in conjunction with other observability tools.
  • High tag cardinality can degrade backends and increase resource consumption.
  • Dependency on Micrometer registry and backend support limits supported metric types.

Summary

Defining Java custom metrics with Micrometer lets you capture application-specific insights vital for understanding and optimizing system behavior. By carefully designing counters, gauges, and timers integrated into your business logic, and exposing them for collection (e.g., by Prometheus), you can build robust observability. Managing production operational concerns including security, performance, and troubleshooting ensures your instrumentation remains a valuable asset without unnecessary risk or overhead.

FAQ

What differentiates a Counter from a Gauge in Micrometer?

A Counter is a strictly increasing value typically counting events like requests or errors. In contrast, a Gauge represents a value that can increase or decrease, such as current memory usage or active sessions.

Can I use Micrometer outside of Spring Boot?

Absolutely. Micrometer is a framework-agnostic metrics library and can be manually instantiated and configured in any Java application.

How do I avoid performance degradation due to excessive metrics?

Limit metrics number and avoid high-cardinality tags, use thread-safe atomic updates, and avoid heavy operations inside metric recording.

How can I expose metrics to Prometheus in non-Spring applications?

Set up an HTTP endpoint that returns PrometheusMeterRegistry.scrape() output, using built-in or third-party HTTP servers.

Does Micrometer support timing asynchronous operations?

Yes, Micrometer's Timer can measure durations of asynchronous or long-running operations via record(Runnable) or recordCallable(Callable&lt;T&gt;) methods.

Sources and further reading

Related reading