Spring Boot Actuator: health check và metrics
1. Tổng quan
Một service chạy trên production cần trả lời được hai câu hỏi: “nó còn sống không?” và “nó đang chạy ra sao?”. Spring Boot Actuator trả lời cả hai qua các endpoint dựng sẵn, chủ yếu là /actuator/health và /actuator/metrics.
Trong bài này, bạn sẽ bật Actuator, viết một HealthIndicator tùy chỉnh và cấu hình liveness/readiness probe cho Kubernetes. Sau đó bạn ghi metrics nghiệp vụ bằng Micrometer, export sang Prometheus và viết test cho cả hai phần.
2. Cài đặt
Bạn cần thêm starter của Actuator. Ví dụ trong bài dùng thêm spring-boot-starter-web để các endpoint được phục vụ qua HTTP:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Khởi động ứng dụng rồi gọi thử:
curl http://localhost:8080/actuator
{"_links":{"self":{...},"health":{...},"health-path":{...}}}
Mặc định Spring Boot 3.x chỉ expose endpoint health qua HTTP. Các endpoint khác vẫn tồn tại nhưng không truy cập được từ bên ngoài. Đây là lựa chọn an toàn có chủ đích.
2.1. Expose thêm endpoint
Bạn liệt kê rõ những endpoint muốn mở trong application.properties:
management.endpoints.web.exposure.include=health,info,metrics
management.endpoint.health.show-details=always
Dòng đầu mở ba endpoint health, info và metrics. Dòng thứ hai cho phép /actuator/health trả về chi tiết từng thành phần thay vì chỉ một trạng thái tổng.
Lưu ý: Tránh dùng
management.endpoints.web.exposure.include=*trên production. Các endpoint nhưenv,heapdumphayconfigpropscó thể lộ mật khẩu, cấu hình nội bộ và dữ liệu trong bộ nhớ.
3. Health check
3.1. Endpoint mặc định
Với cấu hình ở trên, gọi /actuator/health sẽ cho kết quả dạng:
{
"status": "UP",
"components": {
"diskSpace": {
"status": "UP",
"details": { "total": 499963174912, "free": 213456789504, "threshold": 10485760 }
},
"ping": { "status": "UP" }
}
}
Spring Boot tự đăng ký các HealthIndicator dựa trên classpath. Nếu có DataSource, bạn sẽ thấy thêm db. Nếu có Redis thì thêm redis, và tương tự với các thành phần khác. Trạng thái tổng là kết quả gộp: chỉ cần một thành phần DOWN thì toàn bộ là DOWN, và HTTP status trả về là 503.
3.2. Viết HealthIndicator tùy chỉnh
Giả sử service đặt hàng của bạn phụ thuộc vào một cổng thanh toán bên ngoài. Trước hết, ta có một client đơn giản:
public interface PaymentGatewayClient {
boolean ping();
}
Sau đó viết indicator kiểm tra client này:
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;
@Component
public class PaymentGatewayHealthIndicator implements HealthIndicator {
private final PaymentGatewayClient client;
public PaymentGatewayHealthIndicator(PaymentGatewayClient client) {
this.client = client;
}
@Override
public Health health() {
try {
if (client.ping()) {
return Health.up().withDetail("provider", "VNPay").build();
}
return Health.down().withDetail("reason", "ping returned false").build();
} catch (Exception e) {
return Health.down(e).build();
}
}
}
Một số điểm cần chú ý:
- Tên bean là
paymentGatewayHealthIndicator. Spring Boot bỏ hậu tốHealthIndicator, nên key trong JSON làpaymentGateway. withDetail(...)thêm thông tin phụ, chỉ hiển thị khishow-detailscho phép.Health.down(e)tự đưa thông điệp lỗi vào phầndetails.error.
Kết quả khi cổng thanh toán lỗi:
{
"status": "DOWN",
"components": {
"diskSpace": { "status": "UP", ... },
"paymentGateway": {
"status": "DOWN",
"details": { "error": "java.net.ConnectException: Connection refused" }
},
"ping": { "status": "UP" }
}
}
Lưu ý: Method
health()chạy mỗi lần endpoint được gọi. Nếu nó gọi mạng mà không có timeout, một dependency chậm sẽ làm/actuator/healthtreo theo. Bạn nên luôn đặt timeout ngắn (ví dụ 1–2 giây) cho client dùng trong health check.
3.3. Liveness và readiness probe
Kubernetes phân biệt hai loại probe:
- Liveness: ứng dụng có bị treo không? Nếu fail, pod bị restart.
- Readiness: ứng dụng đã sẵn sàng nhận request chưa? Nếu fail, pod bị rút khỏi load balancer nhưng không bị restart.
Spring Boot tự bật hai nhóm này khi phát hiện đang chạy trên Kubernetes. Khi chạy local, bạn bật thủ công:
management.endpoint.health.probes.enabled=true
management.endpoint.health.group.readiness.include=readinessState,paymentGateway
Dòng đầu tạo hai endpoint /actuator/health/liveness và /actuator/health/readiness. Dòng thứ hai đưa paymentGateway vào nhóm readiness. Khi cổng thanh toán sập, pod chỉ ngừng nhận traffic chứ không bị restart vô ích.
curl http://localhost:8080/actuator/health/liveness
{"status":"UP"}
Cấu hình tương ứng trong manifest Kubernetes:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
Lưu ý: Đừng đưa dependency bên ngoài (database, API thanh toán) vào nhóm liveness. Khi database sập, mọi pod sẽ fail liveness và bị restart đồng loạt. Việc restart không sửa được database mà còn gây thêm tải lúc khởi động.
4. Metrics với Micrometer
Actuator dùng Micrometer làm lớp trừu tượng cho metrics. Không cần cấu hình gì thêm, bạn đã có sẵn metrics JVM, CPU, HTTP request, connection pool và nhiều thứ khác.
curl http://localhost:8080/actuator/metrics
{"names":["application.ready.time","http.server.requests","jvm.gc.pause","jvm.memory.used","process.cpu.usage", ...]}
Để xem chi tiết một metric, bạn thêm tên của nó vào URL. Có thể lọc theo tag bằng tham số tag:
curl "http://localhost:8080/actuator/metrics/http.server.requests?tag=uri:/orders"
4.1. Counter cho số đơn hàng
Metrics hệ thống chưa đủ, bạn thường cần cả metrics nghiệp vụ. Ví dụ dưới đây đếm số đơn hàng được tạo, phân loại theo kênh bán:
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final MeterRegistry registry;
public OrderService(MeterRegistry registry) {
this.registry = registry;
}
public Order createOrder(Customer customer, String channel) {
Order order = new Order(customer.id(), channel);
// ... lưu đơn hàng
Counter.builder("orders.created")
.description("Số đơn hàng đã tạo")
.tag("channel", channel)
.register(registry)
.increment();
return order;
}
}
public record Customer(long id, String name) {}
public record Order(long customerId, String channel) {}
MeterRegistry được Spring Boot tự cấu hình và inject vào. Gọi register(registry) nhiều lần với cùng tên và tag là an toàn, vì Micrometer trả về đúng counter đã có. Sau khi tạo ba đơn qua kênh web:
curl http://localhost:8080/actuator/metrics/orders.created
{
"name": "orders.created",
"description": "Số đơn hàng đã tạo",
"measurements": [ { "statistic": "COUNT", "value": 3.0 } ],
"availableTags": [ { "tag": "channel", "values": ["web"] } ]
}
4.2. Timer cho thời gian xử lý thanh toán
Timer vừa đếm số lần gọi, vừa đo tổng thời gian và thời gian lớn nhất:
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
private final Timer paymentTimer;
public PaymentService(MeterRegistry registry) {
this.paymentTimer = Timer.builder("payment.process")
.description("Thời gian xử lý thanh toán")
.register(registry);
}
public boolean pay(Order order) {
return paymentTimer.record(() -> {
// ... gọi cổng thanh toán
return true;
});
}
}
Timer được tạo một lần trong constructor rồi tái sử dụng, vì không có tag thay đổi theo từng lần gọi. record(Supplier) đo thời gian chạy của lambda và trả về kết quả của nó. Endpoint /actuator/metrics/payment.process sẽ hiển thị ba statistic: COUNT, TOTAL_TIME và MAX.
4.3. Tag chung cho mọi metric
Khi nhiều service cùng đẩy metrics về một nơi, bạn nên gắn tên ứng dụng vào mọi metric:
management.metrics.tags.application=order-service
Lưu ý: Không dùng giá trị có số lượng lớn hoặc không giới hạn làm tag, ví dụ
orderId,customerIdhay email. Mỗi tổ hợp tag tạo ra một time series riêng. Hàng triệu đơn hàng sẽ tạo hàng triệu series, làm phình bộ nhớ ứng dụng và hệ thống giám sát. Tag chỉ nên chứa tập giá trị nhỏ và cố định nhưchannel,statushayregion.
5. Export sang Prometheus
Endpoint /actuator/metrics phù hợp để xem nhanh. Để lưu trữ lâu dài và vẽ biểu đồ, bạn cần một hệ thống như Prometheus. Thêm registry tương ứng:
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Sau đó expose endpoint prometheus:
management.endpoints.web.exposure.include=health,info,metrics,prometheus
curl http://localhost:8080/actuator/prometheus | grep -E "orders|payment"
# HELP orders_created_total Số đơn hàng đã tạo
# TYPE orders_created_total counter
orders_created_total{application="order-service",channel="web"} 3.0
payment_process_seconds_count{application="order-service"} 5.0
payment_process_seconds_sum{application="order-service"} 0.412
payment_process_seconds_max{application="order-service"} 0.153
Micrometer tự đổi tên theo quy ước của Prometheus: dấu chấm thành gạch dưới, counter có hậu tố _total, timer tính bằng giây. Phần còn lại là cấu hình Prometheus scrape đường dẫn /actuator/prometheus.
6. Kiểm thử
6.1. Test HealthIndicator
HealthIndicator là một class thông thường, nên bạn test được mà không cần khởi động Spring context:
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import static org.assertj.core.api.Assertions.assertThat;
class PaymentGatewayHealthIndicatorTest {
@Test
void whenPingSucceeds_thenStatusIsUp() {
var indicator = new PaymentGatewayHealthIndicator(() -> true);
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).containsEntry("provider", "VNPay");
}
@Test
void whenPingThrows_thenStatusIsDownWithError() {
var indicator = new PaymentGatewayHealthIndicator(() -> {
throw new IllegalStateException("timeout");
});
Health health = indicator.health();
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails().get("error").toString()).contains("timeout");
}
}
Vì PaymentGatewayClient chỉ có một method, bạn truyền lambda thay cho mock.
6.2. Test metrics
Micrometer cung cấp SimpleMeterRegistry lưu metrics trong bộ nhớ, rất tiện cho unit test:
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class OrderServiceTest {
@Test
void whenOrdersCreated_thenCounterIncrementsPerChannel() {
var registry = new SimpleMeterRegistry();
var service = new OrderService(registry);
var customer = new Customer(1L, "An");
service.createOrder(customer, "web");
service.createOrder(customer, "web");
service.createOrder(customer, "mobile");
assertThat(registry.get("orders.created").tag("channel", "web").counter().count())
.isEqualTo(2.0);
assertThat(registry.get("orders.created").tag("channel", "mobile").counter().count())
.isEqualTo(1.0);
}
}
Nếu metric không tồn tại, registry.get(...) ném MeterNotFoundException. Nhờ vậy test cũng bắt được lỗi gõ sai tên metric.
7. Bảo mật và lỗi hay gặp
Tách port quản trị. Bạn có thể cho Actuator chạy trên port riêng, chỉ mở trong mạng nội bộ:
management.server.port=8081
Khi đó ứng dụng phục vụ ở 8080, còn /actuator/** chỉ có ở 8081. Bạn chặn port này ở firewall hoặc ingress là đủ. Trong Kubernetes, nhớ đổi port trong cấu hình probe sang 8081.
Giới hạn chi tiết health. Trên production, dùng show-details=when-authorized kết hợp Spring Security thay cho always. Người dùng ẩn danh khi đó chỉ thấy {"status":"UP"}.
Một số lỗi thường gặp:
- Gọi
/actuator/metricsnhận về404: bạn chưa thêmmetricsvàoexposure.include. - Health check fail ngẫu nhiên: một indicator gọi mạng không có timeout, hoặc có thời gian phản hồi lâu hơn timeout của probe.
- Pod restart liên tục khi database chập chờn: database đang nằm trong nhóm liveness.
- Bộ nhớ tăng dần theo thời gian: metric dùng tag có số lượng giá trị không giới hạn.
- Không thấy endpoint
/actuator/prometheus: thiếumicrometer-registry-prometheushoặc chưa expose endpoint.
8. Kết luận
Spring Boot Actuator cho bạn health check và metrics gần như miễn phí. Phần việc của bạn là cấu hình cho đúng: chỉ expose những endpoint cần thiết, viết HealthIndicator có timeout cho các dependency quan trọng, và tách liveness khỏi readiness để Kubernetes phản ứng hợp lý.
Với metrics, Micrometer giúp bạn ghi Counter và Timer cho nghiệp vụ chỉ trong vài dòng, rồi export sang Prometheus bằng một dependency. Hãy giữ số lượng giá trị của tag ở mức nhỏ và viết test bằng SimpleMeterRegistry để metrics không âm thầm hỏng sau mỗi lần refactor.