Introduction
Micrometer is a metrics instrumentation library for JVM-based applications that provides a simple facade over the instrumentation clients for the most popular monitoring systems. It's the metrics collection library used by Spring Boot Actuator and supports exporting to various monitoring systems like Prometheus, Atlas, Datadog, and more.
Core Concepts
Micrometer Architecture
// Basic components MeterRegistry -> Registry that holds meters Meter -> Metric instrument (Counter, Timer, Gauge, etc.) MeterFilter -> Modifies meters before registration Tags -> Key-value pairs for dimensional metrics
Setup and Dependencies
Maven Dependencies
<dependencies> <!-- Micrometer Core --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> <version>1.11.5</version> </dependency> <!-- Micrometer Registry for different monitoring systems --> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> <version>1.11.5</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-atlas</artifactId> <version>1.11.5</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-datadog</artifactId> <version>1.11.5</version> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-statsd</artifactId> <version>1.11.5</version> </dependency> <!-- Spring Boot Actuator (includes Micrometer) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>
Basic Meter Types
Counter
@Component
public class CounterMetrics {
private final MeterRegistry meterRegistry;
private final Counter apiRequestCounter;
private final Counter loginAttemptCounter;
public CounterMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// Create counters with different approaches
this.apiRequestCounter = Counter.builder("api.requests")
.description("Total number of API requests")
.tags("environment", "production")
.register(meterRegistry);
this.loginAttemptCounter = meterRegistry.counter("auth.login.attempts");
}
public void recordApiRequest(String endpoint, String method, int statusCode) {
// Counter with tags for dimensional data
Counter.builder("api.http.requests")
.tag("endpoint", endpoint)
.tag("method", method)
.tag("status", String.valueOf(statusCode))
.register(meterRegistry)
.increment();
}
public void recordLoginAttempt(String username, boolean success) {
loginAttemptCounter.increment();
Counter.builder("auth.login.results")
.tag("username", username)
.tag("success", String.valueOf(success))
.register(meterRegistry)
.increment();
}
public void recordUserRegistration(String source) {
// Using the counter directly
apiRequestCounter.increment();
// Or create ad-hoc counters
meterRegistry.counter("user.registrations", "source", source).increment();
}
}
Timer
@Component
public class TimerMetrics {
private final MeterRegistry meterRegistry;
private final Timer databaseQueryTimer;
private final Timer externalApiCallTimer;
public TimerMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.databaseQueryTimer = Timer.builder("database.query.duration")
.description("Time taken for database queries")
.publishPercentiles(0.5, 0.95, 0.99) // 50th, 95th, 99th percentiles
.publishPercentileHistogram()
.register(meterRegistry);
this.externalApiCallTimer = Timer.builder("external.api.call.duration")
.description("Time taken for external API calls")
.register(meterRegistry);
}
public void recordDatabaseQuery(String queryType, Runnable operation) {
databaseQueryTimer.record(() -> {
try {
operation.run();
} catch (Exception e) {
// Record failure
meterRegistry.counter("database.query.errors", "type", queryType).increment();
throw e;
}
});
}
public <T> T recordExternalApiCall(String service, String endpoint, Supplier<T> operation) {
return externalApiCallTimer.record(() -> {
try {
T result = operation.get();
meterRegistry.counter("external.api.calls",
"service", service,
"endpoint", endpoint,
"status", "success").increment();
return result;
} catch (Exception e) {
meterRegistry.counter("external.api.calls",
"service", service,
"endpoint", endpoint,
"status", "error").increment();
throw e;
}
});
}
public Timer.Sample startTimer() {
return Timer.start(meterRegistry);
}
public void stopTimer(Timer.Sample sample, String timerName, String... tags) {
sample.stop(Timer.builder(timerName)
.tags(tags)
.register(meterRegistry));
}
}
Gauge
@Component
public class GaugeMetrics {
private final MeterRegistry meterRegistry;
private final AtomicInteger activeUsers;
private final AtomicLong cacheSize;
private final List<String> queue;
public GaugeMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.activeUsers = new AtomicInteger(0);
this.cacheSize = new AtomicLong(0);
this.queue = new ArrayList<>();
setupGauges();
}
private void setupGauges() {
// Gauge for atomic values
Gauge.builder("app.active.users")
.description("Number of currently active users")
.tag("component", "session")
.register(meterRegistry, activeUsers);
// Gauge for collection size
Gauge.builder("app.queue.size")
.description("Current size of processing queue")
.register(meterRegistry, queue, List::size);
// Gauge for custom object state
Gauge.builder("app.cache.size")
.description("Current cache size in bytes")
.register(meterRegistry, cacheSize, AtomicLong::get);
// Gauge for method reference
Gauge.builder("jvm.memory.used")
.description("JVM memory used")
.baseUnit("bytes")
.register(meterRegistry, this, GaugeMetrics::getUsedMemory);
// Gauge for runtime metrics
Gauge.builder("system.cpu.usage")
.description("System CPU usage")
.baseUnit("percent")
.register(meterRegistry, this, GaugeMetrics::getCpuUsage);
}
public void userLoggedIn() {
activeUsers.incrementAndGet();
}
public void userLoggedOut() {
activeUsers.decrementAndGet();
}
public void addToQueue(String item) {
queue.add(item);
}
public void removeFromQueue(String item) {
queue.remove(item);
}
public void updateCacheSize(long size) {
cacheSize.set(size);
}
private double getUsedMemory() {
Runtime runtime = Runtime.getRuntime();
return runtime.totalMemory() - runtime.freeMemory();
}
private double getCpuUsage() {
// Simplified CPU usage calculation
// In production, use proper system metrics library
return Math.random() * 100; // Placeholder
}
}
Distribution Summary
@Component
public class DistributionMetrics {
private final MeterRegistry meterRegistry;
private final DistributionSummary requestSizeSummary;
private final DistributionSummary responseSizeSummary;
public DistributionMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.requestSizeSummary = DistributionSummary.builder("http.request.size")
.description("Size of HTTP requests in bytes")
.baseUnit("bytes")
.scale(1024) // Scale to kilobytes
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
this.responseSizeSummary = DistributionSummary.builder("http.response.size")
.description("Size of HTTP responses in bytes")
.baseUnit("bytes")
.register(meterRegistry);
}
public void recordRequestSize(String endpoint, long sizeInBytes) {
DistributionSummary.builder("http.request.size.by.endpoint")
.tag("endpoint", endpoint)
.register(meterRegistry)
.record(sizeInBytes);
}
public void recordResponseSize(String endpoint, int statusCode, long sizeInBytes) {
DistributionSummary.builder("http.response.size.by.endpoint")
.tag("endpoint", endpoint)
.tag("status", String.valueOf(statusCode))
.register(meterRegistry)
.record(sizeInBytes);
}
public void recordOrderValue(String customerType, double amount) {
DistributionSummary.builder("order.value")
.description("Value of customer orders")
.baseUnit("currency")
.tag("customer_type", customerType)
.publishPercentiles(0.5, 0.75, 0.95, 0.99)
.register(meterRegistry)
.record(amount);
}
public void recordProcessingTime(String processName, long durationMs) {
DistributionSummary.builder("process.duration")
.description("Duration of various processes")
.baseUnit("milliseconds")
.tag("process", processName)
.register(meterRegistry)
.record(durationMs);
}
}
Spring Boot Integration
Configuration
@Configuration
public class MetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config()
.commonTags(
"application", "my-application",
"environment", System.getenv().getOrDefault("ENVIRONMENT", "development"),
"instance", System.getenv().getOrDefault("HOSTNAME", "localhost")
);
}
@Bean
public MeterFilter meterFilter() {
return MeterFilter.deny(id -> {
String name = id.getName();
// Deny metrics that start with "jvm." in test environment
return name.startsWith("jvm.") &&
"test".equals(System.getenv().get("ENVIRONMENT"));
});
}
@Bean
public MeterFilter renameFilter() {
return MeterFilter.renameTag("http.server.requests", "uri", "endpoint");
}
@Bean
@ConditionalOnProperty(name = "metrics.prometheus.enabled", havingValue = "true")
public PrometheusMeterRegistry prometheusMeterRegistry(PrometheusConfig config, CollectorRegistry collectorRegistry) {
return new PrometheusMeterRegistry(config, collectorRegistry, Clock.SYSTEM);
}
}
// Application properties
@ConfigurationProperties(prefix = "management.metrics.export")
@Data
public class MetricsProperties {
private Prometheus prometheus = new Prometheus();
private Datadog datadog = new Datadog();
private Atlas atlas = new Atlas();
@Data
public static class Prometheus {
private boolean enabled = true;
private String path = "/actuator/prometheus";
private boolean descriptions = true;
private Step step = Step.of(Duration.ofSeconds(10));
}
@Data
public static class Datadog {
private boolean enabled = false;
private String apiKey;
private String uri = "https://api.datadoghq.com";
private Duration step = Duration.ofSeconds(10);
}
@Data
public static class Atlas {
private boolean enabled = false;
private String uri = "http://localhost:7101/api/v1/publish";
private Duration step = Duration.ofSeconds(10);
}
}
Spring Boot Actuator Endpoints
# application.yml
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus,info
base-path: /actuator
endpoint:
metrics:
enabled: true
prometheus:
enabled: true
metrics:
export:
prometheus:
enabled: true
datadog:
enabled: false
tags:
application: ${spring.application.name}
environment: ${spring.profiles.active}
distribution:
percentiles-histogram:
http.server.requests: true
percentiles:
http.server.requests: 0.5, 0.75, 0.95, 0.99
Advanced Meter Registry Configuration
Composite Registry
@Configuration
public class CompositeRegistryConfig {
@Bean
public CompositeMeterRegistry compositeMeterRegistry(
PrometheusMeterRegistry prometheusRegistry,
StatsdMeterRegistry statsdRegistry,
DatadogMeterRegistry datadogRegistry) {
CompositeMeterRegistry composite = new CompositeMeterRegistry();
// Add multiple registries
composite.add(prometheusRegistry);
composite.add(statsdRegistry);
// Conditionally add Datadog
if (datadogRegistry != null) {
composite.add(datadogRegistry);
}
return composite;
}
@Bean
@ConditionalOnProperty("metrics.statsd.enabled")
public StatsdMeterRegistry statsdMeterRegistry(StatsdConfig config, Clock clock) {
return new StatsdMeterRegistry(config, clock);
}
@Bean
@ConditionalOnProperty("metrics.datadog.enabled")
public DatadogMeterRegistry datadogMeterRegistry(DatadogConfig config, Clock clock) {
return new DatadogMeterRegistry(config, clock);
}
}
// Custom configuration
@Configuration
public class StatsdConfig implements StatsdConfig {
@Value("${metrics.statsd.host:localhost}")
private String host;
@Value("${metrics.statsd.port:8125}")
private int port;
@Override
public String get(String key) {
return null; // use defaults
}
@Override
public String host() {
return host;
}
@Override
public int port() {
return port;
}
@Override
public StatsdFlavor flavor() {
return StatsdFlavor.TELEGRAF;
}
}
Custom Metrics Service
Comprehensive Metrics Service
@Service
public class ApplicationMetricsService {
private final MeterRegistry meterRegistry;
private final Timer httpRequestTimer;
private final Counter businessEventCounter;
private final Gauge cacheHitRatioGauge;
private final AtomicLong cacheHits = new AtomicLong();
private final AtomicLong cacheMisses = new AtomicLong();
public ApplicationMetricsService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
this.httpRequestTimer = Timer.builder("http.requests")
.description("HTTP request duration")
.publishPercentiles(0.5, 0.95, 0.99)
.publishPercentileHistogram()
.register(meterRegistry);
this.businessEventCounter = Counter.builder("business.events")
.description("Business events counter")
.register(meterRegistry);
this.cacheHitRatioGauge = Gauge.builder("cache.hit.ratio")
.description("Cache hit ratio")
.register(meterRegistry, this,
service -> service.calculateHitRatio());
}
public void recordHttpRequest(String method, String endpoint, int status, long duration) {
httpRequestTimer.record(duration, TimeUnit.MILLISECONDS);
Counter.builder("http.requests.total")
.tag("method", method)
.tag("endpoint", endpoint)
.tag("status", String.valueOf(status))
.register(meterRegistry)
.increment();
}
public void recordBusinessEvent(String eventType, String entity, String action) {
businessEventCounter.increment();
Counter.builder("business.event.detailed")
.tag("type", eventType)
.tag("entity", entity)
.tag("action", action)
.register(meterRegistry)
.increment();
}
public void recordCacheHit(String cacheName) {
cacheHits.incrementAndGet();
meterRegistry.counter("cache.access", "name", cacheName, "result", "hit").increment();
}
public void recordCacheMiss(String cacheName) {
cacheMisses.incrementAndGet();
meterRegistry.counter("cache.access", "name", cacheName, "result", "miss").increment();
}
public void recordDatabaseQuery(String queryType, long duration, boolean success) {
Timer.builder("database.query")
.tag("type", queryType)
.tag("success", String.valueOf(success))
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
}
public void recordExternalServiceCall(String service, String operation, long duration, boolean success) {
Timer.builder("external.service.call")
.tag("service", service)
.tag("operation", operation)
.tag("success", String.valueOf(success))
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
}
public void recordQueueSize(String queueName, int size) {
Gauge.builder("queue.size")
.tag("name", queueName)
.register(meterRegistry, size);
}
public void recordError(String component, String errorType) {
Counter.builder("errors")
.tag("component", component)
.tag("type", errorType)
.register(meterRegistry)
.increment();
}
public void recordUserAction(String userId, String action, String resource) {
Counter.builder("user.actions")
.tag("user_id", userId)
.tag("action", action)
.tag("resource", resource)
.register(meterRegistry)
.increment();
}
private double calculateHitRatio() {
long hits = cacheHits.get();
long misses = cacheMisses.get();
long total = hits + misses;
return total > 0 ? (double) hits / total : 0.0;
}
// Method timer example
public <T> T timeMethodExecution(String methodName, Supplier<T> operation) {
return Timer.builder("method.execution")
.tag("method", methodName)
.register(meterRegistry)
.record(operation);
}
}
Aspect-Oriented Metrics
Metrics Aspect
@Aspect
@Component
public class MetricsAspect {
private final MeterRegistry meterRegistry;
private final ApplicationMetricsService metricsService;
public MetricsAspect(MeterRegistry meterRegistry, ApplicationMetricsService metricsService) {
this.meterRegistry = meterRegistry;
this.metricsService = metricsService;
}
@Around("@annotation(monitored)")
public Object monitorMethod(ProceedingJoinPoint joinPoint, Monitored monitored) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Timer.Sample sample = Timer.start(meterRegistry);
boolean success = false;
try {
Object result = joinPoint.proceed();
success = true;
return result;
} catch (Exception e) {
metricsService.recordError(className, e.getClass().getSimpleName());
throw e;
} finally {
sample.stop(Timer.builder("method.execution")
.tag("class", className)
.tag("method", methodName)
.tag("success", String.valueOf(success))
.register(meterRegistry));
}
}
@Around("@within(restController) || @annotation(restController)")
public Object monitorRestController(ProceedingJoinPoint joinPoint, RestController restController) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.currentRequestAttributes()).getRequest();
String method = request.getMethod();
String endpoint = request.getRequestURI();
Timer.Sample sample = Timer.start(meterRegistry);
boolean success = false;
int status = 500;
try {
Object result = joinPoint.proceed();
success = true;
status = 200; // Simplified
return result;
} catch (Exception e) {
status = determineStatusCode(e);
throw e;
} finally {
long duration = sample.stop(Timer.builder("http.request.detailed")
.tag("method", method)
.tag("endpoint", endpoint)
.tag("status", String.valueOf(status))
.register(meterRegistry));
metricsService.recordHttpRequest(method, endpoint, status, duration);
}
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void recordServiceError(Exception ex) {
metricsService.recordError("service", ex.getClass().getSimpleName());
}
private int determineStatusCode(Exception e) {
if (e instanceof IllegalArgumentException) {
return 400;
} else if (e instanceof AuthenticationException) {
return 401;
} else if (e instanceof AccessDeniedException) {
return 403;
} else if (e instanceof ResourceNotFoundException) {
return 404;
} else {
return 500;
}
}
}
// Custom annotation for monitoring
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitored {
String value() default "";
String[] tags() default {};
}
Web MVC Metrics
Web Metrics Configuration
@Configuration
public class WebMetricsConfig {
@Bean
public FilterRegistrationBean<MetricsFilter> metricsFilter(MeterRegistry registry) {
FilterRegistrationBean<MetricsFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new MetricsFilter(registry));
registration.addUrlPatterns("/*");
registration.setName("metricsFilter");
registration.setOrder(1);
return registration;
}
@Bean
public WebMvcTagsProvider webMvcTagsProvider() {
return new DefaultWebMvcTagsProvider() {
@Override
public Iterable<Tag> getTags(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Throwable exception) {
return Tags.of(
WebMvcTags.method(request),
WebMvcTags.uri(request),
WebMvcTags.status(response),
WebMvcTags.exception(exception),
Tag.of("client.ip", getClientIp(request)),
Tag.of("user.agent", getUserAgent(request))
);
}
};
}
private String getClientIp(HttpServletRequest request) {
String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader != null) {
return xfHeader.split(",")[0];
}
return request.getRemoteAddr();
}
private String getUserAgent(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
return userAgent != null ? userAgent : "unknown";
}
}
// Custom web metrics interceptor
@Component
public class CustomWebMetricsInterceptor implements HandlerInterceptor {
private final ThreadLocal<Timer.Sample> timerSample = new ThreadLocal<>();
private final MeterRegistry meterRegistry;
public CustomWebMetricsInterceptor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
timerSample.set(Timer.start(meterRegistry));
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
Timer.Sample sample = timerSample.get();
if (sample != null) {
String method = request.getMethod();
String uri = request.getRequestURI();
int status = response.getStatus();
sample.stop(Timer.builder("http.request.custom")
.tag("method", method)
.tag("uri", uri)
.tag("status", String.valueOf(status))
.tag("exception", ex != null ? ex.getClass().getSimpleName() : "none")
.register(meterRegistry));
timerSample.remove();
}
}
}
Database Metrics
Hibernate Metrics
@Component
public class DatabaseMetrics {
private final MeterRegistry meterRegistry;
private final StatisticsService statisticsService;
public DatabaseMetrics(MeterRegistry meterRegistry,
SessionFactory sessionFactory) {
this.meterRegistry = meterRegistry;
this.statisticsService = sessionFactory.getStatistics();
setupHibernateMetrics();
}
private void setupHibernateMetrics() {
// Hibernate query metrics
Gauge.builder("hibernate.queries")
.description("Number of Hibernate queries executed")
.register(meterRegistry, statisticsService, Statistics::getQueryExecutionCount);
// Hibernate cache metrics
Gauge.builder("hibernate.cache.hits")
.description("Hibernate second level cache hits")
.register(meterRegistry, statisticsService, Statistics::getSecondLevelCacheHitCount);
Gauge.builder("hibernate.cache.misses")
.description("Hibernate second level cache misses")
.register(meterRegistry, statisticsService, Statistics::getSecondLevelCacheMissCount);
// Connection metrics
Gauge.builder("hibernate.connections")
.description("Hibernate connections obtained")
.register(meterRegistry, statisticsService, Statistics::getConnectCount);
}
public void recordQueryExecution(String query, long duration, boolean success) {
Timer.builder("database.query.execution")
.tag("query_type", classifyQuery(query))
.tag("success", String.valueOf(success))
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
}
public void recordTransaction(boolean committed, long duration) {
Timer.builder("database.transaction")
.tag("committed", String.valueOf(committed))
.register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
Counter.builder("database.transactions")
.tag("outcome", committed ? "committed" : "rolled_back")
.register(meterRegistry)
.increment();
}
private String classifyQuery(String query) {
if (query == null) return "unknown";
query = query.trim().toUpperCase();
if (query.startsWith("SELECT")) return "select";
if (query.startsWith("INSERT")) return "insert";
if (query.startsWith("UPDATE")) return "update";
if (query.startsWith("DELETE")) return "delete";
return "other";
}
}
Testing Metrics
Metrics Testing
@SpringBootTest
@ExtendWith(SpringExtension.class)
class MetricsTest {
@Autowired
private MeterRegistry meterRegistry;
@Autowired
private ApplicationMetricsService metricsService;
@Test
void testCounterIncrement() {
// Given
Counter counter = meterRegistry.counter("test.counter");
long initialCount = counter.count();
// When
metricsService.recordBusinessEvent("TEST", "ENTITY", "ACTION");
// Then
assertThat(counter.count()).isEqualTo(initialCount + 1);
}
@Test
void testTimerRecording() {
// When
metricsService.timeMethodExecution("testMethod", () -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "result";
});
// Then
Timer timer = meterRegistry.find("method.execution").timer();
assertThat(timer).isNotNull();
assertThat(timer.count()).isEqualTo(1);
assertThat(timer.totalTime(TimeUnit.MILLISECONDS)).isGreaterThan(100);
}
@Test
void testGaugeValue() {
// Given
metricsService.recordCacheHit("testCache");
metricsService.recordCacheMiss("testCache");
// When
Gauge gauge = meterRegistry.find("cache.hit.ratio").gauge();
// Then
assertThat(gauge).isNotNull();
assertThat(gauge.value()).isBetween(0.0, 1.0);
}
@Test
void testMetricsWithTags() {
// When
metricsService.recordHttpRequest("GET", "/api/test", 200, 150L);
// Then
Counter counter = meterRegistry.find("http.requests.total")
.tag("method", "GET")
.tag("endpoint", "/api/test")
.tag("status", "200")
.counter();
assertThat(counter).isNotNull();
assertThat(counter.count()).isEqualTo(1);
}
}
// Test configuration
@TestConfiguration
public class TestMetricsConfig {
@Bean
public MeterRegistry meterRegistry() {
return new SimpleMeterRegistry();
}
}
Best Practices
Production Configuration
@Component
public class MetricsBestPractices {
private final MeterRegistry meterRegistry;
public MetricsBestPractices(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
// 1. Use meaningful metric names
public void recordMeaningfulMetrics() {
// Good
meterRegistry.counter("http.requests", "method", "GET", "status", "200");
// Avoid
meterRegistry.counter("cnt1");
}
// 2. Use tags for dimensionality
public void recordDimensionalMetrics() {
Counter.builder("api.calls")
.tag("service", "user-service")
.tag("operation", "getUser")
.tag("status", "success")
.register(meterRegistry)
.increment();
}
// 3. Avoid high cardinality tags
public void avoidHighCardinality() {
// Avoid - user_id can have thousands of values
// meterRegistry.counter("user.actions", "user_id", userId);
// Better - group by user type or segment
meterRegistry.counter("user.actions", "user_type", getUserType(userId));
}
// 4. Use appropriate meter types
public void useAppropriateMeters() {
// For counting events - use Counter
meterRegistry.counter("user.logins").increment();
// For measuring duration - use Timer
Timer.builder("database.query.time").register(meterRegistry)
.record(duration, TimeUnit.MILLISECONDS);
// For current value - use Gauge
Gauge.builder("queue.size").register(meterRegistry, queueSize);
// For size distribution - use DistributionSummary
DistributionSummary.builder("request.size").register(meterRegistry)
.record(requestSize);
}
// 5. Clean up unused metrics
public void cleanupMetrics() {
// Remove custom metrics when no longer needed
meterRegistry.remove(meterRegistry.find("temporary.metric").counter());
}
private String getUserType(String userId) {
// Logic to determine user type/segment
return "premium"; // or "standard", "trial", etc.
}
}
Conclusion
Micrometer provides a powerful and flexible way to collect metrics in Java applications:
- Vendor-neutral - Supports multiple monitoring systems
- Spring Boot integration - Seamless integration with Spring Boot Actuator
- Rich meter types - Counters, Timers, Gauges, Distribution Summaries
- Dimensional metrics - Tags for powerful filtering and aggregation
- Performance optimized - Designed for high-performance applications
Key benefits:
- Standardized metrics collection across different monitoring systems
- Powerful dimensional data model
- Flexible configuration and customization
- Production-ready used by Spring Boot and many large-scale applications
By following these patterns and best practices, you can implement comprehensive metrics collection that provides deep insights into your application's performance and behavior.