Sáng thứ Hai, dashboard đỏ lòm, error rate tăng 5%. Mở Grafana, thấy spike ở /api/orders. Mở trace, request chậm 4 giây, span gọi Redis timeout. Mở log với trace ID, "pq: canceling statement due to statement timeout" kèm query cụ thể. Từ “có gì đó hỏng” đến “connection pool Redis bị full vì deploy mới quên tăng max connections” mất 3 phút.
Đó là sức mạnh của ba trụ cột observability khi chúng phối hợp đúng cách. Nhưng thực tế thì nhiều team setup xong Grafana, Loki, Tempo rồi lại quay về grep log file, vì log, metric, trace đứng riêng lẻ, không liên kết qua trace ID, không ai biết nhìn vào đâu trước. Bài này đi qua cách hiểu và áp dụng ba trụ cột này trong project thật, khi nào dùng cái nào, setup ra sao, và quan trọng nhất: làm sao liên kết chúng lại.
Mỗi trụ cột trả lời một câu hỏi khác nhau
Trước khi nhảy vào tool hay framework, cần nắm một điều cơ bản: mỗi trụ cột giải quyết một bài toán riêng, và chúng bổ sung cho nhau chứ không thay thế nhau.
Metrics trả lời câu “có đang có vấn đề không?”, error rate, latency P99, QPS. Đây là thứ bạn nhìn đầu tiên trên dashboard mỗi sáng. Nếu con số bình thường thì chưa cần đào sâu gì hết. Nếu bất thường, chuyển sang trace để tìm hiểu tiếp.
Traces trả lời câu “vấn đề nằm ở đâu trong hệ thống?”, request đi qua service A, gọi service B, gọi Redis, chậm nhất ở đoạn nào. Trace cho bạn bản đồ hành trình của một request cụ thể, nhưng nó không nói chi tiết chuyện gì đã xảy ra bên trong mỗi span.
Logs trả lời câu “cụ thể chuyện gì đã xảy ra?”, message lỗi, stack trace, giá trị biến, query SQL thực thi. Đây là nơi cuối cùng bạn tìm nguyên nhân gốc.
Bỏ bất kỳ trụ nào thì bạn phải đoán mò ở bước còn lại. Metrics nói “5% request trả 5xx”, nhưng ở service nào? Traces chỉ ra “chậm ở Redis”, nhưng lỗi gì cụ thể? Logs cho biết “connection pool full”, nhưng từ khi nào và ảnh hưởng bao nhiêu user? Ghép ba cái lại với nhau: chẩn đoán nhanh, hành động đúng, không tốn thời gian đoán lung tung.
Logs, chi tiết sự kiện, nhưng đắt nhất
Level log và ý nghĩa thực tế
Nhiều dev mới sử dụng log level khá tuỳ hứng, cái gì cũng console.log rồi đẩy production. Nhưng log level tồn tại vì lý do rất thực dụng: để bạn lọc được khi hệ thống sinh ra hàng GB log mỗi ngày.
DEBUG là chi tiết cho lúc phát triển, biến trung gian, giá trị trả về từ function, nội dung payload. Trên production không bật mặc định, trừ khi bạn cần debug một vấn đề cụ thể. Lúc đó bật qua feature flag cho riêng service đó, dùng xong tắt ngay. Hay gặp tình huống team bật DEBUG toàn cluster để tìm bug, rồi quên tắt, hai ngày sau bill log storage tăng gấp 5 lần.
INFO dành cho sự kiện hợp lệ mà bạn cần biết đã xảy ra: “order created”, “user logged in”, “payment processed”. Đủ để hiểu flow hệ thống, không quá nhiều để gây nhiễu. Nguyên tắc đơn giản: nếu bạn đọc INFO log mà không hiểu hệ thống đang làm gì thì thiếu. Nếu đọc mà bị ngợp thì thừa.
WARN là khi có bất thường nhưng hệ thống vẫn xử lý được, “retry lần 1/3 sau lỗi mạng”, “cache miss phải fallback DB”, “config deprecated sẽ bị remove ở version sau”. Nếu bạn thấy WARN dày đặc trên production, có thể bạn đang set level sai hoặc hệ thống đang sắp gặp vấn đề lớn hơn. ERROR thì rõ ràng: lỗi cần can thiệp, request fail, data không consistent. FATAL khi tiến trình chết, log line cuối cùng trước khi process exit.
Heuristic đơn giản: nếu pager reo vì toàn WARN thì bạn đang log sai level. Nếu ERROR đầy log nhưng pager im re thì hoặc alert config sai, hoặc level đang bị dùng sai. Cả hai trường hợp đều cần sửa, không phải sửa code mà sửa cách dùng log level.
Structured logging là bắt buộc
Kiểu log console.log("Failed to load user " + userId + " at " + new Date()) thoạt nhìn vô hại, “có log là tốt rồi”. Nhưng đến lúc phải tìm pattern trong 2 triệu log line, regex vỡ tung vì mỗi developer format message khác nhau, có người dùng dấu ngoặc, có người dùng dash, có người ghi timestamp ở đầu, người ghi ở cuối.
Giải pháp là chuyển sang structured logging: log dạng JSON có trường rõ ràng, không phải text tự do.
{
"ts": "2026-04-10T12:00:01.234Z",
"level": "ERROR",
"service": "order-svc",
"msg": "db query timeout",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"route": "/api/orders",
"method": "POST",
"elapsed_ms": 5021,
"err": "pq: canceling statement due to statement timeout"
}
Khi log có cấu trúc như vậy, tool như Elastic, Loki, hay Datadog parse ngay được, filter và aggregate theo field. Bạn muốn tìm “tất cả log ERROR của service order-svc trong 30 phút qua có route /api/orders”, một query đơn giản. Với text tự do kiểu "Failed to load user 42 at 12:00" thì phải regex, chậm và dễ vỡ khi ai đó đổi format log.
Hầu hết ngôn ngữ đều có thư viện structured logging tốt: pino hoặc winston (Node.js), zerolog hoặc zap (Go), structlog (Python), serilog (.NET). Nên dùng từ ngày đầu, migrate log format sau này đau đầu lắm vì phải sửa khắp nơi.
Một điều quan trọng hay bị bỏ qua: đừng log PII trần, password, token, email, số CMND tuyệt đối không nên xuất hiện trong log. Log user_id (khoá nội bộ) thay vì email. Nếu cần context thêm thì mask: email_suffix="@gmail.com". Lý do không chỉ là bảo mật: khi user xoá tài khoản theo GDPR, log chứa PII có thể cần purge, và đi purge data trong hệ thống log phân tán là cơn ác mộng. Dữ liệu nhạy cảm trong log là nợ compliance mà bạn không muốn gánh.
Chi phí log, thứ ít ai nghĩ tới cho đến khi bill tới
Log là trụ cột đắt nhất trong ba trụ cột, đơn giản vì nó sinh nhiều data nhất. Một endpoint QPS 10,000 mà log mỗi request, kể cả chỉ 200 bytes/line, thì một ngày đã gần 2 tỷ bytes, tức khoảng 1.7 GB chỉ cho một endpoint. Nhân lên cho cả hệ thống, IO nghẽn, storage phình, bill cloud tăng vèo.
Hai cách tiết kiệm phổ biến. Cách thứ nhất là sampling, log 1 trong N request ở level INFO, giữ lại đủ để thấy pattern nhưng không quá nhiều. Vẫn log 100% ERROR vì lỗi thì không được bỏ qua. Cách thứ hai là rate limit log theo key, ví dụ cùng một lỗi “connection refused to redis-primary:6379” thì không log quá 10 lần/giây, thay vào đó aggregate thành “connection refused (suppressed 847 times in last 10s)”. Đủ để biết có vấn đề mà không ngốn storage.
Retention cũng cần tính từ đầu, đừng để default rồi quên. Giữ log nóng 7-14 ngày cho debug hàng ngày, đây là khoảng thời gian bạn cần khi debug incident. Archive 90 ngày cho audit hoặc điều tra incident cũ mà khách hàng report muộn. Xa hơn thì đẩy xuống object storage rẻ (S3 Glacier, GCS Coldline). Thiết kế ngân sách log từ đầu, đến khi bill tăng 3 lần rồi mới giật mình thì muộn, và cắt log thì sợ mất data.
Metrics, đếm và đo tập hợp
Ba loại metric cơ bản
Metrics có vô số thuật ngữ nhưng thực ra chỉ cần hiểu ba loại cơ bản.
Counter chỉ tăng, không bao giờ giảm, tổng số request, tổng số lỗi, tổng bytes gửi đi. Bạn không nhìn giá trị tuyệt đối của counter mà nhìn tốc độ tăng (rate) để biết QPS hay error rate. Prometheus dùng rate() function cho việc này. Gauge là giá trị tại thời điểm hiện tại, có thể lên xuống, số connection đang dùng, memory used, queue length, goroutine count. Histogram (hoặc Summary) cho phân phối giá trị, quan trọng nhất là latency percentiles. P99 2 giây nghĩa là 1% request chậm hơn 2 giây. Nghe ít nhưng với 1 triệu request/ngày thì đó là 10,000 request bị ảnh hưởng, user ở đuôi phân phối vẫn là user.
Prometheus scrape endpoint /metrics định kỳ (thường 15-30 giây) là mô hình pull phổ biến nhất hiện tại. OpenTelemetry metrics đang dần thống nhất cách instrument, hỗ trợ cả push (qua OTLP) và pull.
Chuẩn đặt tên
Đặt tên metric tuỳ hứng sẽ thành vấn đề khi có vài chục service, mỗi team gọi khác nhau, team A dùng requestCount, team B dùng http_total_requests, team C dùng api.request.count. Query xuyên service thành cực hình. Theo Prometheus convention thì sẽ chuẩn và dễ query:
http_requests_total{service="order",route="/api/orders",method="POST",status="200"}
http_request_duration_seconds_bucket{service="order",route="/api/orders",le="0.5"}
db_connections_in_use{pool="primary"}
Snake_case, đơn vị rõ ràng trong tên (_seconds, _bytes, _total), label là metadata bổ sung. Team nào cũng hiểu, dashboard nào cũng dùng được, alert rule viết một lần áp dụng cho mọi service.
Cardinality, cạm bẫy lớn nhất của metrics
Đây là bài học mà hầu như ai làm metrics cũng phải trải qua ít nhất một lần. Cardinality là số tổ hợp label khả dĩ, và mỗi tổ hợp là một time series trong TSDB (time-series database). Series càng nhiều, TSDB càng nặng: dùng nhiều RAM hơn, scrape chậm hơn, query chậm hơn.
{route="/api/orders"} thì OK, số route template có giới hạn, tầm vài chục đến vài trăm. Nhưng {user_id="42"} thì mỗi user là một series mới, có triệu user là triệu series. Tệ hơn nữa: {url="/api/orders/abc123"}, ID nằm trong URL path, mỗi request có thể sinh series mới. Chỉ vài ngày là database metric phình nổ, không query được.
Quy tắc quan trọng: label phải có tập giá trị hữu hạn và nhỏ, status code (khoảng 5 giá trị phổ biến), route template (vài chục), HTTP method (4-5 cái), region (vài cái). Dữ liệu per-user hay per-request thuộc về log hoặc trace span attributes, không phải metric label.
Hay gặp tình huống team phải rebuild toàn bộ Prometheus cluster vì một service mới log request_path (có dynamic segment chứa UUID) vào label metric. Vài triệu series phình ra trong 2 ngày, query timeout, scrape chậm, Grafana đứng hình. Fix không chỉ là đổi code, phải chờ TTL retention hết để series cũ được dọn, hoặc compact thủ công. Bài học đắt nhưng nhớ lâu.
RED và USE, hai framework bắt đầu
Khi mới setup dashboard cho service, đừng cố nghĩ ra hàng trăm metric. Bắt đầu với hai framework đơn giản, cover được 80% use case.
RED dùng cho mọi service kiểu request-response: Rate, bao nhiêu request/giây đang nhận; Errors, bao nhiêu phần trăm trả lỗi; Duration, latency P50, P95, P99. Ba cái này trên dashboard là đủ cho first-response khi incident xảy ra. Bạn biết ngay service nào đang chậm, đang lỗi, đang bị tải nặng. Có thể thêm metric khác sau, nhưng RED là baseline.
USE dùng cho resource (CPU, memory, disk, network, connection pool): Utilization, resource đang dùng bao nhiêu phần trăm capacity; Saturation, hàng đợi có đang đầy không, request có đang chờ không; Errors, hardware error, network error, OOM kill. CPU utilization 80% có thể OK nếu là batch job chạy đêm. Nhưng nếu saturation (queue length) đang tăng liên tục thì bắt đầu lo, nghĩa là request đến nhanh hơn khả năng xử lý.
SLI, SLO và error budget
Ba khái niệm này nghe có vẻ SRE-heavy nhưng thực ra dev nên hiểu vì nó ảnh hưởng trực tiếp đến quyết định deploy.
SLI (Service Level Indicator) là con số đo được, ví dụ rate(5xx) / rate(total) hoặc histogram_quantile(0.99, ...). Nó là metric cụ thể mà bạn dùng để đánh giá health. SLO (Service Level Objective) là mục tiêu bạn cam kết, “99.9% request trả status < 500 trong 28 ngày rolling”. Đây là lời hứa của team bạn với user (hoặc team khác consume API của bạn). Error budget là phần cho phép lỗi, 100% - 99.9% = 0.1%, tức khoảng 43 phút downtime trong 30 ngày.
Metric không có SLO đi kèm thì chỉ là metric “xem cho biết”, không giúp ra quyết định. Khi có SLO, nó trở thành công cụ thực sự: “error budget còn 30%, deploy tính năng mới được” vs “budget sắp hết, freeze deploy, tập trung fix stability trước”. Rất thực dụng, không phải lý thuyết suông.
Traces, hành trình một request xuyên hệ thống
Mô hình trace và span
Có thể hình dung trace như một cây gia phả của request. Một trace có ID duy nhất (hex 16 bytes theo W3C Trace Context). Bên trong có nhiều span, mỗi span đại diện cho một đoạn công việc: một HTTP call đến service khác, một query DB, một lần đọc cache, một đoạn business logic. Span có span_id, parent_span_id (trỏ về span cha), thời gian bắt đầu/kết thúc, và attributes dạng key-value chứa context.
Khi service A gọi service B qua HTTP, A nhét traceparent header vào request, header này chứa trace ID và span ID hiện tại. Service B đọc header đó ra, tạo span con với parent_span_id trỏ về span của A, tiếp nối cây trace. Đây gọi là context propagation, và đây là phần quan trọng nhất cần setup đúng khi triển khai tracing. Nếu propagation bị đứt ở đâu đó (quên bật middleware, custom HTTP client không forward header), cây trace sẽ bị đứt ở đó, bạn thấy “lỗ đen” giữa hành trình request, không biết chuyện gì xảy ra.
Giá trị thực tế mà trace mang lại
Trace phát huy sức mạnh rõ nhất trong hệ thống microservices, nơi một request đi qua 5-10 service là chuyện bình thường. Không có trace, khi latency tăng bạn phải mở log từng service một, tìm timestamp khớp, ghép thủ công, tốn thời gian và dễ sót. Có trace, bạn mở waterfall view thấy ngay “80% thời gian nằm ở span gọi payment-gateway, span đó pending 3.2 giây trước khi timeout”.
Trace cũng phát hiện được fan-out không cần thiết. Ví dụ: một trace cho thấy request có 47 query DB nối tiếp nhau, mỗi query chỉ mất 50ms, log từng cái trông “bình thường”. Nhưng nhìn từ trace, tổng cộng 47 × 50ms = 2.35 giây, đó là dấu hiệu N+1 query rõ ràng. Fix bằng eager loading, latency giảm từ 2.5s xuống 200ms. Không có trace, rất khó nhận ra pattern này vì mỗi query đơn lẻ đều nhanh.
Và quan trọng nhất: trace liên kết với log và metric nhờ trace_id chung. Bạn thấy span lỗi trên Jaeger, copy trace_id, paste vào Loki, ra toàn bộ log của request đó trên mọi service nó đi qua. Đây là lúc ba trụ cột thực sự phát huy sức mạnh tổng hợp.
Sampling, vì ghi 100% trace không khả thi
Ở QPS thấp (vài trăm request/giây), ghi hết trace không sao. Nhưng khi QPS lên hàng chục nghìn, mỗi trace có 10-50 span, mỗi span vài trăm bytes, chi phí storage và processing tăng rất nhanh. Sampling là cách giải quyết.
Head-based sampling quyết định ngay tại entry point: random giữ 1 trong 100 request. Ưu điểm là đơn giản, dễ dự đoán chi phí (1% QPS), dễ implement. Nhược điểm là mất ngẫu nhiên, có thể vô tình bỏ qua request hiếm gặp nhưng quan trọng, ví dụ request lỗi chỉ chiếm 0.1% tổng traffic.
Tail-based sampling thông minh hơn: ghi buffer tất cả span vào collector, đợi request hoàn thành rồi mới quyết định giữ hay bỏ. Ưu tiên giữ trace có lỗi, trace chậm bất thường, trace từ endpoint business-critical. Nhược điểm là cần collector có memory đủ lớn để buffer (tỷ lệ thuận với QPS × trace duration), và phức tạp hơn khi vận hành.
Một tip hữu ích: force sample 100% cho request có header X-Debug-Trace: true hoặc request từ internal tool. Khi cần debug on-demand, bạn không muốn phụ thuộc vào may mắn “hy vọng request này rơi vào 1% được sample”. Cài đặt này đơn giản mà giá trị rất lớn.
Instrument đủ rộng
Auto-instrumentation của OpenTelemetry bật sẵn cho HTTP client/server, DB driver (Postgres, Redis, MySQL), message broker (Kafka, RabbitMQ), và các framework phổ biến (Express, NestJS, Spring, Django, FastAPI). Bật lên là có trace cơ bản mà không cần sửa code, thường chỉ cần thêm agent hoặc vài dòng init.
Nhưng nếu chỉ bật auto-instrument cho một số service mà quên service khác, cây span sẽ thưa thớt. Bạn thấy request vào service A mất 3 giây nhưng span bên trong chỉ show 200ms cho HTTP call rồi “biến mất”, 2.8 giây còn lại nằm ở service chưa được instrument. Đây là “blind spot” trong trace. Vậy nên phải instrument tất cả service trên đường đi của request, kể cả service “nhỏ” hay “không quan trọng”.
Attributes trên mỗi span nên tuân theo OpenTelemetry Semantic Conventions: service.name, service.version, http.method, http.route, http.status_code, db.system, db.operation, db.statement (truncated). Khi có lỗi: set error=true và ghi exception.message, exception.stacktrace. Jaeger, Tempo, Datadog đều hiểu chuẩn này, follow convention thì không cần custom mapping cho từng backend.
Tương quan ba trụ cột, chìa khoá là trace ID
Đây là phần tạo giá trị lớn nhất, mà cũng là phần nhiều team bỏ qua nhất. Ba trụ cột đứng riêng lẻ thì mỗi cái chỉ cho một phần bức tranh. Kết nối chúng qua trace ID thì bạn có toàn cảnh, và đó mới thực sự là observability.
Workflow debug nhanh nhất diễn ra như thế này. Metric alert báo “error rate /api/orders > 2%”. Click vào exemplar trên Grafana, exemplar là sample point trên biểu đồ metric có gắn trace_id, nhảy thẳng sang Tempo/Jaeger xem trace tương ứng. Trên span lỗi trong trace đó, lấy trace_id. Paste trace_id vào Loki/Elastic, ra toàn bộ log từ mọi service liên quan đến request đó. Đọc log, tìm nguyên nhân gốc. Ba bước nhảy, từ alert đến root cause.
Ba bước nhảy metrics → trace → log này chỉ hoạt động khi bạn đảm bảo ba điều. Thứ nhất, mọi service phải inject trace_id vào mọi log line, làm qua logging middleware một lần, không cần dev nhớ log thủ công mỗi lần viết code. Thứ hai, mọi HTTP call giữa service phải propagate traceparent header, dùng OTel SDK thì nó tự làm, chỉ cần đảm bảo custom HTTP client cũng forward. Thứ ba, metric phải hỗ trợ exemplar, Prometheus từ version 2.26+ đã support, cần bật feature flag.
Đầu tư setup context propagation và correlation một lần, thu lời mỗi incident sau đó. Mỗi lần debug nhanh hơn 10-15 phút so với grep log thủ công, nhân với số incident trong một năm, đó là con số rất đáng kể.
Baggage, truyền context xuyên service
Baggage là cặp key-value đi kèm trace qua mọi service trong chuỗi call, ví dụ tenant=acme, feature=new-checkout, deployment=canary. Hữu ích khi muốn filter log hoặc metric theo tenant mà không phải mỗi service tự tìm tenant từ DB, entry service (API gateway) set baggage, các service downstream đọc ra.
Nhưng baggage truyền qua HTTP header nên cần cẩn thận: đừng nhét dữ liệu lớn vì nó tăng size mỗi request (header đi qua mọi hop). Và tuyệt đối không nhét dữ liệu nhạy cảm (token, PII) vì nó visible ở mọi service, kể cả service third-party nếu bạn gọi external API.
OpenTelemetry, chuẩn mở giảm lock-in
Vài năm gần đây, OpenTelemetry (OTel) đã thống nhất mô hình cho cả ba trụ cột, và đang nhanh chóng trở thành tiêu chuẩn de facto. SDK có cho hầu hết ngôn ngữ phổ biến, Go, Java, Python, Node.js, .NET, Rust, C++. Auto-instrumentation bật sẵn cho nhiều framework và library. OTLP (OpenTelemetry Protocol, qua gRPC hoặc HTTP) là format truyền chung, mọi vendor lớn đều support.
Kiến trúc khuyến nghị: ứng dụng chỉ biết OTel SDK + OTLP, gửi tất cả telemetry (log, metric, trace) tới OTel Collector chạy sidecar hoặc daemonset. Collector nhận OTLP, xử lý (batch, filter, sample, enrich attributes), rồi xuất ra backend bất kỳ, Prometheus cho metric, Tempo/Jaeger cho trace, Loki/Elastic cho log. Hoặc Datadog, New Relic, Honeycomb nếu dùng SaaS.
Lợi ích lớn nhất của kiến trúc này: đổi vendor = đổi config collector, không đổi code ứng dụng. Ví dụ: migrate từ Datadog sang Grafana stack (Prometheus + Tempo + Loki), code ứng dụng không đổi một dòng, chỉ đổi exporter config trong collector manifest. Đây là lý do nên dùng OTel thay vì vendor-specific SDK, dù vendor nào cũng support OTel rồi.
Alert, biến metric thành hành động
Metric đẹp trên dashboard mà không có alert đi kèm thì sớm muộn cũng bị lờ đi. Không ai ngồi stare dashboard cả ngày, người ta chỉ mở khi đã có incident. Alert biến metric thành notification chủ động.
Nguyên tắc hiệu quả: alert theo SLO, không theo resource threshold. “CPU > 80%” là triệu chứng, không phải tác động lên user, có khi CPU 90% mà user vẫn happy vì latency vẫn thấp. “Latency P99 > 2s trong 5 phút liên tục” gần hơn với trải nghiệm thật của user. Burn rate alert cho SLO còn tốt hơn nữa, nó tính tốc độ tiêu error budget, cảnh báo khi budget đang bị tiêu quá nhanh, cho bạn thời gian phản ứng trước khi SLO bị vi phạm.
Mỗi alert phải có runbook kèm, link tới document nói on-call nên kiểm tra gì đầu tiên, chạy command gì để verify, escalate cho ai nếu không tự fix được. Alert không có runbook thì người trực 2 giờ sáng chỉ biết hoảng và ping senior trên Slack, không giúp gì cho MTTR.
Alert fatigue là vấn đề thật mà nhiều team gặp. Nếu trong tuần bạn nhận hàng chục alert mà response là “không ai làm gì, tự hết” thì có hai khả năng: alert đó sai ngưỡng cần tune lại, hoặc có vấn đề thật nhưng team đang ignore vì quá quen. Cả hai đều nguy hiểm. Ignore alert là thói quen phá hoại nhất trong vận hành, vì đến khi alert thật sự critical reo, không ai để ý nữa.
Anti-pattern hay gặp
Dưới đây là những pattern hay gặp, liệt kê để bạn khỏi lặp lại.
Log thay cho metric. Parse log file để đếm request, tính error rate, chậm, tốn resource, fragile khi format log thay đổi. Counter metric sinh ra để làm đúng việc này, nhanh hơn nhiều bậc.
Metric thay cho log. Nhồi chi tiết request vào label metric, user_id, request_path, payload hash, cardinality nổ tung, TSDB chết. Chi tiết per-request thuộc về log hoặc trace span attributes, không phải metric label.
Trace lẻ tẻ. Một nửa service có trace, nửa kia không. Kết quả: cây span có “lỗ đen” ở giữa, bạn thấy request vào service A rồi biến mất, 2 giây sau xuất hiện ở service C mà không biết chuyện gì xảy ra ở giữa. Instrument tất cả hoặc trace gần như vô dụng cho cross-service debugging.
Không đồng bộ timestamp. Clock drift giữa các server làm trace trông sai thứ tự, span con có vẻ bắt đầu trước span cha, waterfall view vẽ lung tung. Chạy NTP hoặc chrony trên mọi server, kiểm tra drift định kỳ. Đây là bug ngớ ngẩn nhưng gây confuse khủng khiếp khi đọc trace.
Bật DEBUG toàn hệ thống. Log nổ, storage nổ, tín hiệu tốt chìm trong biển nhiễu. Chỉ bật DEBUG cho service cụ thể, trong khoảng thời gian ngắn, qua feature flag hoặc config reload, không phải redeploy. Và nhớ tắt sau khi debug xong.
Không audit chi phí observability. Đến khi bill SaaS observability tăng 3 lần so với tháng trước mới ngạc nhiên. Đặt budget sớm, review monthly, set alert cho chính chi phí observability nữa, nghe meta nhưng thật sự cần thiết.
Tóm tắt
Logs kể chi tiết từng sự kiện, structured JSON bắt buộc, trace_id trong mọi log line, tuyệt đối không log PII. Metrics đếm và đo tập hợp, RED/USE làm framework khởi đầu, label cardinality phải thấp, có SLO đi kèm thì mới có giá trị ra quyết định. Traces vẽ hành trình request xuyên hệ thống, sample hợp lý, instrument đủ rộng, attributes theo OTel semantic conventions.
Tương quan ba trụ cột qua trace_id và exemplar là công cụ gỡ lỗi production mạnh nhất hiện có. Setup một lần, mỗi incident sau đó đi từ “có gì đó hỏng” tới “đây là nguyên nhân cụ thể” nhanh hơn hàng chục phút. Observability không phải “cài tool rồi chờ xem”, đó là thứ cần thiết kế vào kiến trúc từ ngày đầu, cũng như bạn thiết kế database schema hay API contract vậy.