Micrometer và Prometheus trong Spring Boot: Metric Actuator trên Production
Tích hợp Micrometer và Prometheus trên Spring Boot 4: mô hình truy cập Actuator, cấu hình scrape, metric HTTP, health probe và quy tắc cảnh báo.

Spring Boot Actuator cung cấp các endpoint health, metric và chẩn đoán, còn Micrometer là lớp facade metric đứng phía sau chúng. Thêm phụ thuộc micrometer-registry-prometheus sẽ tạo ra endpoint /actuator/prometheus để Prometheus scrape theo chu kỳ cố định. Các ví dụ dưới đây nhắm tới Spring Boot 4.1 (bản GA hiện tại, 4.2 đang ở giai đoạn release candidate), Micrometer 1.16 và Prometheus Java client 1.x.
Tích hợp Micrometer của Spring Boot với Prometheus gồm ba bước: thêm spring-boot-starter-actuator và micrometer-registry-prometheus, thêm prometheus vào management.endpoints.web.exposure.include, rồi trỏ một scrape_config của Prometheus tới /actuator/prometheus. Các metric JVM, HTTP, HikariCP và hệ thống được công bố mà không cần viết thêm code.
Thiết Lập Micrometer trong Spring Boot: Actuator và Registry Prometheus
Các Phụ Thuộc Maven Cần Thiết
Starter Actuator kích hoạt các endpoint và kéo theo Micrometer. Registry Prometheus định dạng mọi meter theo định dạng exposition của Prometheus. Starter AspectJ chỉ cần thiết cho các annotation @Timed và @Counted.
<!-- pom.xml -->
<!-- Actuator + Micrometer + Prometheus (Spring Boot 4.x) -->
<dependencies>
<!-- Spring Boot Actuator: monitoring endpoints, includes Micrometer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Micrometer Registry Prometheus (Prometheus Java client 1.x) -->
<!-- Exposes metrics in Prometheus format at /actuator/prometheus -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<!-- AspectJ for @Timed and @Counted -->
<!-- Spring Boot 4 renamed spring-boot-starter-aop to spring-boot-starter-aspectj -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
</dependencies>Hai cái bẫy về phiên bản vẫn xuất hiện trong các hướng dẫn cũ. Kể từ Micrometer 1.13 (Spring Boot 3.3), micrometer-registry-prometheus được xây dựng trên Prometheus Java client 1.x; code 0.x cũ nằm trong module micrometer-registry-prometheus-simpleclient đã bị deprecated, và tài liệu Micrometer Prometheus chỉ xem đó là giải pháp tạm thời. Ngoài ra, trên Spring Boot 4, một pom.xml vẫn khai báo spring-boot-starter-aop sẽ không còn resolve được: hướng dẫn chuyển đổi Spring Boot 4.0 đổi tên nó thành spring-boot-starter-aspectj.
Cấu Hình Endpoint Actuator
Mặc định, chỉ endpoint health được phơi qua HTTP. Endpoint /actuator/prometheus tồn tại ngay khi registry có mặt trong classpath, nhưng sẽ trả về 404 cho tới khi được thêm vào danh sách exposure.
Spring Boot 3.4 đã thay các công tắc bật/tắt (management.endpoints.enabled-by-default, management.endpoint.<id>.enabled) bằng một mô hình truy cập, và các thuộc tính cũ đã bị deprecated. Mẫu opt-in đặt quyền truy cập mặc định là none rồi cấp read-only cho từng endpoint:
# application.yml
# Actuator configuration for production (Spring Boot 3.4+ access model)
management:
endpoints:
# Every endpoint is inaccessible unless granted below
access:
default: none
web:
exposure:
# Endpoints exposed over HTTP
# health, info, prometheus are the minimum for monitoring
include: health,info,prometheus,metrics,env,loggers
base-path: /actuator
endpoint:
health:
access: read-only
show-details: when-authorized
show-components: when-authorized
info:
access: read-only
prometheus:
access: read-only
metrics:
access: read-only
env:
access: read-only
# Mask sensitive values for anonymous callers
show-values: when-authorized
loggers:
# unrestricted allows POST to change a log level at runtime
access: unrestrictedExposure và access là hai cơ chế kiểm soát riêng biệt. exposure.include quyết định endpoint có được ánh xạ qua HTTP hay không; access quyết định endpoint cho phép làm gì sau khi đã được ánh xạ. Một file thuộc tính vẫn dùng enabled: true vẫn chạy được kèm cảnh báo deprecation, và đó chính là lúc nên chuyển đổi.
Tùy chọn show-details: when-authorized hiển thị chi tiết health chỉ với người dùng đã xác thực và có vai trò phù hợp. Bản thân các endpoint được bảo vệ bằng một SecurityFilterChain riêng:
// Securing Actuator endpoints (Spring Boot 4, Spring Security 7)
package com.example.monitoring.config;
import static org.springframework.security.web.access.IpAddressAuthorizationManager.hasIpAddress;
import org.springframework.boot.security.autoconfigure.actuate.web.servlet.EndpointRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration(proxyBeanMethods = false)
public class ActuatorSecurityConfig {
@Bean
SecurityFilterChain actuatorSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(EndpointRequest.toAnyEndpoint())
.authorizeHttpRequests(auth -> auth
// Health and info public for load balancers
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
// Prometheus accessible from the internal network only
.requestMatchers(EndpointRequest.to("prometheus")).access(hasIpAddress("10.0.0.0/8"))
// Other endpoints restricted to admins
.anyRequest().hasRole("ACTUATOR_ADMIN")
)
.httpBasic(basic -> {})
.build();
}
}Trong Spring Boot 4, EndpointRequest đã chuyển sang org.springframework.boot.security.autoconfigure.actuate.web.servlet, như được trình bày trong tài liệu tham khảo endpoint Actuator. DSL Java AuthorizedUrl của authorizeHttpRequests không có phương thức hasIpAddress: quy tắc IP đi qua IpAddressAuthorizationManager.hasIpAddress, có sẵn từ Spring Security 6.3.
Metric Tùy Chỉnh với Micrometer
Counter, Gauge và Timer cho Ứng Dụng
Micrometer cung cấp nhiều loại metric phù hợp với từng tình huống. Counter đo các sự kiện tích lũy, gauge đo giá trị tức thời và timer đo thời lượng của một thao tác.
// Custom business metrics service
package com.example.monitoring.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
@Service
public class OrderMetricsService {
// Counter for orders created
private final Counter ordersCreatedCounter;
// Timer to measure processing duration
private final Timer orderProcessingTimer;
// Atomic value for pending orders gauge
private final AtomicInteger pendingOrdersCount = new AtomicInteger(0);
public OrderMetricsService(MeterRegistry registry) {
// Exported as orders_created_total: the _total suffix is added by the registry
this.ordersCreatedCounter = Counter.builder("orders.created")
.description("Total number of orders created")
.tag("channel", "web")
.register(registry);
// Timer with histogram buckets: percentiles computed server-side in PromQL
this.orderProcessingTimer = Timer.builder("orders.processing.duration")
.description("Order processing duration")
.publishPercentileHistogram()
.register(registry);
// Gauge linked to atomic value
// Read on each scrape
Gauge.builder("orders.pending.count", pendingOrdersCount, AtomicInteger::get)
.description("Number of orders pending processing")
.register(registry);
}
public void recordOrderCreated() {
ordersCreatedCounter.increment();
pendingOrdersCount.incrementAndGet();
}
public void recordOrderProcessed(Runnable processingLogic) {
// Measures execution duration
orderProcessingTimer.record(processingLogic);
pendingOrdersCount.decrementAndGet();
}
public <T> T recordOrderProcessedWithResult(Supplier<T> processingLogic) {
return orderProcessingTimer.record(processingLogic);
}
}Có hai chi tiết đặt tên quan trọng khi dùng registry Prometheus. Tên meter của Micrometer dùng dấu chấm và được chuyển sang snake case (orders.processing.duration trở thành orders_processing_duration_seconds). Counter tự động nhận hậu tố _total, nên đặt tên counter là orders.created.total chỉ gây nhiễu.
publishPercentiles(0.5, 0.95, 0.99) tính percentile bên trong từng JVM, và các giá trị này không thể tổng hợp giữa các instance. publishPercentileHistogram() công bố các bucket mà histogram_quantile() có thể tổng hợp trên toàn bộ cụm instance, và đây là lựa chọn phù hợp khi dùng Prometheus.
Annotation @Timed và @Counted
Để tránh viết code lặp, Micrometer cung cấp các annotation đo đạc phương thức thông qua AspectJ.
// Automatic instrumentation with annotations
package com.example.monitoring.service;
import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final PaymentValidator paymentValidator;
public PaymentService(PaymentValidator paymentValidator) {
this.paymentValidator = paymentValidator;
}
// @Timed creates a Timer
// Measures each call and publishes count, sum, max and histogram buckets
@Timed(
value = "payment.process.duration",
description = "Payment processing duration",
histogram = true
)
public PaymentResult processPayment(PaymentRequest request) {
// Call through another bean: the proxy intercepts it
paymentValidator.validate(request);
return executePayment(request);
}
// @Counted increments a counter on each call
// Useful for discrete events
@Counted(
value = "payment.refunds",
description = "Total number of refunds"
)
public void refundPayment(String transactionId) {
// Refund logic
}
}# application.yml
# Enables TimedAspect, CountedAspect and the other Micrometer annotation aspects
management:
observations:
annotations:
enabled: trueKể từ Spring Boot 3.2, không còn cần khai báo thủ công các bean TimedAspect và CountedAspect: đặt management.observations.annotations.enabled=true cùng với spring-boot-starter-aspectj trong classpath sẽ tự động cấu hình chúng. Giá trị mặc định của thuộc tính này là false, điều giải thích phần lớn các báo cáo kiểu "@Timed của tôi không làm gì cả".
Annotation @Timed và @Counted chỉ hoạt động trên các phương thức public của bean Spring được gọi qua proxy. Lời gọi từ một phương thức sang phương thức khác trong cùng class sẽ bỏ qua proxy và không được đo đạc, còn annotation đặt trên phương thức private sẽ bị bỏ qua. Gắn annotation lên một controller Spring MVC vốn đã được đo đạc sẽ tạo ra observation trùng lặp.
Sẵn sàng chinh phục phỏng vấn Spring Boot?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Endpoint Health Tùy Chỉnh
Health Indicator cho Nghiệp Vụ
Health Indicator kiểm tra trạng thái của các phụ thuộc bên ngoài và các thành phần nghiệp vụ trọng yếu. Spring Boot cung cấp sẵn indicator cho cơ sở dữ liệu, Redis và nhiều dịch vụ phổ biến khác. Trong Spring Boot 4, API health nằm trong module riêng spring-boot-health, và Health cùng HealthIndicator đã chuyển từ org.springframework.boot.actuate.health sang org.springframework.boot.health.contributor.
// Health indicator for payment gateway (Spring Boot 4 packages)
package com.example.monitoring.health;
import org.springframework.boot.health.contributor.Health;
import org.springframework.boot.health.contributor.HealthIndicator;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import java.time.Duration;
import java.time.Instant;
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final RestClient restClient;
private final String gatewayHealthUrl;
public PaymentGatewayHealthIndicator(RestClient.Builder restClientBuilder) {
this.restClient = restClientBuilder.build();
this.gatewayHealthUrl = "https://api.payment-gateway.com/health";
}
@Override
public Health health() {
Instant start = Instant.now();
try {
// Call gateway health endpoint
var response = restClient.get()
.uri(gatewayHealthUrl)
.retrieve()
.toBodilessEntity();
Duration responseTime = Duration.between(start, Instant.now());
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("responseTime", responseTime.toMillis() + "ms")
.withDetail("statusCode", response.getStatusCode().value())
.build();
} else {
return Health.down()
.withDetail("statusCode", response.getStatusCode().value())
.withDetail("reason", "Unexpected status code")
.build();
}
} catch (Exception e) {
Duration responseTime = Duration.between(start, Instant.now());
return Health.down()
.withDetail("error", e.getClass().getSimpleName())
.withDetail("message", e.getMessage())
.withDetail("responseTime", responseTime.toMillis() + "ms")
.build();
}
}
}Indicator này xuất hiện trong /actuator/health dưới tên paymentGateway, tức tên bean bỏ đi hậu tố HealthIndicator.
Health Group cho Kubernetes
Spring Boot 4 bật sẵn probe liveness và readiness trên mọi nền tảng, không chỉ khi phát hiện Kubernetes. Các group /actuator/health/liveness và /actuator/health/readiness tồn tại mà không cần cấu hình gì, và không cần khai báo thủ công bean LivenessStateHealthIndicator hay ReadinessStateHealthIndicator. Cấu hình chỉ cần thiết khi muốn thêm thành phần vào một group hoặc định nghĩa group tùy chỉnh:
# application.yml
# Health groups configuration for Kubernetes
management:
endpoint:
health:
probes:
# true by default in Spring Boot 4
enabled: true
# Also serve the groups at /livez and /readyz on the main server port
add-additional-paths: true
group:
# Readiness: can the application receive traffic?
readiness:
include: readinessState,db
# Custom group for critical dependencies
critical:
include: db,paymentGateway
show-details: when-authorizedGroup liveness nên chỉ giới hạn ở livenessState. Đưa cơ sở dữ liệu hoặc một gateway bên ngoài vào liveness sẽ khiến Kubernetes khởi động lại mọi pod khi phụ thuộc đó gặp sự cố, biến một sự cố cục bộ thành sự cố toàn phần. Các phụ thuộc bên ngoài thuộc về readiness, và chỉ khi ứng dụng không thể phục vụ bất kỳ yêu cầu nào nếu thiếu chúng.
Probe Kubernetes sau đó trỏ tới các endpoint chuyên biệt:
# kubernetes-deployment.yml
# Kubernetes probes configuration
spec:
containers:
- name: order-service
livenessProbe:
httpGet:
path: /livez
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3Với add-additional-paths: true, các probe vẫn nằm trên cổng ứng dụng ngay cả khi Actuator chạy trên một cổng quản trị riêng, nhờ vậy một server chính bị lỗi vẫn được probe phát hiện.
Tích Hợp Prometheus với Spring Boot và Grafana
Cấu Hình Scraping của Prometheus
Prometheus thu thập metric bằng cách truy vấn endpoint /actuator/prometheus định kỳ. Cấu hình scrape xác định các mục tiêu và đường dẫn metric, vì Prometheus mặc định dùng /metrics.
# prometheus.yml
# Prometheus configuration for Spring Boot
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'spring-boot-apps'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
static_configs:
- targets:
- 'order-service:9090'
- 'payment-service:9090'
- 'inventory-service:9090'
# Relabeling to strip the port from the instance label
relabel_configs:
- source_labels: [__address__]
target_label: instance
regex: '([^:]+):\d+'
replacement: '${1}'
# Kubernetes service discovery
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
# Only scrape pods with annotation
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)Các target dùng cổng 9090 vì cấu hình production ở phần sau chuyển Actuator sang một cổng quản trị riêng. Nếu không có management.server.port, target scrape là cổng của ứng dụng.
Metric Spring Boot Actuator Được Công Bố Mặc Định
Actuator kết hợp Micrometer phơi bày các metric JVM, process, HikariCP và HTTP mà không cần code. Các truy vấn PromQL sau bao quát những metric được theo dõi đầu tiên trong production:
# PromQL queries for JVM monitoring
# Heap memory usage, summed across heap pools
sum by (instance) (jvm_memory_used_bytes{area="heap"})
# Heap usage percentage
sum by (instance) (jvm_memory_used_bytes{area="heap"})
/ sum by (instance) (jvm_memory_max_bytes{area="heap"}) * 100
# Live threads
jvm_threads_live_threads
# Garbage collection: seconds spent paused per second
rate(jvm_gc_pause_seconds_sum[5m])
# GC pauses per minute
rate(jvm_gc_pause_seconds_count[1m]) * 60
# CPU used by the JVM process
process_cpu_usage
# Active database connections
hikaricp_connections_active
# Connection pool utilization
hikaricp_connections_active / hikaricp_connections_max * 100jvm_memory_used_bytes{area="heap"} trả về một chuỗi cho mỗi memory pool (Eden, Survivor, Old Gen với G1). Chia nó cho giá trị max mà không có sum by (instance) sẽ tạo ra tỷ lệ theo từng pool, trông sai lệch trên dashboard.
Một bean MeterBinder bổ sung các gauge cấp ứng dụng mà metric mặc định không bao gồm:
// Additional JVM metrics
package com.example.monitoring.metrics;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.stereotype.Component;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
@Component
public class CustomJvmMetrics implements MeterBinder {
@Override
public void bindTo(MeterRegistry registry) {
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
// System load average
Gauge.builder("system.load.average", osBean, OperatingSystemMXBean::getSystemLoadAverage)
.description("System load average over 1 minute")
.register(registry);
// Available processors count
Gauge.builder("system.cpu.count", osBean, OperatingSystemMXBean::getAvailableProcessors)
.description("Number of available processors")
.register(registry);
// Application uptime
Gauge.builder("application.uptime.seconds",
ManagementFactory.getRuntimeMXBean(),
bean -> bean.getUptime() / 1000.0)
.description("Application uptime in seconds")
.register(registry);
}
}system.cpu.count và process.uptime đã được các binder có sẵn của Micrometer công bố. Ví dụ trên minh họa mẫu triển khai, còn một binder thực tế chỉ nên đăng ký những gì metric mặc định còn thiếu.
Dashboard Grafana Sẵn Có
Grafana có các dashboard cộng đồng cho Spring Boot hoạt động trên metric chuẩn của Actuator. Một dashboard viết tay dùng các loại panel timeseries và stat:
{
"annotations": {
"list": []
},
"panels": [
{
"title": "Request Rate",
"type": "timeseries",
"targets": [
{
"expr": "sum by (method, uri, status) (rate(http_server_requests_seconds_count{application=\"$application\"}[5m]))",
"legendFormat": "{{method}} {{uri}} - {{status}}"
}
]
},
{
"title": "Response Time P99",
"type": "timeseries",
"targets": [
{
"expr": "histogram_quantile(0.99, sum by (le, method, uri) (rate(http_server_requests_seconds_bucket{application=\"$application\"}[5m])))",
"legendFormat": "{{method}} {{uri}}"
}
]
},
{
"title": "Error Rate",
"type": "stat",
"targets": [
{
"expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\",status=~\"5..\"}[5m])) / sum(rate(http_server_requests_seconds_count{application=\"$application\"}[5m])) * 100"
}
]
}
]
}Nhiều dashboard sao chép từ các hướng dẫn cũ vẫn dùng loại panel graph và singlestat. Singlestat đã bị loại bỏ trong Grafana 8, và Grafana 12 đã gỡ bỏ hoàn toàn hỗ trợ AngularJS, vì vậy các panel đó chỉ hiển thị sau khi được chuyển sang timeseries và stat.
Để nhập một dashboard cộng đồng: Grafana > Dashboards > New > Import, rồi nhập ID 4701 (JVM Micrometer) hoặc 12900 (SpringBoot APM). Kiểm tra loại panel sau khi nhập: các bản sửa đổi cũ của dashboard cộng đồng có thể vẫn chứa panel Angular mà Grafana 12 phải chuyển đổi.
Cảnh Báo với Prometheus
Quy Tắc Cảnh Báo Cốt Lõi
Quy tắc cảnh báo của Prometheus kích hoạt thông báo khi metric vượt qua các ngưỡng tới hạn.
# alerting-rules.yml
# Alert rules for Spring Boot applications
groups:
- name: spring-boot-alerts
rules:
# Alert if application is down
- alert: ApplicationDown
expr: up{job="spring-boot-apps"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Application {{ $labels.instance }} is down"
description: "{{ $labels.instance }} has been down for more than 1 minute"
# Alert on HTTP error rate
- alert: HighErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (application)
/
sum(rate(http_server_requests_seconds_count[5m])) by (application)
> 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "High error rate on {{ $labels.application }}"
description: "Error rate is {{ $value | humanizePercentage }}"
# Alert on P99 latency, aggregated per application
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum by (le, application) (rate(http_server_requests_seconds_bucket[5m]))
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "High latency on {{ $labels.application }}"
description: "P99 latency is {{ $value | humanizeDuration }}"
# Heap memory alert
- alert: HighHeapUsage
expr: |
sum by (instance) (jvm_memory_used_bytes{area="heap"})
/ sum by (instance) (jvm_memory_max_bytes{area="heap"})
> 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "High heap memory usage on {{ $labels.instance }}"
description: "Heap usage is at {{ $value | humanizePercentage }}"
# Database connection pool exhausted alert
- alert: DatabaseConnectionPoolExhausted
expr: |
hikaricp_connections_active
/ hikaricp_connections_max
> 0.9
for: 2m
labels:
severity: critical
annotations:
summary: "Database connection pool nearly exhausted"
description: "{{ $value | humanizePercentage }} of connections in use"
# Excessive GC alert
- alert: HighGCPause
expr: |
rate(jvm_gc_pause_seconds_sum[5m])
/ rate(jvm_gc_pause_seconds_count[5m])
> 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "High GC pause time"
description: "Average GC pause is {{ $value | humanizeDuration }}"Các cảnh báo trên bao quát những vấn đề sản xuất phổ biến nhất: tính sẵn sàng, hiệu năng và tài nguyên. Quy tắc HighLatency cần sum by (le, ...): thiếu nó, histogram_quantile tính một P99 cho mỗi chuỗi (theo URI, method và status), và chỉ một endpoint chậm với ba lời gọi cũng đủ kích hoạt cảnh báo.
Metric HTTP và Metric Cơ Sở Dữ Liệu trong Spring Boot
Đo Đạc Tự Động Yêu Cầu HTTP
Spring Boot đo đạc mọi yêu cầu Spring MVC và WebFlux đầu vào thông qua Observation API và công bố timer http.server.requests, gắn các tag method, uri, status, outcome và exception.
# application.yml
# HTTP metrics configuration
management:
metrics:
distribution:
# Histogram buckets for server-side percentiles
percentiles-histogram:
http.server.requests: true
# SLO buckets: count requests faster than each boundary
slo:
http.server.requests: 100ms, 500ms, 1s, 2s
tags:
# Common tags added to every meter
application: ${spring.application.name}
environment: ${spring.profiles.active:default}Các interface WebMvcTagsContributor và WebMvcTagsProvider xuất hiện trong nhiều ví dụ đã bị loại bỏ từ Spring Boot 3.0. Tag tùy chỉnh giờ được thêm bằng cách kế thừa DefaultServerRequestObservationConvention, như tài liệu tham khảo metric của Spring Boot mô tả:
// Adds custom tags to http.server.requests (Spring Boot 3.0+)
package com.example.monitoring.config;
import io.micrometer.common.KeyValue;
import io.micrometer.common.KeyValues;
import org.springframework.http.server.observation.DefaultServerRequestObservationConvention;
import org.springframework.http.server.observation.ServerRequestObservationContext;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
@Component
public class TenantObservationConvention extends DefaultServerRequestObservationConvention {
@Override
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
HttpServletRequest request = context.getCarrier();
String userId = request.getHeader("X-User-Id");
String tenantId = request.getHeader("X-Tenant-Id");
// Keep the default tags and add two bounded ones
return super.getLowCardinalityKeyValues(context).and(
KeyValue.of("user.type", userId != null ? "authenticated" : "anonymous"),
KeyValue.of("tenant", tenantId != null ? tenantId : "default")
);
}
}Chỉ các key-value có cardinality thấp mới trở thành label của Prometheus. Một tag tenant chấp nhận được với vài chục tenant nhưng trở thành vấn đề cardinality khi có hàng nghìn tenant. Các giá trị có cardinality cao (ID người dùng, ID đơn hàng) thuộc về getHighCardinalityKeyValues, vốn chỉ đưa vào trace. Phần tracing của Observation API được trình bày chi tiết trong observability Spring Boot với OpenTelemetry.
Metric HikariCP và Truy Vấn SQL
Metric của pool HikariCP được phơi bày tự động với tiền tố hikaricp, gắn tag theo tên pool, bên cạnh các gauge chung jdbc.connections. Đặt tên pool giúp tag dễ đọc:
# application.yml
# HikariCP pool configuration
spring:
datasource:
hikari:
# Becomes the "pool" tag on hikaricp_* metrics
pool-name: OrderServicePool
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000Spring Boot không đo thời gian ở cấp truy vấn. Dự án datasource-micrometer bọc DataSource và tạo một observation cho mỗi kết nối, truy vấn và result set, cho ra cả metric lẫn span trace. Dòng 2.x nhắm tới Spring Boot 4, dòng 1.x nhắm tới Spring Boot 3:
<!-- pom.xml -->
<!-- JDBC observations: 2.x for Spring Boot 4, 1.x for Spring Boot 3 -->
<dependency>
<groupId>net.ttddyy.observation</groupId>
<artifactId>datasource-micrometer-spring-boot</artifactId>
<version>2.0.1</version>
</dependency>Cách này thay thế cách làm thủ công là bọc DataSource trong một ProxyDataSourceBuilder với listener tự viết, vốn bỏ qua auto-configuration DataSource của Spring Boot và phải cập nhật theo mỗi lần nâng cấp Boot. Truy vấn chậm thường là dấu hiệu của mẫu N+1, được trình bày trong giải pháp truy vấn N+1 với Spring Data JPA.
Thực Hành Tốt Nhất cho Sản Xuất
Cardinality của Metric
Cardinality quá cao làm giảm hiệu năng của Prometheus. Mỗi tổ hợp tag duy nhất tạo ra một chuỗi thời gian riêng biệt.
// AVOID: unbounded cardinality
package com.example.monitoring.antipattern;
@Service
public class AntiPatternHighCardinality {
private final MeterRegistry registry;
// BAD: userId creates one series per user
public void trackUserAction(String userId, String action) {
Counter.builder("user.actions")
.tag("userId", userId) // Millions of possible values
.tag("action", action)
.register(registry)
.increment();
}
}// Controlled cardinality
package com.example.monitoring.bestpractice;
@Service
public class GoodPracticeCardinality {
private final MeterRegistry registry;
// GOOD: user category instead of ID
public void trackUserAction(User user, String action) {
Counter.builder("user.actions")
.tag("userType", user.getSubscriptionType()) // FREE, PREMIUM, ENTERPRISE
.tag("action", action)
.register(registry)
.increment();
}
// GOOD: let a timer histogram bucket durations
public void trackResponseTime(long responseTimeMs) {
Timer.builder("response.time")
.publishPercentileHistogram()
.register(registry)
.record(Duration.ofMillis(responseTimeMs));
}
}Một Timer có bucket histogram trả lời các câu hỏi về độ trễ tốt hơn một counter với tag bucket tự chế: PromQL có thể tính bất kỳ percentile nào từ các bucket, và ranh giới bucket luôn nhất quán giữa các service.
Cấu Hình Sẵn Sàng cho Sản Xuất
# application-production.yml
# Production configuration (Spring Boot 4)
management:
server:
# Separate port for management endpoints, not exposed publicly
port: 9090
endpoints:
access:
default: none
web:
exposure:
include: health,info,prometheus
endpoint:
health:
access: read-only
show-details: when-authorized
probes:
add-additional-paths: true
info:
access: read-only
prometheus:
access: read-only
observations:
annotations:
enabled: true
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
minimum-expected-value:
http.server.requests: 1ms
maximum-expected-value:
http.server.requests: 30s
tags:
application: ${spring.application.name}
environment: production
version: ${app.version:unknown}Với access.default: none, các endpoint như env, beans, configprops và mappings đều không truy cập được mà không cần liệt kê từng cái. management.prometheus.metrics.export.enabled mặc định là true, và thiết lập step không có tác dụng với Prometheus: đây là registry kiểu pull, và chu kỳ scrape được đặt trong prometheus.yml.
Lỗi Thường Gặp với Actuator và Prometheus
| Triệu chứng | Nguyên nhân | Cách khắc phục |
|---|---|---|
/actuator/prometheus trả về 404 | Endpoint không có trong danh sách exposure, hoặc thiếu registry | Thêm prometheus vào management.endpoints.web.exposure.include và kiểm tra micrometer-registry-prometheus có trong classpath |
| Target Prometheus hiển thị "server returned HTTP status 401" | Spring Security bảo vệ mọi endpoint trừ health | Mở prometheus cho mạng của scraper hoặc cấu hình basic auth trong cấu hình scrape |
@Timed không tạo ra metric | management.observations.annotations.enabled mặc định là false, hoặc thiếu AspectJ | Đặt thuộc tính thành true và thêm spring-boot-starter-aspectj |
Không tìm thấy spring-boot-starter-aop sau khi nâng cấp | Đã đổi tên trong Spring Boot 4 | Dùng spring-boot-starter-aspectj |
Import Health hoặc HealthIndicator lỗi trên Boot 4 | Package đã chuyển sang org.springframework.boot.health.contributor | Cập nhật các import |
Cảnh báo deprecation với management.endpoint.*.enabled | Mô hình bật/tắt đã bị thay thế trong Spring Boot 3.4 | Dùng management.endpoints.access.default và management.endpoint.<id>.access |
| Percentile trên dashboard lệch xa thực tế | Giá trị publishPercentiles bị lấy trung bình giữa các pod | Dùng publishPercentileHistogram() và histogram_quantile() |
Câu Hỏi Phỏng Vấn về Micrometer trong Spring Boot
Actuator xuất hiện trong hầu hết các buổi phỏng vấn Spring Boot cho vị trí mid-level và senior. Những câu trả lời phân biệt một hồ sơ có kinh nghiệm với một junior:
- Khác biệt giữa liveness và readiness: liveness báo Kubernetes khởi động lại pod, readiness báo Kubernetes ngừng định tuyến lưu lượng. Ứng viên đưa cơ sở dữ liệu vào group liveness là người chưa từng vận hành nó qua một sự cố.
- Vì sao dùng Micrometer thay vì client Prometheus trực tiếp: Micrometer là một facade. Cùng một đoạn code
CountervàTimercó thể xuất sang Prometheus, Datadog hay OTLP chỉ bằng cách đổi phụ thuộc, và phần đo đạc sẵn có của Spring (HTTP, pool JDBC, cache) được viết dựa trên nó. - Percentile phía client so với histogram: percentile phía client không thể tổng hợp giữa các instance, còn histogram thì có. Đây là câu hỏi bẫy phổ biến nhất của chủ đề này.
- Cardinality là gì và vì sao nó làm hỏng Prometheus: mỗi tổ hợp label duy nhất là một chuỗi thời gian; một tag ID người dùng nhân mức dùng bộ nhớ lên theo số lượng người dùng.
- Cách bảo mật Actuator: giới hạn exposure, đặt mức truy cập, chạy phần quản trị trên một cổng riêng và bảo vệ bằng một
SecurityFilterChainriêng.
Nhiều câu hỏi cùng loại được tập hợp trong module câu hỏi phỏng vấn Spring Boot Actuator.
Nguồn Tham Khảo
- Spring Boot 4.0 Migration Guide:
spring-boot-starter-aopđổi tên thànhspring-boot-starter-aspectj, probe bật mặc định, module mớispring-boot-health - Spring Boot 3.4 Release Notes:
enabled-by-defaultvàendpoint.<id>.enabledbị deprecated, thay bằng mô hình truy cập - Tài liệu tham khảo endpoint Spring Boot Actuator: thuộc tính access, package của
EndpointRequest, health indicator trongorg.springframework.boot.health.contributor, group probe và đường dẫn bổ sung - Tài liệu tham khảo metric Spring Boot:
DefaultServerRequestObservationConvention, metric HikariCP vàjdbc.connections, việc deprecated simpleclient - Tài liệu tham khảo observability Spring Boot:
management.observations.annotations.enabledvà yêu cầu về AspectJ - Tài liệu Micrometer Prometheus: registry Prometheus Java client 1.x và module simpleclient cũ
- datasource-micrometer trên GitHub: 2.x cho Spring Boot 4, 1.x cho Spring Boot 3
- Grafana: gỡ bỏ AngularJS trong Grafana 12: panel Graph và Singlestat được thay bằng Time series và Stat
- Các bản phát hành Spring Boot: 4.1.2 là bản GA hiện tại, 4.2.0-RC1 đang ở giai đoạn tiền phát hành
Những Điểm Cần Nhớ về Micrometer Spring Boot và Prometheus
- Ba bước để scrape hoạt động: starter Actuator,
micrometer-registry-prometheus,prometheustrong danh sách exposure - Mô hình truy cập từ 3.4:
management.endpoints.access.default: nonecộngread-onlycho từng endpoint thay thế các cờenabledđã deprecated - Thay đổi trong Spring Boot 4:
spring-boot-starter-aspectj, API health trongorg.springframework.boot.health.contributor,EndpointRequesttrongorg.springframework.boot.security.autoconfigure.actuate.web.servlet, probe bật mặc định - Annotation cần một công tắc:
management.observations.annotations.enabled=true, không cần beanTimedAspectthủ công - Tag HTTP qua Observation API: kế thừa
DefaultServerRequestObservationConvention, giữ label ở cardinality thấp - Histogram thay vì percentile phía client:
publishPercentileHistogram()vàsum by (le)trong mọihistogram_quantile - Liveness chỉ xét cục bộ: phụ thuộc bên ngoài đặt trong readiness hoặc group tùy chỉnh, không bao giờ trong liveness
Những thiết lập mặc định này mang lại cho một service Spring Boot 4 các metric tổng hợp chính xác giữa các pod, các endpoint không để lộ gì ra mạng công cộng, và các cảnh báo kích hoạt bởi sự cố thực sự thay vì bởi một URI chậm.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Bạn có tìm ra lỗi trong Spring Boot không?
Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Viết bởi
Anthony Fillion-MailletNgười sáng lập SharpSkill
Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.
Cập nhật ngày 26 tháng 10, 2026
Thẻ
Chia sẻ
Bài viết liên quan

Observability trong Spring Boot 2026: OpenTelemetry, Distributed Tracing va Cau hoi Phong van
Lam chu observability trong Spring Boot voi OpenTelemetry va Micrometer Tracing. Huong dan cau hinh distributed tracing, Observation API, xuat OTLP, va chuan bi phong van ky thuat.

Spring Boot Structured Logging 2026: Log JSON cho Production với Logback và OpenTelemetry
Hướng dẫn đầy đủ về structured logging Spring Boot 4.x. Hỗ trợ JSON native, OpenTelemetry starter, MDC tracing và tích hợp ELK Stack cho observability production.

Việc làm Freelance Developer Java Spring Boot 2026: Tìm hợp đồng ở đâu, mức giá theo ngày, cách chuẩn bị
Hướng dẫn toàn diện cho developer freelance Java Spring Boot năm 2026: nền tảng tìm kiếm hợp đồng, mức giá theo ngày tại các quốc gia, và cách chuẩn bị cho phỏng vấn kỹ thuật.