Mini-challenge: TaskFlow v5 — gắn máy đo
Instrument TaskFlow: health indicator riêng, metric nghiệp vụ có tag an toàn, dashboard P99 trên Prometheus và Grafana, trace một request xuyên hai service.
🎯 Đề bài
TaskFlow đã đi qua từng mảnh riêng lẻ suốt module này: Actuator mở endpoint, health indicator trả lời đúng probe, Micrometer đo đúng loại meter, percentile đọc đúng con số, Observation API gộp việc đo, và OTLP theo dấu một request xuyên service. Bài lab này khép vòng — gắn bộ quan sát đầy đủ cho TaskFlow, đúng như một service thật chuẩn bị lên production.
flowchart LR
S1["Buoc 1<br/>Actuator production"] --> S2["Buoc 2<br/>Health indicator dung probe"]
S2 --> S3["Buoc 3<br/>Metric nghiep vu"]
S3 --> S4["Buoc 4<br/>Percentile, dashboard, SLO"]
S4 --> S5["Buoc 5<br/>Tracing OTLP xuyen service"]Năm bước dưới đây độc lập với nhau — làm theo thứ tự hay nhảy tới bước bạn thấy khó nhất trước đều được. Mỗi bước có tiêu chí hoàn thành quan sát được: bạn tự kiểm bằng cách gọi đúng endpoint hoặc đọc đúng dashboard, không cần đoán.
Không ai nhắc bạn trước ba chỗ này — đúng là những chỗ TaskFlow thật đã từng sai trong các bài trước. Tự phát hiện chúng là một phần của bài học; đáp án nằm ở phần Lời giải.
🔍 Input — Process — Output
- Input: TaskFlow đã có
spring-boot-starter-actuator,micrometer-registry-prometheus, mộtNotificationServiceHealthIndicator(bài 02),TaskMetricsđếm task theopriority(bài 03), mộtTimerđo thời lượng xử lý task (bài 03), vàTaskService.completeTaskgọi sangnotificationService(bài 05). Còn thiếu: management port tách riêng, security cho Actuator, nhóm readiness đúng, percentile gộp được, dashboard P99, và tracing. - Process: cấu hình production cho Actuator, gán health indicator vào đúng probe, rà lại tag của metric nghiệp vụ, bật percentile-histogram rồi dựng dashboard P99 kèm một SLO có số, bật tracing OTLP xuyên hai service rồi nối vào log.
- Output: một TaskFlow có bề mặt Actuator an toàn, hai probe phản ánh đúng nghĩa, metric không nổ cardinality, dashboard P99 gộp đúng qua nhiều instance kèm SLO cụ thể, và một trace đọc được ngược từ đúng dòng log lỗi.
📦 Concept mapping
| Bước | Learning outcome trong module |
|---|---|
| 1 — Actuator production | Design bề mặt Actuator cho production: expose endpoint nào, chặn endpoint nào, vì sao tách cổng quản trị |
| 2 — Health indicator đúng probe | Implement health indicator riêng và map đúng vào readiness so với liveness probe |
| 3 — Metric nghiệp vụ | Implement metric Micrometer đúng loại meter và kiểm soát cardinality của tag |
| 4 — Percentile, dashboard, SLO | Diagnose độ trễ bằng percentile P99 và explain vì sao trung bình cộng che mất sự cố |
| 5 — Tracing OTLP xuyên service | Trace một request xuyên nhiều service bằng Observation API và OTLP, nối trace ID vào log |
▶️ Starter — 5 bước, mỗi bước một tiêu chí hoàn thành quan sát được
Bước 1 — Actuator cho production
Yêu cầu: chọn đúng tập endpoint expose cho production (không dùng include: "*"), tách management.server.port sang cổng riêng, và bảo vệ bằng một SecurityFilterChain khớp EndpointRequest.toAnyEndpoint().
Tiêu chí hoàn thành: gọi vào cổng ứng dụng không thấy bất kỳ path /actuator/* nào ngoài route bạn chủ định để công khai; gọi vào cổng quản trị mà không có Authorization header thì nhận đúng mã lỗi từ chối cho mọi endpoint trừ health; env và heapdump không nằm trong whitelist expose.
Bước 2 — Health indicator đúng probe
Yêu cầu: NotificationServiceHealthIndicator đã có từ bài 02 phải được gán vào đúng nhóm probe — quyết định nó thuộc readiness hay liveness dựa trên đúng câu hỏi bài 02 đã dạy.
Tiêu chí hoàn thành: khi giả lập notification-service không phản hồi (dừng nó, hoặc cho ping() ném exception), đúng một trong hai endpoint /actuator/health/readiness hoặc /actuator/health/liveness chuyển sang DOWN — endpoint còn lại vẫn UP.
Bước 3 — Metric nghiệp vụ, tag an toàn
Yêu cầu: rà lại TaskMetrics (bài 03) và mọi Counter/Timer/Gauge nghiệp vụ khác bạn đã viết — mỗi tag phải nhận tập giá trị hữu hạn biết trước tại thời điểm viết code.
Tiêu chí hoàn thành: gọi /actuator/prometheus, đếm số dòng cùng tên metric — con số đó phải khớp đúng tích số giá trị của các tag hữu hạn, không phình theo số lượng user hay task thực tế đang tồn tại trong hệ thống.
Bước 4 — Percentile, dashboard P99, một SLO có số
Yêu cầu: bật chế độ percentile gộp được qua nhiều instance cho Timer xử lý task, dựng một truy vấn Prometheus đọc đúng P99 của toàn hệ thống, hiển thị nó trên Grafana, rồi viết một SLO đủ ba phần.
Tiêu chí hoàn thành: truy vấn Prometheus dùng histogram_quantile(...); panel Grafana vẽ đúng một đường P99 gộp của toàn hệ thống, không phải nhiều đường tách theo từng pod; SLO viết ra có đủ metric, ngưỡng số, và cửa sổ thời gian, và error budget tính ra được một con số request cụ thể.
Bước 5 — Trace một request xuyên hai service
Yêu cầu: bật tracing OTLP cho TaskFlow, gọi một request chạm cả TaskFlow lẫn notification-service (ví dụ hoàn thành một task), rồi nối traceId vào log JSON.
Tiêu chí hoàn thành: một trace trên Jaeger hoặc Tempo có ít nhất hai span thuộc hai service khác nhau, cùng chung một trace ID; một dòng log JSON của TaskFlow có field traceId khớp đúng trace đó; bạn tìm lại được trace chỉ từ dòng log, không cần biết trước timestamp hay endpoint.
💡 Gợi ý
Gợi ý bước 1
Tự hỏi trước khi viết YAML: TaskFlow thật sự cần endpoint nào để vận hành hằng ngày, không phải "endpoint nào đang tồn tại"? heapdump có nằm trong danh sách đó không? EndpointRequest.toAnyEndpoint() đã dùng nguyên ở bài 01 — thứ cần thêm chỉ là whitelist đúng và một vai trò cho mọi request không phải health.
Gợi ý bước 2
Áp đúng câu hỏi bài 02 đã dạy: restart TaskFlow có sửa được sự cố ở notification-service không? Property readiness.include thay thế toàn bộ danh sách mặc định, không cộng thêm — thiếu phần tử nào thì phần tử đó biến mất khỏi cách tính, không phải vẫn còn đó.
Gợi ý bước 3
Với mỗi tag đang gắn, tự hỏi: giá trị của nó bạn liệt kê hết được ngay bây giờ, hay phải chờ dữ liệu thực tế mới biết có bao nhiêu giá trị? Danh sách priority là hữu hạn; danh sách người dùng thì không.
Gợi ý bước 4
Hai property percentiles và percentiles-histogram khác nhau đúng một điều: ai là người tính ra con số percentile cuối cùng — từng instance riêng lẻ, hay backend sau khi đã gộp dữ liệu thô. Chỉ một trong hai cho phép Prometheus gộp đúng qua nhiều pod.
Gợi ý bước 5
Ba dependency cần thêm đã có tên đầy đủ ở bài 06 — bridge, exporter, và một property endpoint. RestClient/@HttpExchange tự chèn traceparent khi được dựng đúng qua builder auto-config — kiểm lại đoạn code gọi sang notification-service có đi qua builder đó không.
✅ Lời giải
Bước 1 — Actuator cho production
management:
server:
port: 9090
endpoints:
web:
exposure:
include: "health,info,metrics,prometheus,loggers"
@Configuration(proxyBeanMethods = false)
public class ActuatorSecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain actuatorChain(HttpSecurity http) throws Exception {
http.securityMatcher(EndpointRequest.toAnyEndpoint());
http.authorizeHttpRequests(requests -> requests
.requestMatchers(EndpointRequest.to("health")).permitAll()
.anyRequest().hasRole("ADMIN"));
http.httpBasic(withDefaults());
return http.build();
}
}
heapdump và env bị loại khỏi whitelist có chủ đích — TaskFlow không cần chúng cho vận hành hằng ngày, và cả hai là hai endpoint rủi ro nhất theo bài 01.
Bước 2 — Health indicator đúng probe
management:
endpoint:
health:
probes:
enabled: true
group:
readiness:
include: readinessState, notificationService
notification-service xuống không có nghĩa TaskFlow chết — restart TaskFlow không sửa được notification-service, nên indicator này thuộc readiness, không phải liveness. Nhóm liveness giữ nguyên mặc định, chỉ livenessState.
Cạm bẫy 1: nếu bạn gán NotificationServiceHealthIndicator vào liveness (hoặc trỏ liveness vào endpoint health tổng hợp có gộp cả indicator này), một sự cố tạm thời ở notification-service sẽ khiến Kubernetes giết cả pod TaskFlow — đúng nguyên nhân restart loop đã học ở bài 02, chỉ đổi từ database sang notification-service.
Bước 3 — Metric nghiệp vụ, tag an toàn
@Service
public class TaskMetrics {
private final Map<Priority, Counter> tasksCreatedByPriority;
public TaskMetrics(MeterRegistry registry) {
this.tasksCreatedByPriority = new EnumMap<>(Priority.class);
for (Priority p : Priority.values()) {
tasksCreatedByPriority.put(p, Counter.builder("taskflow.tasks.created")
.tag("priority", p.name())
.register(registry));
}
}
public void onTaskCreated(Task task) {
tasksCreatedByPriority.get(task.getPriority()).increment();
}
}
Với 3 giá trị priority, /actuator/prometheus phải cho đúng 3 dòng taskflow_tasks_created_total, không hơn — dù TaskFlow đang có 10 hay 10.000 task.
Cạm bẫy 2: nếu bạn từng viết registry.counter("taskflow.tasks.created", "userId", task.getOwnerId(), "priority", ...) để "lọc theo user cho tiện", số dòng ở /actuator/prometheus sẽ tăng theo đúng số user từng tạo task — cardinality nổ theo tích, y hệt SpotTheBug ở bài 03. Muốn tra ai tạo task nào thì dùng highCardinalityKeyValue của Observation API (bài 05) hoặc trace, không dùng tag của Counter.
Bước 4 — Percentile, dashboard P99, một SLO có số
management:
metrics:
distribution:
percentiles-histogram:
taskflow.tasks.processing: true
Truy vấn Prometheus đọc P99 gộp đúng qua mọi instance:
histogram_quantile(0.99, sum(rate(taskflow_tasks_processing_seconds_bucket[5m])) by (le))
sum(...) by (le) gộp bucket của mọi pod trước, histogram_quantile chỉ tính percentile sau bước gộp đó — đúng cơ chế bài 04 đã giải thích. Đưa query này vào một panel Grafana kiểu time series là có dashboard P99.
SLO ví dụ: "99% task xử lý dưới 800ms trong 30 ngày." Với 500.000 task xử lý trong cửa sổ đó, error budget = 1% × 500.000 = 5.000 task được phép vượt 800ms cả tháng.
Cạm bẫy 3: nếu bạn tính P99 hệ thống bằng cách lấy trung bình cộng của P99 riêng từng instance (percentiles client-side rồi tự cộng chia), con số ra không phải P99 thật của hệ thống — percentile không phải đại lượng tuyến tính, không cộng được (bài 04, mục 3). Chỉ percentiles-histogram cộng với histogram_quantile() sau bước gộp bucket mới cho kết quả đúng.
Bước 5 — Trace một request xuyên hai service
dependencies {
implementation "io.micrometer:micrometer-tracing-bridge-otel"
implementation "io.opentelemetry:opentelemetry-exporter-otlp"
}
management:
otlp:
tracing:
endpoint: http://localhost:4318/v1/traces
tracing:
sampling:
probability: 1.0
probability: 1.0 chỉ hợp lý tạm thời cho lab này, để chắc chắn thấy trace ngay lần gọi đầu — production giữ nguyên mặc định 10% hoặc thấp hơn như bài 06 đã dạy.
Gọi POST /api/tasks/{id}/complete (kích hoạt TaskService.completeTask → notificationService.notifyCompleted), rồi mở Jaeger/Tempo: một trace hiện ra với ít nhất hai span, một của TaskFlow, một của notification-service, cùng chung trace ID. Log JSON của TaskFlow tại đúng request đó có dạng:
{"timestamp":"2026-07-29T09:14:22.103Z","level":"INFO","service":"taskflow","traceId":"4bf92f3577b34da6a3ce929d0e0e4736","spanId":"00f067aa0ba902b7","message":"task.complete end taskId=482"}
Copy traceId từ dòng log, dán vào ô tìm kiếm trace của Jaeger/Tempo — đúng đường nhảy hai chiều bài 06 đã mô tả.
🎓 Mở rộng
- Thêm
tail-based samplingở tầng OpenTelemetry Collector, để giữ lại đúng trace lỗi hoặc chậm thay vì lấy mẫu ngẫu nhiên đầu request. - Viết một alert rule dựa trên tốc độ tiêu error budget (burn rate), thay vì chỉ đặt ngưỡng P99 tĩnh.
- Mở rộng trace xuyên ba service thay vì hai — thêm
inventory-servicevào chuỗi gọi, quan sát trace phình thêm một tầng span như thế nào. - Thử tắt
percentiles-histogramvà chỉ dùngpercentilestrên một môi trường nhiều instance thật — tự nhìn Grafana vẽ nhiều đường P99 lệch nhau, đối chiếu với dashboard đúng ở bước 4.
✨ Điều bạn vừa làm được
TaskFlow bây giờ có một bề mặt Actuator chỉ mở đúng thứ cần, hai probe phản ánh đúng nghĩa "sống" và "sẵn sàng", metric nghiệp vụ không âm thầm nổ cardinality, một dashboard P99 đọc đúng qua nhiều instance kèm SLO có số, và một trace nối được từ dòng log lỗi tới toàn bộ hành trình request xuyên hai service. Đây là bộ observability tối thiểu một service Spring Boot cần trước khi lên production thật — không phải bài tập, mà là checklist bạn sẽ dùng lại nguyên vẹn ở dự án tiếp theo.
Bài tiếp theo: Tổng kết module
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên