Monitoring & Observability (Giám sát & Khả năng quan sát)Monitoring & Observability
Thuộc knowledge base theo DevOps Roadmap.
Tổng quan
Monitoring cho bạn biết hệ thống có khỏe hay không; observability cho phép bạn hỏi tại sao nó không khỏe. Monitoring là thu thập các tín hiệu định nghĩa trước (CPU, error rate, độ sâu queue) và cảnh báo khi vượt ngưỡng — nó trả lời những câu hỏi bạn đã nghĩ ra từ trước. Observability là một thuộc tính của hệ thống: mức độ mà các dữ liệu đầu ra (telemetry) cho phép giải thích bất kỳ trạng thái nội bộ nào, kể cả những kiểu lỗi không ai lường trước. Trong kiến trúc microservice, nơi một request của người dùng có thể đi qua hàng chục service, “unknown unknowns” chiếm đa số — vì vậy observability trở thành năng lực cốt lõi của DevOps chứ không còn là việc phụ của ops.
Cách đóng khung phổ biến trong ngành là ba trụ cột — metrics, logs và traces — thường được mở rộng thêm events (deploy, thay đổi config, scaling) cung cấp bối cảnh “cái gì vừa thay đổi?”. Mỗi trụ cột trả lời một câu hỏi khác nhau: metric nói có gì đó sai (rẻ, đã tổng hợp, phù hợp để alert), trace nói sai ở đâu trên đường đi của request, còn log nói vì sao với đầy đủ chi tiết. Các team trưởng thành sẽ liên kết cả ba: alert nổ trên một metric, bạn nhảy sang một trace mẫu (exemplar), rồi xoay sang log của span đang lỗi. Ba trụ cột là mô hình khởi đầu, không phải giáo điều — mục tiêu sâu xa hơn là telemetry cardinality cao, được liên kết, khám phá được, cho phép bạn cắt lát theo bất kỳ chiều nào (user, region, version, endpoint) khi đang xử lý sự cố.
Vượt ra ngoài tooling, observability là nền móng của reliability engineering: SLI định lượng hành vi mà người dùng nhìn thấy, SLO biến chúng thành mục tiêu, và error budget biến độ tin cậy thành một “đồng tiền” mà team có thể chi tiêu cho tốc độ phát triển tính năng. Không có đo lường thì “hệ thống ổn định” chỉ là ý kiến cá nhân. Trạng thái đích cần nhắm tới: khi có gì đó hỏng lúc 3 giờ sáng, một engineer on-call chưa từng thấy lỗi này bao giờ vẫn có thể đi từ “một alert vừa nổ” tới “đây là nguyên nhân gốc” chỉ trong vài phút, chỉ bằng telemetry — mà không phải deploy lại để thêm một câu print.
Một lưu ý: telemetry là dữ liệu, không phải dashboard
Người mới hay đánh đồng observability với dashboard. Dashboard là để trả lời các câu hỏi đã biết. Observability thực sự lộ ra khi câu hỏi là mới: “vì sao request checkout từ người dùng Android ở eu-west chỉ chậm khi app ở version 4.2?”. Bạn chỉ trả lời được nếu dữ liệu mang theo các chiều đó và bạn query xuyên qua chúng được một cách tùy ý. Hãy thiết kế telemetry sao cho cắt lát được theo những chiều mà bạn chưa nghĩ tới việc vẽ biểu đồ từ trước.
Kiến thức nền tảng
Ba trụ cột (cộng thêm events)
| Tín hiệu | Là gì | Điểm mạnh | Điểm yếu | Trả lời |
|---|---|---|---|---|
| Metrics | Chuỗi thời gian dạng số (counter, gauge, histogram) | Lưu trữ rẻ, query nhanh, lý tưởng cho alert và xu hướng | Đã tổng hợp trước: không có chi tiết theo request; giới hạn cardinality | Có gì đó sai không? |
| Logs | Bản ghi sự kiện có timestamp, tốt nhất là structured (JSON) | Chi tiết tùy ý, phù hợp điều tra sự cố | Đắt khi khối lượng lớn; nhiễu; khó tổng hợp | Vì sao sai? |
| Traces | Đường đi của một request qua các service, dưới dạng cây các span có đo thời gian | Chỉ đúng chỗ gây latency/lỗi xuyên qua ranh giới service | Cần instrument; thường phải sampling | Sai ở đâu? |
| Events | Thay đổi rời rạc: deploy, feature flag, config, scaling | Giải thích cái gì thay đổi ngay trước sự cố | Chỉ hữu ích nếu thực sự được ghi lại và liên kết | Cái gì thay đổi? |
Monitoring vs observability trong một câu: monitoring = known-unknowns (dashboard và alert cho các lỗi đã lường trước); observability = unknown-unknowns (telemetry cardinality cao, khám phá được, hỗ trợ đặt câu hỏi tùy ý khi đang xử lý sự cố). Chúng bổ trợ nhau, không phải đối thủ — bạn monitor những gì lường trước được và dựa vào observability cho mọi thứ còn lại.
Một cách đóng khung hữu ích từ cộng đồng observability là wide, structured events như một primitive hợp nhất: thay vì nghĩ theo ba silo tách biệt, hãy phát ra một event được chú thích phong phú cho mỗi đơn vị công việc (một request), mang theo hàng chục trường (user, endpoint, version, region, thời gian DB, cache hit, error), rồi dẫn xuất metrics, traces và logs từ các event đó. Dù bạn theo mô hình này hay ba trụ cột kinh điển, nguyên tắc vẫn như nhau: giữ lại bối cảnh, giữ cardinality query được, liên kết xuyên các tín hiệu.
Prometheus
Hệ thống metrics open-source mặc định, một dự án CNCF đã graduated:
- Pull model — Prometheus server chủ động scrape các endpoint HTTP
/metricstheo lịch; target được tìm qua service discovery (Kubernetes, EC2, Consul, cấu hình tĩnh). Pull khiến sức khỏe của target quan sát được (“nó có sống không?” chính là scrape có thành công không) và tránh việc mọi app phải có credential để push. Pushgateway chỉ dành cho batch job sống ngắn — chết trước khi kịp bị scrape — chứ không phải một push endpoint dùng chung. - Exporters — bộ chuyển đổi expose metric cho những thứ không tự làm được:
node_exporter(CPU/mem/disk/network của host),kube-state-metrics(trạng thái object K8s),cAdvisor(tài nguyên container),blackbox_exporter(probe endpoint), cùng hàng trăm exporter cho database, message queue, v.v. - Data model — mỗi time series được định danh duy nhất bằng tên metric cộng một tập label key/value. Label là thứ khiến PromQL mạnh (cắt lát theo
service,status,region) và nguy hiểm (mỗi tổ hợp label duy nhất là một series mới — xem phần cardinality bên dưới). - Các loại metric:
counter— chỉ tăng đơn điệu (request, error); gần như luôn bọc trongrate()— không bao giờ vẽ counter thô.gauge— tăng/giảm (nhiệt độ, độ dài queue, memory đang dùng).histogram— bucket phía server; cho phéphistogram_quantile()tính percentile latency và tổng hợp được qua nhiều instance.summary— quantile tính sẵn phía client; query rẻ hơn nhưng không tổng hợp được qua nhiều instance (không thể trung bình các p99 đã tính sẵn).
- TSDB — lưu trữ time-series cục bộ trên node Prometheus; để lưu dài hạn và scale ngang/HA, remote-write sang Thanos, Cortex hoặc Grafana Mimir.
- Alertmanager — thành phần riêng nhận alert đang firing từ rule của Prometheus và xử lý grouping (gộp các alert liên quan thành một thông báo), deduplication (giữa các cặp Prometheus HA), silence (tắt tiếng khi bảo trì), inhibition (chặn alert hạ nguồn khi alert thượng nguồn đã nổ) và route đến Slack/PagerDuty/email/webhook theo label.
PromQL — từ cơ bản đến các pattern bạn sẽ thực sự dùng:
# Tốc độ HTTP request mỗi giây trong 5 phút, theo service (RED: Rate)
sum by (service) (rate(http_requests_total[5m]))
# Tỷ lệ lỗi (5xx / tổng) — RED: Errors
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
# Latency p95 từ histogram — RED: Duration.
# Tổng hợp *rate của bucket* theo 'le' TRƯỚC, RỒI mới lấy quantile.
histogram_quantile(0.95,
sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
# Phần trăm memory đã dùng trên mỗi node (USE: Utilization)
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
# Disk sẽ đầy trong 4 giờ tới? (dự đoán tuyến tính trên 6h lịch sử)
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) < 0
# Top 5 pod theo CPU (dùng topk cho dashboard dễ đọc, đừng vẽ mọi thứ)
topk(5, sum by (pod) (rate(container_cpu_usage_seconds_total[5m])))
# Availability trong một cửa sổ, từ recording rule (tính SLO hiệu quả)
sum(rate(http_requests_total{status!~"5.."}[30d]))
/ sum(rate(http_requests_total[30d]))
Các “bẫy” PromQL cần khắc cốt:
- Luôn
rate()một counter trước khi sum;sum(counter)trên counter hay bị restart là vô nghĩa. - Áp
rate()/increase()trước khi aggregate, vàhistogram_quantile()sau khi tổng hợp bucket theole— sai thứ tự sẽ âm thầm cho ra số sai. - Khoảng trong
rate(x[5m])nên ít nhất gấp 4 lần scrape interval, nếu không sẽ bị gap/nhiễu. rate()tự xử lý counter reset (restart);irate()chỉ dùng hai sample cuối (hợp cho biểu đồ biến động nhanh, dở cho alert).
Grafana
Lớp visualization và alerting tiêu chuẩn: dashboard trên Prometheus, Loki, Tempo, Elasticsearch, CloudWatch và hàng chục data source khác trong một giao diện. Các khả năng chính:
- Templating/variables — một dashboard tham số hóa bằng
$namespace/$service/$podthay vì hàng trăm bản copy. - Unified alerting — định nghĩa và route alert xuyên nhiều data source ngay từ Grafana (thay thế hoặc bổ trợ Alertmanager).
- Correlation — nhảy metrics → traces qua exemplar (một mẫu metric link tới một trace ID đại diện), và traces → logs qua
trace_iddùng chung. Đây chính là quy trình “một click từ triệu chứng tới nguyên nhân”. - Dashboard là code — provision dashboard từ JSON/Terraform/Jsonnet/Grafonnet, được version-control và review, không bao giờ sửa tay trên production rồi mất khi rebuild cluster.
Quản lý log
- Structured logging trước tiên: xuất JSON với các trường nhất quán (
timestamp,level,service,trace_id,span_id,user_id) để log query được và liên kết được với trace. Một bức tường textprintfkhông cấu trúc gần như vô dụng ở quy mô lớn. - Log level có kỷ luật:
ERROR= cần chú ý;WARN= đáng ngờ nhưng đã xử lý;INFO= sự kiện nghiệp vụ đáng chú ý;DEBUG= chi tiết phát triển (thường tắt ở prod). Log quá nhiều thì chết chìm trong chi phí và nhiễu; quá ít thì mù. - Loki — “Prometheus cho log”: chỉ index một tập nhỏ label, không index full-text, nên vận hành và lưu trữ rẻ hơn hẳn. Query bằng LogQL (label selector + pipeline filter/parse); ghép tự nhiên với Grafana và agent Promtail/Grafana Alloy. Đánh đổi: tìm full-text là quét tuyến tính trên các stream đã khớp, nên giữ label cardinality thấp và dựa vào chúng để thu hẹp phạm vi.
- ELK / Elastic Stack — Elasticsearch (index full-text đảo ngược) + Logstash/Beats (thu thập/biến đổi) + Kibana (UI). Tìm kiếm và phân tích cực mạnh, nhưng vận hành nặng, ngốn memory và storage, dễ đội chi phí. OpenSearch là bản fork open-source.
- Fluentd / Fluent Bit — collector/router log trung lập về vendor (CNCF). Fluent Bit là bản viết bằng C nhẹ hơn, lựa chọn thường gặp làm DaemonSet trên Kubernetes để tail stdout của container và chuyển tới bất kỳ backend nào (Loki, Elastic, S3, Kafka). Vector (Datadog) là lựa chọn hiện đại phổ biến khác.
# LogQL: log lỗi của một app, parse và lọc theo một trường JSON
{app="checkout", env="prod"} |= "error" | json | status >= 500
# Metric sinh từ log: tỷ lệ lỗi theo pod trong 5m
sum by (pod) (rate({app="checkout"} |= "error" [5m]))
# Trích latency từ log và tính p95 (metric query trên log)
quantile_over_time(0.95,
{app="checkout"} | json | unwrap latency_ms [5m]) by (route)
Mô hình tư duy LogQL: label selector {...} chọn stream nào để đọc (có index, nhanh); mọi thứ sau một dấu | là một pipeline áp lên các dòng đã khớp (line filter, parser json/logfmt, label filter). Thu hẹp bằng label trước, lọc text sau — làm ngược lại thì chậm và tốn kém.
Distributed tracing
- OpenTelemetry (OTel) — chuẩn trung lập vendor của CNCF để sinh telemetry: API/SDK cho từng ngôn ngữ, auto-instrumentation (bọc thư viện HTTP/DB/gRPC mà không sửa code), giao thức OTLP, và OTel Collector (nhận → xử lý/batch/sample/làm giàu → export). Mấu chốt: instrument một lần, export đi đâu cũng được — đổi Jaeger sang Tempo sang một vendor mà không đụng vào code app. OTel nay bao trùm cả traces, metrics và logs, trở thành điểm hội tụ của cả ba trụ cột.
- Span và context — một trace là một cây các span, mỗi span là một đơn vị công việc có đo thời gian, kèm tên, start/end, attribute và event. Context propagation mang trace/span ID qua ranh giới tiến trình bằng header W3C
traceparent; một hop làm rơi header sẽ cắt đôi trace thành các mảnh rời rạc — lỗi tracing phổ biến nhất. - Jaeger — backend tracing CNCF để lưu và hiển thị trace: timeline dạng flame-graph, sơ đồ phụ thuộc giữa các service, histogram latency.
- Grafana Tempo — backend trace chỉ cần object storage rẻ (S3/GCS), query bằng TraceQL; thiết kế để nhận 100% trace với chi phí thấp, correlation từ metrics (exemplar) và logs.
- Sampling kiểm soát chi phí:
- Head-based — quyết định ngay khi trace bắt đầu (ví dụ giữ 10%). Đơn giản và rẻ, nhưng có thể rơi mất đúng cái error trace hiếm mà bạn cần nhất.
- Tail-based — buffer span trong Collector và quyết định sau khi trace kết thúc, nên giữ được mọi lỗi và trace chậm trong khi sample các request thành công nhàm chán. Mạnh hơn, tốn tài nguyên hơn.
Khái niệm chính
SLI, SLO, SLA và error budget
| Thuật ngữ | Định nghĩa | Ví dụ |
|---|---|---|
| SLI | Chỉ số đo được về sức khỏe dịch vụ theo góc nhìn người dùng | Tỷ lệ request < 300 ms và không phải 5xx |
| SLO | Mục tiêu nội bộ cho một SLI trong một khoảng thời gian | 99.9% thành công trong 30 ngày |
| SLA | Cam kết hợp đồng có chế tài (hoàn tiền/phạt) | Uptime 99.5%/tháng, không đạt thì hoàn tiền |
| Error budget | 1 − SLO: mức “được phép lỗi” | 99.9% ⇒ 43.2 phút lỗi/tháng |
Error budget là một công cụ chính sách, không chỉ là một con số: còn budget → cứ ship tính năng và chấp nhận rủi ro; hết budget → dừng các launch rủi ro và đầu tư vào reliability. Cách này giải quyết mâu thuẫn kinh điển dev-vs-ops bằng dữ liệu thay vì tranh cãi — reliability trở thành một “đồng tiền” chung, được định lượng, thay vì một cuộc cãi vã.
Chọn SLI tốt. Một SLI tốt là (1) lấy người dùng làm trung tâm (đo cái người dùng thực sự trải nghiệm, không phải proxy nội bộ), (2) là tỷ lệ giữa event tốt trên tổng event (nên tự nhiên là phần trăm), và (3) đo gần người dùng (ở load balancer hoặc client, không phải sâu trong một backend chưa từng thấy các request lỗi). Các loại SLI thường gặp: availability (tỷ lệ request thành công), latency (tỷ lệ phục vụ dưới một ngưỡng), quality/correctness, và freshness/durability cho hệ thống dữ liệu.
Toán học của các mục tiêu thường gặp (các “số 9”):
| SLO | Downtime / 30 ngày | Downtime / năm |
|---|---|---|
| 99% (“two nines”) | ~7.2 giờ | ~3.65 ngày |
| 99.9% (“three nines”) | ~43.2 phút | ~8.77 giờ |
| 99.95% | ~21.6 phút | ~4.38 giờ |
| 99.99% (“four nines”) | ~4.3 phút | ~52.6 phút |
Một kỷ luật quan trọng: đừng đặt 99.99% chỉ vì nghe cho oai. Mỗi con số 9 thêm vào tốn kém theo cấp số nhân, và một mục tiêu chặt hơn mức người dùng có thể cảm nhận (hoặc hơn mức các dependency chịu được) là lãng phí budget. Chọn SLO giữ người dùng vui và không chặt hơn thế.
Best practice về alerting
- Alert theo triệu chứng (symptom), không theo nguyên nhân. Page khi “người dùng thấy lỗi/chậm” (SLO burn), không page vì “CPU 90%”. CPU cao mà người dùng vẫn vui thì không phải khẩn cấp; tín hiệu nguyên nhân nên nằm trên dashboard hoặc thành ticket. Riêng quy tắc này loại bỏ phần lớn nhiễu alert.
- Burn-rate alert (pattern từ Google SRE workbook): cảnh báo dựa trên tốc độ tiêu error budget thay vì một ngưỡng tĩnh. Nhiều cửa sổ bắt được cả vấn đề nhanh lẫn chậm: fast burn (ví dụ tiêu > 14.4× budget trong 1 giờ → cả budget tháng hết trong ~2 ngày) page ngay; slow burn (> 1× trong 3 ngày) tạo ticket. Ghép một cửa sổ dài (đây có phải xu hướng thật?) với một cửa sổ ngắn (nó còn đang xảy ra lúc này không?) tránh cảnh báo cho blip đã tự hồi phục.
- Mọi page phải actionable và khẩn cấp. Nếu phản ứng là “chờ xem sao”, hoặc script tự sửa được, hoặc để đến sáng cũng được — thì không nên đánh thức con người. Mỗi alert phải link tới runbook. Một page là một lần cắt ngang giấc ngủ của con người; hãy coi chi phí đó là thật.
- Chống alert fatigue — kiểu hỏng khi alert bị phớt lờ vì đa số là nhiễu: review khối lượng alert định kỳ, xóa hoặc hạ cấp alert không ai xử lý, group/inhibit các alert liên quan trong Alertmanager, và dùng
for:để yêu cầu điều kiện kéo dài mới bắn. - Phân cấp severity:
page(đánh thức người ngay) vsticket/warning(xử lý trong giờ hành chính) vs chỉ-dashboard (bối cảnh, không thông báo). Route mỗi loại cho phù hợp.
# Prometheus alert rule: SLO burn đa cửa sổ, đa burn-rate.
# Với SLO 99.9%, error budget là 0.001. Cháy ở mức 14.4x nghĩa là
# budget 30 ngày sẽ hết trong ~2 ngày nếu kéo dài.
# Phép AND của cửa sổ 1h và 5m tránh page cho một spike đã hồi phục.
groups:
- name: slo-burn
rules:
- alert: HighErrorBudgetBurn
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[1h]))
/ sum(rate(http_requests_total[1h]))
) > (14.4 * 0.001)
and
(
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
) > (14.4 * 0.001)
for: 2m
labels:
severity: page
annotations:
summary: "Error budget đang cháy nhanh gấp 14.4 lần (SLO 99.9%)"
description: "Tỷ lệ 5xx trong cả 1h và 5m đều vượt ngưỡng fast-burn."
runbook_url: "https://runbooks.example.com/slo-burn"
# Slow burn: page ít khẩn cấp hơn nhiều, bắt lỗi thấp nhưng đều đặn.
- alert: SlowErrorBudgetBurn
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[6h]))
/ sum(rate(http_requests_total[6h]))
) > (1 * 0.001)
for: 15m
labels:
severity: ticket
annotations:
summary: "Error budget đang cháy ~1x (chậm) — điều tra trong hôm nay"
runbook_url: "https://runbooks.example.com/slo-burn"
Infrastructure monitoring vs application monitoring
| Tầng | Theo dõi gì | Công cụ/phương pháp điển hình |
|---|---|---|
| Infrastructure | CPU, memory, disk, network, sức khỏe node/pod | node_exporter, kube-state-metrics, cAdvisor; USE method (Utilization, Saturation, Errors) cho từng resource |
| Application | Request rate, errors, duration, saturation; KPI nghiệp vụ | Instrument app (OTel/Prometheus client), RED method (Rate, Errors, Duration) cho từng service; Four Golden Signals của Google = latency, traffic, errors, saturation |
| End-user / synthetic | Trải nghiệm người dùng thật; probe từ bên ngoài | Blackbox exporter, synthetic check, RUM (Real User Monitoring), Core Web Vitals |
Hai phương pháp đáng thuộc lòng:
- USE (cho resource): với mỗi resource, theo dõi Utilization, Saturation và Errors. Trả lời “resource này có phải nút thắt cổ chai không?” — tuyệt cho hạ tầng.
- RED (cho service): với mỗi service phục vụ request, theo dõi Rate, Errors và Duration. Trả lời “người dùng có được phục vụ tốt không?” — tuyệt cho application và microservice.
Metric hạ tầng hiếm khi đáng để page một mình — chúng cung cấp lý do sau khi alert theo triệu chứng đã nổ (“lỗi tăng vọt vì node hết memory”). Synthetic/blackbox check là lưới an toàn cho tình huống “sập toàn bộ mà telemetry nội bộ không kịp báo”, vì một hệ thống sập hẳn không thể tự phát telemetry.
Chi phí và cardinality
Chi phí telemetry tỷ lệ với cardinality của label (metrics), khối lượng (logs) và số lượng span (traces). Cardinality là số time series duy nhất = tích số các giá trị phân biệt của mọi label. Gắn label metric với một giá trị không giới hạn là kiểu tự gây sự cố kinh điển:
# BOM CARDINALITY — TUYỆT ĐỐI KHÔNG làm thế này.
# Mỗi user một time series; một triệu user = một triệu series,
# và Prometheus giữ tất cả trong memory. Đây là cách team làm OOM chính hệ giám sát.
http_requests_total{user_id="...", request_id="...", session="..."}
# Ổn: label bị giới hạn (status có ~5 giá trị, route là tập hữu hạn).
http_requests_total{status="500", route="/checkout", method="POST"}
Quy tắc: không bao giờ gắn label metric với user ID, request ID, email, URL đầy đủ chứa ID, hay timestamp. Đặt các chiều chi tiết, không giới hạn vào traces và logs (vốn sinh ra để làm việc đó), không phải metrics. Sample trace (tail-based để giữ cái đáng quan tâm). Phân bậc retention log (hot vài ngày, cold storage rẻ cho compliance). Hóa đơn observability phình to trong im lặng — một label tồi có thể làm chi phí metrics tăng 100× chỉ sau một đêm, nên hãy review cardinality như review một database schema.
Best Practices
- Alert theo triệu chứng và SLO, không theo nguyên nhân. Người dùng không quan tâm CPU; chỉ page khi hành vi mà người dùng thấy bị suy giảm, còn tín hiệu nguyên nhân để trên dashboard phục vụ chẩn đoán. Đây là quy tắc đòn bẩy cao nhất để giảm nhiễu.
- Định nghĩa SLI/SLO trước khi dựng dashboard. Instrument mà không có mục tiêu chỉ tạo ra biểu đồ đẹp, không tạo ra quyết định. Bắt đầu từ “một người dùng vui trải nghiệm gì?” và đo cái đó.
- Dùng burn-rate alert đa cửa sổ, đa burn-rate. Bắt được sự cố lớn trong vài phút lẫn rò rỉ chậm qua nhiều ngày, đồng thời bỏ qua các blip đã hồi phục — tốt hơn hẳn một ngưỡng tĩnh.
- Mọi page đều phải actionable và có runbook. Alert không kèm phản ứng được ghi chép và cần con người là nhiễu; review và cắt tỉa danh mục alert mỗi quý; xóa cái không bao giờ nổ hoặc không ai xử lý.
- Chuẩn hóa instrumentation trên OpenTelemetry. SDK trung lập vendor và Collector giúp đổi backend mà không phải instrument lại — instrument một lần, export đi đâu cũng được.
- Log JSON có cấu trúc và luôn kèm
trace_id. Tốc độ debug đến từ khả năng liên kết xuyên trụ cột (metric → trace → log qua ID dùng chung); log không cấu trúc thì không join được vào đâu. - Kiểm soát cardinality và volume một cách chủ động. Không dùng label metric không giới hạn; đẩy các chiều chi tiết vào traces/logs; sample trace; phân bậc retention log. Review chi phí telemetry như review một hóa đơn — nó phình lên trong im lặng.
- Giám sát từ bên ngoài nữa. Blackbox/synthetic probe bắt được các sự cố sập toàn phần mà telemetry nội bộ không thể tự báo, vì một hệ thống chết không phát ra gì.
- Ghi lại event deploy và thay đổi config. Đa số sự cố xảy ra ngay sau một thay đổi; overlay event deploy/flag/config lên dashboard trả lời “cái gì vừa đổi?” ngay lập tức và rút ngắn thời gian chẩn đoán.
- Coi dashboard và alert là code. Version hóa, review và provision (Terraform/Jsonnet/JSON) để chúng sống sót khi rebuild cluster, luôn audit được, và được code-review như mọi thay đổi khác.
- Phủ RED/USE cơ bản cho mọi service và resource. Một baseline nhất quán (RED cho mỗi service, USE cho mỗi resource, Four Golden Signals ở biên) tốt hơn một đống dashboard “mỗi nơi một kiểu”.
- Chọn mục tiêu SLO có chủ đích — đừng chạy đua số 9. Mỗi con số 9 thêm vào tốn kém cấp số nhân; một mục tiêu vượt mức người dùng cảm nhận hoặc dependency chịu được là lãng phí budget. Khớp SLO với kỳ vọng thật của người dùng.
- Chi tiêu error budget như một tính năng. Budget còn là “giấy phép” để ship và chấp nhận rủi ro; budget hết là tín hiệu chậm lại và gia cố. Làm chính sách rõ ràng và được cả dev lẫn ops tôn trọng.
- Instrument toàn bộ request path và propagate context. Một hop
traceparentbị đứt sẽ âm thầm cắt vụn trace; hãy xác minh propagation qua mọi service, queue và ranh giới bất đồng bộ. - Kiểm thử chính hệ thống observability trước khi cần đến. Chạy chaos/game day: chủ động phá và xác minh alert có nổ, dashboard có giải thích được, runbook có dùng được — để sự cố thật lúc 3 giờ sáng không phải là lần test đầu tiên.
- Giữ retention và access của telemetry hợp lý. Retention độ phân giải cao trong thời gian ngắn cộng lịch sử downsample/rollup dài hơn; và nhớ rằng log và trace có thể chứa PII — áp cùng mức access control và redaction như với mọi dữ liệu nhạy cảm.
Tài liệu tham khảo
- roadmap.sh — DevOps Roadmap
- Tài liệu Prometheus
- PromQL basics · querying functions
- Prometheus metric types · histograms & quantiles
- Prometheus: instrumentation & naming best practices
- Tài liệu Alertmanager
- Tài liệu Grafana
- Grafana Loki · LogQL · Grafana Tempo · TraceQL
- Tài liệu Elastic Stack · OpenSearch
- Tài liệu Fluent Bit · Fluentd · Vector
- Tài liệu OpenTelemetry · OTel Collector · W3C Trace Context
- Tài liệu Jaeger
- Google SRE Book — Monitoring Distributed Systems
- Google SRE Book — Service Level Objectives
- Google SRE Workbook — Alerting on SLOs · Implementing SLOs
- Brendan Gregg — The USE Method
- Sách: Site Reliability Engineering (O’Reilly, đọc miễn phí tại sre.google)
- Sách: Observability Engineering — Majors, Fong-Jones, Miranda (O’Reilly)
Part of the DevOps Roadmap knowledge base.
Overview
Monitoring tells you whether your system is healthy; observability lets you ask why it isn’t. Monitoring is collecting predefined signals (CPU, error rate, queue depth) and alerting when they cross thresholds — it answers questions you thought to ask in advance. Observability is a property of the system: how well its external outputs (telemetry) let you explain any internal state, including failure modes nobody predicted. In microservice architectures, where a single user request may touch dozens of services, the “unknown unknowns” dominate, which is why observability has become a core DevOps competency rather than an ops afterthought.
The industry framing is the three pillars — metrics, logs, and traces — often extended with events (deploys, config changes, scaling actions) that provide the “what changed?” context. Each pillar answers a different question: metrics say something is wrong (cheap, aggregated, alertable), traces say where in the request path it’s wrong, and logs say why with full detail. Mature teams correlate all three: an alert fires on a metric, you jump to an exemplar trace, and pivot to the logs of the failing span. The pillars are a starting mental model, not a religion — the deeper goal is high-cardinality, correlated, explorable telemetry that lets you slice by any dimension (user, region, version, endpoint) during an incident.
Beyond tooling, observability underpins reliability engineering itself: SLIs quantify user-visible behavior, SLOs turn them into targets, and error budgets turn reliability into a currency teams can spend on feature velocity. Without measurement, “reliable” is just an opinion. The end state you’re aiming for: when something breaks at 3 a.m., an on-call engineer who has never seen this failure before can go from “an alert fired” to “here is the root cause” in minutes, using telemetry alone — without redeploying to add a print statement.
A note on telemetry as data, not dashboards
Beginners equate observability with dashboards. Dashboards are for known questions. Real observability shows up when the question is novel: “why are checkout requests from Android users in eu-west slow only on app version 4.2?” You can only answer that if the data carries those dimensions and you can query across them ad hoc. Design your telemetry so it can be sliced by dimensions you did not think to graph in advance.
Fundamentals
The three pillars (plus events)
| Signal | What it is | Strengths | Weaknesses | Answers |
|---|---|---|---|---|
| Metrics | Numeric time series (counters, gauges, histograms) | Cheap to store, fast to query, ideal for alerting and trends | Pre-aggregated: no per-request detail; cardinality limits | Is something wrong? |
| Logs | Timestamped, ideally structured (JSON) event records | Arbitrary detail, great for forensics | Expensive at volume; noisy; hard to aggregate | Why is it wrong? |
| Traces | A request’s path across services as a tree of timed spans | Pinpoints latency and failures across service boundaries | Requires instrumentation; usually sampled | Where is it wrong? |
| Events | Discrete changes: deploys, feature flags, config, scaling | Explain what changed right before an incident | Only useful if actually recorded and correlated | What changed? |
Monitoring vs observability in one line: monitoring = known-unknowns (dashboards and alerts for anticipated failures); observability = unknown-unknowns (high-cardinality, explorable telemetry that supports ad-hoc questions during an incident). They are complementary, not rivals — you monitor the things you can predict and rely on observability for everything else.
A useful reframing from the observability community is wide, structured events as a unifying primitive: instead of thinking in three separate silos, emit one richly-annotated event per unit of work (a request), carrying dozens of fields (user, endpoint, version, region, DB time, cache hit, error), then derive metrics, traces, and logs from those events. Whether you adopt that model or the classic three pillars, the principle is the same: capture context, keep cardinality queryable, correlate across signals.
Prometheus
The default open-source metrics system, a CNCF graduated project:
- Pull model — the Prometheus server scrapes
/metricsHTTP endpoints on a schedule; targets are found via service discovery (Kubernetes, EC2, Consul, static config). Pull makes targets’ health observable (“is it up?” is just whether the scrape succeeded) and avoids every app needing push credentials. The Pushgateway exists only for short-lived batch jobs that die before they can be scraped — it is not a general push endpoint. - Exporters — adapters exposing metrics for things that can’t natively:
node_exporter(host CPU/mem/disk/network),kube-state-metrics(K8s object state),cAdvisor(container resources),blackbox_exporter(probing endpoints), plus hundreds for databases, message queues, and more. - Data model — every time series is uniquely identified by a metric name plus a set of key/value labels. Labels are what make PromQL powerful (slice by
service,status,region) and dangerous (each unique label combination is a new series — see cardinality below). - Metric types:
counter— monotonically increasing (requests, errors); you almost always wrap it inrate()— never graph a raw counter.gauge— goes up and down (temperature, queue length, memory in use).histogram— server-side buckets of observations; enableshistogram_quantile()for latency percentiles and is aggregatable across instances.summary— client-side precomputed quantiles; cheaper to query but not aggregatable across instances (you can’t average pre-computed p99s).
- TSDB — local time-series storage on the Prometheus node; for long-term retention and horizontal/HA scale, remote-write to Thanos, Cortex, or Grafana Mimir.
- Alertmanager — a separate component that receives firing alerts from Prometheus rules and handles grouping (collapse related alerts into one notification), deduplication (across HA Prometheus pairs), silences (mute during maintenance), inhibition (suppress downstream alerts when an upstream one fires), and routing to Slack/PagerDuty/email/webhook by label.
PromQL — from basics to the patterns you’ll actually use:
# Per-second HTTP request rate over 5m, by service (RED: Rate)
sum by (service) (rate(http_requests_total[5m]))
# Error ratio (5xx / all) — RED: Errors
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
# p95 latency from a histogram — RED: Duration.
# Aggregate the *bucket rates* by 'le' first, THEN take the quantile.
histogram_quantile(0.95,
sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
# Memory usage percentage per node (USE: Utilization)
100 * (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)
# Disk full within 4 hours? (linear prediction over 6h of history)
predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 4*3600) < 0
# Top 5 pods by CPU (use topk for readable dashboards, not raw everything)
topk(5, sum by (pod) (rate(container_cpu_usage_seconds_total[5m])))
# Availability over a window, from a recording rule (efficient SLO math)
sum(rate(http_requests_total{status!~"5.."}[30d]))
/ sum(rate(http_requests_total[30d]))
PromQL gotchas to internalize:
- Always
rate()a counter before summing;sum(counter)on a restart-prone counter is meaningless. - Apply
rate()/increase()before aggregation, andhistogram_quantile()after aggregating buckets byle— getting this order wrong silently produces wrong numbers. - The range in
rate(x[5m])should be at least 4× the scrape interval, or you get gaps/noise. rate()handles counter resets (restarts) automatically;irate()uses only the last two samples (good for fast-moving graphs, bad for alerting).
Grafana
The standard visualization and alerting layer: dashboards over Prometheus, Loki, Tempo, Elasticsearch, CloudWatch, and dozens of other data sources in one pane. Key capabilities:
- Templating/variables — one dashboard parameterized by
$namespace/$service/$podinstead of hundreds of copies. - Unified alerting — define and route alerts across data sources from Grafana itself (an alternative or complement to Alertmanager).
- Correlation — jump metrics → traces via exemplars (a metric sample links to a representative trace ID), and traces → logs via shared
trace_id. This is the “single click from symptom to cause” workflow. - Dashboards as code — provision dashboards from JSON/Terraform/Jsonnet/Grafonnet, version-controlled and reviewed, never hand-edited on production and lost in the next cluster rebuild.
Log management
- Structured logging first: emit JSON with consistent fields (
timestamp,level,service,trace_id,span_id,user_id) so logs are queryable and correlatable with traces. A wall of unstructuredprintftext is nearly useless at scale. - Log levels with discipline:
ERROR= something needs attention;WARN= suspicious but handled;INFO= notable business events;DEBUG= development detail (usually off in prod). Log too much and you drown in cost and noise; too little and you’re blind. - Loki — “Prometheus for logs”: indexes only a small set of labels, not the full log text, making it dramatically cheaper to run and store. Queried with LogQL (label selectors + a filter/parse pipeline); pairs naturally with Grafana and the Promtail/Grafana Alloy agents. Trade-off: full-text search is a linear scan of matched streams, so keep labels low-cardinality and lean on them to narrow the search.
- ELK / Elastic Stack — Elasticsearch (inverted full-text index) + Logstash/Beats (ingest/transform) + Kibana (UI). Extremely powerful search and analytics, but operationally heavier, memory- and storage-hungry, and easy to make expensive. OpenSearch is the open-source fork.
- Fluentd / Fluent Bit — vendor-neutral log collectors/routers (CNCF). Fluent Bit is the lightweight C implementation, the usual choice as a Kubernetes DaemonSet tailing container stdout and shipping to any backend (Loki, Elastic, S3, Kafka). Vector (Datadog) is a popular modern alternative.
# LogQL: error logs for one app, parsed and filtered by a JSON field
{app="checkout", env="prod"} |= "error" | json | status >= 500
# Log-derived metric: error rate per pod over 5m
sum by (pod) (rate({app="checkout"} |= "error" [5m]))
# Extract latency from logs and compute p95 (metric query over logs)
quantile_over_time(0.95,
{app="checkout"} | json | unwrap latency_ms [5m]) by (route)
LogQL mental model: the label selector {...} chooses which streams to read (indexed, fast); everything after a | is a pipeline applied to matched lines (line filters, json/logfmt parsers, label filters). Narrow with labels first, filter text second — the reverse is slow and expensive.
Distributed tracing
- OpenTelemetry (OTel) — the CNCF vendor-neutral standard for generating telemetry: APIs/SDKs per language, auto-instrumentation (wrap HTTP/DB/gRPC libraries with no code changes), the OTLP wire protocol, and the OTel Collector (receive → process/batch/sample/enrich → export). The whole point: instrument once, export anywhere — swap Jaeger for Tempo for a vendor without touching app code. OTel now covers traces, metrics, and logs, making it the convergence point for all three pillars.
- Spans and context — a trace is a tree of spans, each a timed unit of work with a name, start/end, attributes, and events. Context propagation carries the trace/span IDs across process boundaries via the W3C
traceparentheader; a hop that drops the header splits the trace into disconnected fragments — the single most common tracing bug. - Jaeger — CNCF tracing backend for storing and visualizing traces: flame-graph-style timelines, service dependency graphs, latency histograms.
- Grafana Tempo — trace backend that needs only cheap object storage (S3/GCS), queried via TraceQL; designed for 100% ingestion at low cost with correlation from metrics (exemplars) and logs.
- Sampling controls cost:
- Head-based — decide at trace start (e.g., keep 10%). Simple and cheap, but you may drop the rare error trace you most wanted.
- Tail-based — buffer spans in the Collector and decide after the trace completes, so you can keep all errors and slow traces while sampling the boring successful ones. More powerful, more resource-intensive.
Key Concepts
SLI, SLO, SLA and error budgets
| Term | Definition | Example |
|---|---|---|
| SLI | A measured indicator of service health from the user’s perspective | Fraction of requests < 300 ms and non-5xx |
| SLO | Internal target for an SLI over a window | 99.9% successful over 30 days |
| SLA | Contractual promise with consequences (credits/penalties) | 99.5% monthly uptime or refund |
| Error budget | 1 − SLO: the allowed unreliability | 99.9% ⇒ 43.2 min/month of failure |
The error budget is a policy instrument, not just a number: budget remaining → ship features and take risks; budget exhausted → freeze risky launches and invest in reliability. This resolves the classic dev-vs-ops tension with data instead of arguments — reliability becomes a shared, quantified currency rather than a shouting match.
Choosing good SLIs. A good SLI is (1) user-centric (measures what a user actually experiences, not internal proxies), (2) a ratio of good events to total events (so it’s naturally a percentage), and (3) measured close to the user (at the load balancer or client, not deep in a backend that never saw the failed requests). Common SLI types: availability (fraction of successful requests), latency (fraction served under a threshold), quality/correctness, and freshness/durability for data systems.
Common target math (the “nines”):
| SLO | Downtime / 30 days | Downtime / year |
|---|---|---|
| 99% (“two nines”) | ~7.2 hours | ~3.65 days |
| 99.9% (“three nines”) | ~43.2 minutes | ~8.77 hours |
| 99.95% | ~21.6 minutes | ~4.38 hours |
| 99.99% (“four nines”) | ~4.3 minutes | ~52.6 minutes |
A key discipline: don’t set 99.99% because it sounds good. Each extra nine costs exponentially more engineering, and a target tighter than what users can even perceive (or than your dependencies support) is wasted budget. Pick the SLO that keeps users happy and no tighter.
Alerting best practices
- Alert on symptoms, not causes. Page on “users see errors/latency” (SLO burn), not “CPU is 90%”. High CPU with happy users is not an emergency; cause-based signals belong on dashboards or as tickets. This single rule eliminates most alert noise.
- Burn-rate alerts (Google SRE workbook pattern): alert on how fast the error budget is being consumed rather than a static threshold. Multiple windows catch both fast and slow problems: a fast burn (e.g., > 14.4× budget consumption over 1 h → the whole month’s budget gone in ~2 days) pages immediately; a slow burn (> 1× over 3 days) files a ticket. Pairing a long window (is it a real trend?) with a short window (is it still happening now?) prevents alerting on a blip that already recovered.
- Every page must be actionable and urgent. If the response is “wait and see”, or a script could fix it, or it can wait until morning — it should not page a human. Link a runbook from every alert. A page is an interruption of a human’s sleep; treat that cost as real.
- Fight alert fatigue — the failure mode where alerts get ignored because most are noise: review alert volume regularly, delete or demote alerts nobody acts on, group/inhibit related alerts in Alertmanager, and use
for:durations to require the condition to persist before firing. - Severity tiers:
page(wake a human now) vsticket/warning(handle in business hours) vs dashboards-only (context, no notification). Route each appropriately.
# Prometheus alert rule: multi-window, multi-burn-rate SLO alert.
# For a 99.9% SLO, the error budget is 0.001. Firing at 14.4x burn
# means the 30-day budget would be exhausted in ~2 days if sustained.
# The AND of a 1h and 5m window avoids paging on an already-recovered spike.
groups:
- name: slo-burn
rules:
- alert: HighErrorBudgetBurn
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[1h]))
/ sum(rate(http_requests_total[1h]))
) > (14.4 * 0.001)
and
(
sum(rate(http_requests_total{status=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m]))
) > (14.4 * 0.001)
for: 2m
labels:
severity: page
annotations:
summary: "Error budget burning 14.4x too fast (99.9% SLO)"
description: "5xx ratio over 1h and 5m both exceed the fast-burn threshold."
runbook_url: "https://runbooks.example.com/slo-burn"
# Slow burn: pages far less urgently, catches steady low-grade errors.
- alert: SlowErrorBudgetBurn
expr: |
(
sum(rate(http_requests_total{status=~"5.."}[6h]))
/ sum(rate(http_requests_total[6h]))
) > (1 * 0.001)
for: 15m
labels:
severity: ticket
annotations:
summary: "Error budget burning ~1x (slow) — investigate today"
runbook_url: "https://runbooks.example.com/slo-burn"
Infrastructure vs application monitoring
| Layer | What you watch | Typical tools/methods |
|---|---|---|
| Infrastructure | CPU, memory, disk, network, node/pod health | node_exporter, kube-state-metrics, cAdvisor; USE method (Utilization, Saturation, Errors) per resource |
| Application | Request rate, errors, duration, saturation; business KPIs | App instrumentation (OTel/Prometheus client), RED method (Rate, Errors, Duration) per service; Google’s Four Golden Signals = latency, traffic, errors, saturation |
| End-user / synthetic | Real user experience; probes from outside | Blackbox exporter, synthetic checks, RUM (Real User Monitoring), Core Web Vitals |
The two methods worth memorizing:
- USE (for resources): for every resource, watch Utilization, Saturation, and Errors. Answers “is this resource the bottleneck?” — great for infrastructure.
- RED (for services): for every request-serving service, watch Rate, Errors, and Duration. Answers “are users being served well?” — great for applications and microservices.
Infrastructure metrics rarely deserve pages on their own — they provide the why once a symptom-based alert fires (“errors spiked because the node ran out of memory”). Synthetic/blackbox checks are your safety net for “the whole thing is down and nothing internal noticed”, because a system that’s fully down can’t emit its own telemetry.
Cost and cardinality
Telemetry costs scale with label cardinality (metrics), volume (logs), and span count (traces). Cardinality is the number of unique time series = the product of every label’s distinct values. Labeling a metric with an unbounded value is the classic self-inflicted outage:
# CARDINALITY BOMB — do NOT do this.
# One time series per user; a million users = a million series,
# and Prometheus keeps them all in memory. This is how teams OOM their monitoring.
http_requests_total{user_id="...", request_id="...", session="..."}
# Fine: labels are bounded (status has ~5 values, route is a bounded set).
http_requests_total{status="500", route="/checkout", method="POST"}
Rules of thumb: never label metrics with user ID, request ID, email, full URL with IDs, or timestamps. Put unbounded, high-detail dimensions in traces and logs (which are built for it), not metrics. Sample traces (tail-based to keep the interesting ones). Tier log retention (hot for days, cheap cold storage for compliance). Observability bills grow silently — a single bad label can 100× your metrics cost overnight, so review cardinality the way you review a database schema.
Best Practices
- Alert on symptoms and SLOs, not causes. Users don’t care about CPU; page only when user-visible behavior degrades, and keep cause signals on dashboards for diagnosis. This is the highest-leverage rule for reducing noise.
- Define SLIs/SLOs before building dashboards. Instrumentation without targets produces pretty graphs and no decisions. Start from “what does a happy user experience?” and measure that.
- Use multi-window, multi-burn-rate alerting. It catches fast outages in minutes and slow leaks over days, while ignoring recovered blips — dramatically better than a static threshold.
- Make every page actionable, with a runbook. An alert without a documented, human-necessary response is noise. Review and prune the alert catalog quarterly; delete what never fires or never gets acted on.
- Standardize on OpenTelemetry for instrumentation. Vendor-neutral SDKs and the Collector keep you free to switch backends without re-instrumenting — instrument once, export anywhere.
- Log structured JSON and always include
trace_id. Cross-pillar correlation (metric → trace → log by shared ID) is where debugging speed comes from; unstructured logs can’t be joined to anything. - Control cardinality and volume deliberately. No unbounded metric labels; push high-detail dimensions into traces/logs; sample traces; tier log retention. Review telemetry cost like you review a bill — it grows silently.
- Monitor from the outside too. Blackbox/synthetic probes catch total failures your internal telemetry can’t report, because a dead system emits nothing.
- Record deploy and config events. Most incidents follow a change; overlaying deploy/flag/config events on dashboards answers “what changed?” instantly and shrinks time-to-diagnosis.
- Treat dashboards and alerts as code. Version, review, and provision them (Terraform/Jsonnet/JSON) so they survive cluster rebuilds, stay auditable, and can be code-reviewed like any change.
- Cover the RED/USE basics for every service and resource. A consistent baseline (RED per service, USE per resource, Four Golden Signals at the edge) beats a pile of bespoke, inconsistent dashboards.
- Pick SLO targets deliberately — don’t chase nines. Each extra nine costs exponentially more; a target beyond what users perceive or dependencies support wastes budget. Match the SLO to real user expectations.
- Spend the error budget as a feature. A remaining budget is permission to ship and take risks; an exhausted one is a signal to slow down and harden. Make the policy explicit and honored by both dev and ops.
- Instrument the request path end-to-end and propagate context. A broken
traceparenthop silently fragments traces; verify propagation across every service, queue, and async boundary. - Test your observability before you need it. Run chaos/game days: break things on purpose and verify alerts fire, dashboards explain, and runbooks work — so the first real 3 a.m. incident isn’t the first test.
- Keep telemetry retention and access sane. Short high-resolution retention plus longer downsampled/rolled-up history; and remember logs and traces can contain PII — apply the same access control and redaction you would to any sensitive data.
References
- roadmap.sh — DevOps Roadmap
- Prometheus documentation
- PromQL basics · querying functions
- Prometheus metric types · histograms & quantiles
- Prometheus: instrumentation & naming best practices
- Alertmanager documentation
- Grafana documentation
- Grafana Loki · LogQL · Grafana Tempo · TraceQL
- Elastic Stack documentation · OpenSearch
- Fluent Bit documentation · Fluentd · Vector
- OpenTelemetry documentation · OTel Collector · W3C Trace Context
- Jaeger documentation
- Google SRE Book — Monitoring Distributed Systems
- Google SRE Book — Service Level Objectives
- Google SRE Workbook — Alerting on SLOs · Implementing SLOs
- Brendan Gregg — The USE Method
- Book: Site Reliability Engineering (O’Reilly, free at sre.google)
- Book: Observability Engineering — Majors, Fong-Jones, Miranda (O’Reilly)