Observability 7 — Logs & Traces (Loki, Tempo, OpenTelemetry)

13 thg 7, 2026 2 lượt xem
#tracing
#observability
#devops
#loki
#opentelemetry

Ba trụ cột: metric là chưa đủ

Suốt các bài trước, series này xoay quanh metric — con số tổng hợp theo thời gian: QPS, latency p99, tỷ lệ lỗi. Metric trả lời rất tốt câu hỏi "có gì đó đang sai không?""sai nhiều đến mức nào?". Nhưng metric có một giới hạn cố hữu: nó là dữ liệu đã bị tổng hợp. Khi một alert bắn "error ratio 8%", metric cho bạn biết mức độ nhưng không cho biết request nào lỗi, lỗi ở bước nào, và tại sao. Đó là lúc cần hai trụ cột còn lại của observability.

Mô hình "ba trụ cột" (three pillars) kinh điển:

Trụ cộtTrả lời câu hỏiBản chất dữ liệuChi phí lưu
MetricsCó sai không? Sai bao nhiêu?Số tổng hợp theo thời gian (aggregated)Rẻ nhất
LogsChuyện gì đã xảy ra ở thời điểm đó?Bản ghi sự kiện rời rạc, giàu chi tiếtTrung bình–đắt
TracesRequest đi qua đâu, chậm/hỏng ở đâu?Đường đi của một request qua nhiều serviceTrung bình

Ba trụ cột bổ sung cho nhau chứ không thay thế. Quy trình điều tra lý tưởng đi từ tổng quát đến chi tiết: alert (metric) → dashboard xác nhận (metric) → tìm request lỗi cụ thể (trace) → đọc log chi tiết của service hỏng (log). Bài này lấp hai trụ cột logs và traces, rồi chỉ cách nối chúng lại — phần giá trị nhất.

Trụ cột LOGS

Structured logging: nền tảng của mọi thứ

Log truyền thống là chuỗi văn bản tự do: [2026-07-03 14:03:12] ERROR payment failed for account 12345. Con người đọc được, nhưng máy thì khổ sở: muốn lọc theo account phải parse regex, muốn thống kê phải grep + đếm. Trong hệ phân tán hàng chục service, log dạng này gần như vô dụng khi cần truy vấn.

Structured logging (log có cấu trúc) giải quyết bằng cách phát log dưới dạng JSON với các trường được đặt tên rõ ràng:

{
  "timestamp": "2026-07-03T14:03:12.481Z",
  "level": "error",
  "service": "payment-gateway",
  "message": "payment authorization failed",
  "request_id": "req-9f3a2b",
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "account_id": 12345,
  "amount": 5000000,
  "downstream": "core-banking",
  "error_code": "TIMEOUT"
}

Ba yếu tố bắt buộc của log tốt trong hệ phân tán:

  • Level chuẩn hoádebug / info / warn / error. Cho phép lọc nhanh và đặt cảnh báo theo mức. Trong ngân hàng, tuyệt đối phân biệt warn (đáng chú ý) và error (đã hỏng, cần người xử lý).
  • request_id — mã định danh một request ở phạm vi một service. Mọi dòng log sinh ra trong lúc xử lý cùng một request đều mang chung request_id, cho phép gom lại thành một câu chuyện hoàn chỉnh.
  • trace_id — mã định danh xuyên suốt nhiều service (sẽ nói kỹ ở phần traces). Đây là chiếc chìa khoá vàng nối log với trace: có trace_id trong log nghĩa là từ một request lỗi ở trace, bạn nhảy thẳng được sang toàn bộ log liên quan.

Một lưu ý sống còn với ngân hàng: không log dữ liệu nhạy cảm. Số thẻ đầy đủ, mật khẩu, OTP, số dư chi tiết theo khách hàng — phải mask hoặc loại bỏ. Log tập trung thường được nhiều người truy cập, và log là nơi rò rỉ PII phổ biến nhất. Liên hệ nguyên tắc kiểm soát truy cập ở Bảo mật & SRE.

Grafana Loki: "Prometheus cho log"

Cách tư duy nhanh nhất về Grafana Loki: nó là Prometheus phiên bản dành cho log. Cùng triết lý, cùng mô hình nhãn (label), tích hợp liền mạch với Grafana — chỉ khác là lưu dòng log thay vì chuỗi số.

Ý tưởng cốt lõi và cũng là khác biệt lớn nhất so với Elasticsearch: Loki KHÔNG index nội dung (full-text) của log, chỉ index metadata dạng label. Mỗi luồng log (log stream) được gán một tập label như {service="payment-gateway", level="error", env="prod"}. Loki dựng chỉ mục cho các label này; còn nội dung dòng log thì được nén và lưu thô (thường trên object storage như S3), không đánh chỉ mục.

Hệ quả thực tế:

  • Rẻ hơn nhiều — không phải xây và lưu chỉ mục full-text khổng lồ (chỉ mục Elasticsearch có thể lớn ngang hoặc hơn dữ liệu gốc). Loki chủ yếu tốn tiền lưu trữ object storage giá rẻ.
  • Ghi (ingest) nhẹ — không tốn CPU phân tích, tokenize từng từ khi nhận log.
  • Đánh đổi: truy vấn theo từ khoá tự do sẽ quét tuần tự (grep) qua các log stream khớp label, thay vì tra chỉ mục tức thì. Nếu label thu hẹp tốt phạm vi (ví dụ đúng service + đúng khung giờ + level=error), phần cần quét nhỏ nên vẫn nhanh. Nếu bạn quét từ khoá trên toàn bộ log không lọc label, sẽ chậm.

So sánh trực tiếp với Elasticsearch (xem Elasticsearch — tổng quanindexing/ingest):

Tiêu chíGrafana LokiElasticsearch
IndexChỉ index label (metadata)Index full-text toàn bộ nội dung
Chi phí lưu & vận hànhThấpCao (chỉ mục lớn, cần nhiều RAM)
Truy vấn từ khoá tự doQuét tuần tự trong stream đã lọc labelRất nhanh (inverted index)
Phân tích/aggregation phức tạpHạn chếMạnh (search analytics đầy đủ)
Tích hợp Grafana & metricRất chặt (cùng hệ)Qua data source riêng
Hợp khiLog vận hành, tương quan với metricSearch/audit trail cần tìm kiếm mạnh

Nguyên tắc chọn: nếu mục tiêu là log vận hành để điều tra sự cố cùng metric, Loki thường đủ và rẻ hơn hẳn. Nếu cần tìm kiếm/phân tích mạnh trên audit trail (ví dụ tra cứu lịch sử giao dịch theo nhiều tiêu chí tự do), Elasticsearch phù hợp hơn. Nhiều nhà băng chạy cả hai cho hai mục đích khác nhau.

LogQL: truy vấn log

Loki dùng ngôn ngữ truy vấn LogQL, cố tình thiết kế giống PromQL để ai đã biết PromQL sẽ thấy quen. Một truy vấn LogQL gồm hai phần: log stream selector (chọn stream theo label, trong {}) và filter/pipeline (lọc, phân tích nội dung).

Lọc log lỗi của payment-gateway chứa từ "timeout" (minh hoạ LogQL, không phải SQL):

{service="payment-gateway", level="error"} |= "timeout"

Toán tử filter: |= chứa, != không chứa, |~ khớp regex, !~ không khớp regex. Việc chọn label trước ({service=..., level="error"}) quyết định hiệu năng — nó thu nhỏ phần log phải quét.

LogQL còn có metric queries: biến log thành số để vẽ đồ thị như metric. Ví dụ đếm số dòng log lỗi mỗi phút:

sum(rate({service="payment-gateway", level="error"}[1m]))

Hoặc trích trường JSON rồi tính p99 latency ghi trong log:

quantile_over_time(0.99,
  {service="payment-gateway"} | json | unwrap latency_ms [5m])

Chính khả năng này khiến Loki mạnh: có thể dựng cảnh báo "số log error/phút vượt ngưỡng" ngay trên log, và đặt panel log cạnh panel metric trên cùng dashboard Grafana (Grafana).

Thu thập log: agent đẩy log vào Loki

Ứng dụng ghi log ra stdout hoặc file; cần một agent thu và đẩy vào Loki. Các lựa chọn phổ biến:

  • Promtail — agent truyền thống của hệ Loki: đọc file/journal, gán label, đẩy vào Loki. (Đang dần được thay bằng Grafana Alloy — agent hợp nhất, nhưng vai trò tương tự.)
  • Grafana Alloy / OpenTelemetry Collector — agent thu thập đa năng, xử lý cả log, metric, trace.

Điểm cần cẩn trọng khi cấu hình agent là gán label. Label tốt = ít giá trị (low cardinality): service, env, level, namespace. Tuyệt đối không đưa giá trị biến thiên cao (high cardinality) như request_id, user_id, trace_id vào label — mỗi giá trị unique tạo một log stream mới, làm nổ số stream và giết hiệu năng Loki. Những trường đó nằm trong nội dung log JSON và được lọc bằng pipeline | json, chứ không phải label. Đây đúng là bài học cardinality từ Prometheus (Instrumentation) áp dụng sang log.

# promtail — minh hoạ cấu hình (KHÔNG phải SQL)
scrape_configs:
  - job_name: payment-gateway
    static_configs:
      - targets: [localhost]
        labels:
          service: payment-gateway
          env: prod
          __path__: /var/log/payment/*.log
    pipeline_stages:
      - json:
          expressions:
            level: level
            trace_id: trace_id   # trích ra để dùng, KHÔNG set làm label
      - labels:
          level:                 # chỉ level lên label (low cardinality)

Trụ cột TRACES

Distributed tracing là gì

Trong kiến trúc microservice, một request của người dùng thường không được xử lý bởi một service duy nhất. Một lệnh "chuyển khoản" ở NCB có thể đi qua: api-gatewaypayment-gatewaycore-bankingfraud-checkledger, mỗi bước lại gọi database hoặc dịch vụ ngoài. Khi request này chậm hoặc lỗi, metric của từng service riêng lẻ không cho biết bức tranh tổng thể — mỗi service tưởng mình ổn, nhưng tổng thời gian lại 3 giây.

Distributed tracing (truy vết phân tán) giải quyết đúng bài toán đó: nó theo dõi một request duy nhất khi nó đi xuyên qua toàn bộ hệ thống, ghi lại từng chặng dừng với thời gian bắt đầu/kết thúc.

Hai khái niệm cốt lõi:

  • Span — một đơn vị công việc có tên, thời điểm bắt đầu, thời lượng, và metadata (attributes). Ví dụ: span "gọi core-banking", span "query DB", span "kiểm tra fraud". Mỗi span có thể có span cha (parent) và span con.
  • Trace — tập hợp mọi span của cùng một request, buộc lại bằng một trace_id chung, tổ chức thành cây theo quan hệ cha–con. Nhìn vào một trace, bạn thấy toàn bộ request như một sơ đồ Gantt: chặng nào chạy tuần tự, chặng nào song song, chặng nào ngốn hết thời gian.

Context propagation: sợi chỉ nối các span

Điều gì khiến các span rải rác ở nhiều service biết chúng thuộc cùng một trace? Câu trả lời là context propagation (truyền ngữ cảnh). Khi service A gọi service B qua HTTP, nó đính kèm trace_idspan_id cha vào header của request. Service B đọc header đó, tạo span mới với parent chính là span của A, và tiếp tục truyền tiếp cho service C nó gọi.

Chuẩn header phổ biến nhất hiện nay là W3C Trace Context với header traceparent:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             └┬┘ └──────────── trace_id ─────────────┘ └── span_id ──┘ └┬┘
           version                                                    flags

Chính trace_id này cũng là thứ ta ghi vào structured log ở phần trên — đó là cách một dòng log biết mình thuộc trace nào. Không có context propagation, tracing sụp đổ: mỗi service tạo trace riêng, không nối được thành đường đi hoàn chỉnh.

Vì sao microservice bắt buộc cần tracing

Với monolith, một request nằm gọn trong một process — stack trace và log cục bộ là đủ. Với microservice, các lý do khiến tracing gần như bắt buộc:

  • Xác định thủ phạm trong chuỗi gọi: request 3 giây, nhưng của service nào? Trace chỉ thẳng span ngốn 2.4 giây.
  • Thấy fan-out ẩn: một request tưởng đơn giản hoá ra gọi DB 40 lần (N+1) — chỉ trace mới lộ ra.
  • Phân biệt lỗi lan truyền: fraud-check timeout khiến payment-gateway báo lỗi — trace cho thấy gốc rễ ở đâu, tránh đổ lỗi nhầm service.

Grafana Tempo & Jaeger

Hai backend lưu trữ và truy vấn trace phổ biến:

  • Grafana Tempo — backend trace của hệ Grafana. Triết lý giống Loki: rẻ, chỉ cần object storage, không index toàn bộ span mà chủ yếu tra theo trace_id. Tích hợp cực chặt với Grafana và với Loki/Prometheus — đây là điểm mạnh lớn nhất khi bạn đã dùng stack Grafana.
  • Jaeger — dự án tracing trưởng thành (gốc từ Uber, nay thuộc CNCF), UI phân tích trace rất tốt, tra cứu theo service/operation/tag phong phú hơn. Nhiều tổ chức dùng Jaeger như hệ tracing độc lập.

Cả hai đều nhận dữ liệu qua OpenTelemetry, nên lựa chọn backend không khoá bạn vào một cách instrument ứng dụng cố định.

OpenTelemetry: chuẩn mở thống nhất

Trước đây, mỗi hệ tracing/metric có SDK và định dạng riêng — chọn Jaeger thì code gắn chặt Jaeger, đổi sang cái khác phải viết lại instrumentation. OpenTelemetry (OTel) ra đời để chấm dứt cảnh đó: nó là chuẩn mở, vendor-neutral, thống nhất cả ba tín hiệu metrics + logs + traces. Đây là dự án của CNCF và đang trở thành tiêu chuẩn de-facto của toàn ngành observability.

OTel gồm hai phần chính:

  • SDK / API — thư viện gắn vào ứng dụng để sinh span, metric, log theo một chuẩn duy nhất. Nhiều framework còn có auto-instrumentation: tự động tạo span cho HTTP server, HTTP client, truy vấn DB mà gần như không cần sửa code.
  • OpenTelemetry Collector — một tiến trình trung gian nhận (receive) dữ liệu từ app, xử lý (batch, lọc, sampling, thêm attribute), rồi export ra nhiều backend cùng lúc. Collector là điểm tách rời tuyệt vời: app chỉ nói "OTLP" (giao thức OTel), còn đưa dữ liệu đi đâu do Collector quyết định.

Giá trị chiến lược của OTel với ngân hàng: vendor-neutral. Bạn instrument ứng dụng một lần theo OTel, sau này muốn đổi backend (Tempo → Jaeger, hay đổi nhà cung cấp APM thương mại) chỉ cần đổi cấu hình export ở Collector, không đụng vào code hàng chục service. Với hệ thống core-banking sống hàng chục năm, tránh khoá cứng nhà cung cấp là một lợi ích lớn.

Tương quan ba trụ cột: giá trị thật sự

Ba trụ cột rời rạc chỉ có giá trị hạn chế. Sức mạnh thực sự đến từ việc nối chúng lại để nhảy qua lại trong lúc điều tra. Hai "chiếc cầu" quan trọng nhất:

  1. Exemplar — nối metric → trace. Exemplar là một mẫu ví dụ được đính kèm vào một điểm dữ liệu metric (thường là bucket của histogram latency), mang theo trace_id của một request cụ thể tạo ra giá trị đó. Nhìn thấy p99 latency vọt lên trên đồ thị Grafana, bạn click vào điểm đỉnh và exemplar đưa thẳng bạn tới trace của một request thật đã chậm — thay vì đoán mò.
  2. trace_id trong log — nối trace → log. Đã cấy trace_id vào structured log ở đầu bài, giờ từ một trace lỗi trong Tempo, Grafana cho phép nhảy sang Loki lọc {...} | json | trace_id="4bf92f..." để xem mọi dòng log chi tiết của đúng request đó, xuyên mọi service.

Quy trình điều tra hoàn chỉnh, đi từ mơ hồ đến cụ thể:

Vòng lặp này — alert → metric dashboard → trace request lỗi → log chi tiết — là lý do người ta xây observability. Không có tương quan, mỗi bước là một cuộc tìm kiếm thủ công tốn hàng chục phút; có tương quan, cả chuỗi rút xuống vài cú click.

Sampling: không phải trace nào cũng lưu

Ở lưu lượng cao, lưu mọi trace là quá tốn (mỗi giao dịch sinh hàng chục span). Sampling (lấy mẫu) chọn giữ lại một phần trace. Hai chiến lược:

  • Head-based sampling — quyết định giữ/bỏ ngay tại điểm đầu (khi request bắt đầu), thường theo tỉ lệ cố định (ví dụ giữ 10%). Đơn giản, nhẹ, quyết định cục bộ; nhưng có thể vô tình bỏ đúng trace lỗi vì lúc bắt đầu chưa biết request sẽ lỗi.
  • Tail-based sampling — quyết định sau khi trace hoàn tất, dựa trên kết quả: giữ toàn bộ trace lỗi và trace chậm (ví dụ latency > 1s), chỉ lấy mẫu thưa các trace bình thường. Bắt được đúng thứ đáng điều tra; đổi lại tốn tài nguyên hơn vì Collector phải giữ tạm mọi span của một trace tới khi nó kết thúc rồi mới quyết định.

Với ngân hàng, cấu hình thực tế thường là tail-based: giữ 100% trace lỗi và trace vượt ngưỡng latency, lấy mẫu ~1–5% trace thành công. Cách này đảm bảo mọi sự cố đều có trace để điều tra, mà không phải trả tiền lưu hàng triệu trace "khoẻ mạnh" vô ích.

Use case thực tế

Bối cảnh: 14:03 giờ cao điểm, alert bắn "payment-gateway error ratio 8%" (theo Alerting). Đội SRE NCB cần tìm nguyên nhân trước khi khách hàng khiếu nại lan rộng.

Điều tra theo tương quan ba trụ cột:

  1. Metric (dashboard Grafana): panel error ratio đỏ, panel p99 latency payment-gateway nhảy từ 200ms lên 2.8s. Metric xác nhận sự cố có thật và định vị service, nhưng chưa rõ vì sao.
  2. Metric → Trace (exemplar): click điểm đỉnh p99, exemplar mang trace_id=4bf92f... mở trace trong Tempo. Trace cho thấy span payment-gateway → core-banking chiếm 2.4s trong tổng 2.8s — thủ phạm là chặng gọi core-banking, không phải bản thân payment-gateway.
  3. Trace → Log (trace_id): từ trace, nhảy sang Loki lọc {service="core-banking"} | json | trace_id="4bf92f...". Log chi tiết lộ hàng loạt error_code=DB_POOL_EXHAUSTED — connection pool tới database cạn kiệt.
  4. Đối chiếu nghiệp vụ: để định lượng ảnh hưởng, đội đối chiếu số giao dịch bị ảnh hưởng theo phút trên sandbox nghiệp vụ (PostgreSQL chỉ đọc):
-- ▶ Chạy được
SELECT date_trunc('minute', created_at) AS minute, kind, COUNT(*) AS n
FROM transactions
WHERE created_at >= NOW() - INTERVAL '30 minutes'
GROUP BY 1, 2
ORDER BY 1 DESC;

Kết quả: nguyên nhân gốc là connection pool DB của core-banking bị cạn (một truy vấn báo cáo nặng chiếm hết connection). Toàn bộ chuỗi điều tra — từ alert tới nguyên nhân gốc — mất dưới 5 phút nhờ exemplar và trace_id nối liền ba trụ cột. Không có tương quan, riêng bước tìm ra "lỗi nằm ở core-banking chứ không phải payment-gateway" đã có thể ngốn cả buổi mò log rời rạc từng service.

Ghi nhớ

  • Ba trụ cột bổ sung nhau: metric (có sai không?) → trace (sai ở đâu?) → log (chi tiết vì sao?). Metric là dữ liệu đã tổng hợp nên không đủ để điều tra một mình.
  • Structured logging (JSON) với level, request_id, trace_id là nền tảng. trace_id trong log là chìa khoá nối log với trace. Không log dữ liệu nhạy cảm (số thẻ, OTP, PII).
  • Loki = "Prometheus cho log": chỉ index label chứ không full-text → rẻ và ghi nhẹ, đổi lại truy vấn từ khoá là quét tuần tự trong stream đã lọc label. Chọn label low-cardinality; đừng đưa trace_id/user_id làm label.
  • Loki vs Elasticsearch: Loki cho log vận hành tương quan metric (rẻ); Elasticsearch cho search/audit trail cần tìm kiếm mạnh (đắt hơn). Xem es-01, es-05.
  • LogQL giống PromQL: {selector} chọn stream theo label, rồi |=/|~/| json lọc và phân tích; có cả metric query để đếm/tính trên log.
  • Distributed tracing: span (một đơn vị công việc) buộc thành trace bằng trace_id chung; context propagation (header traceparent) là sợi chỉ nối span xuyên service. Bắt buộc với microservice để định vị thủ phạm trong chuỗi gọi.
  • Tempo (rẻ, hợp stack Grafana) và Jaeger (UI phân tích mạnh) là hai backend trace phổ biến.
  • OpenTelemetry (OTel): chuẩn mở, vendor-neutral, thống nhất metrics/logs/traces; gồm SDK (instrument, có auto-instrumentation) + Collector (nhận, xử lý, export đa backend). Instrument một lần, đổi backend không sửa code — tránh khoá nhà cung cấp.
  • Tương quan qua exemplar (metric→trace) và trace_id trong log (trace→log) biến điều tra từ hàng chục phút mò mẫm thành vài cú click.
  • Sampling: head-based (quyết định lúc đầu, rẻ, có thể bỏ sót lỗi) vs tail-based (quyết định sau khi trace xong, giữ trọn trace lỗi/chậm — thường dùng trong ngân hàng).

Bài viết liên quan

Continuous Integration/Delivery/Deployment, cấu trúc pipeline, ví dụ GitHub Actions và chiến lược release.

13 thg 7, 2026 4

Container vs máy ảo, image/layer, Dockerfile, volume, network, Docker Compose và best practices.

13 thg 7, 2026 3

Vì sao cần orchestration; Pod, Deployment, Service, Ingress; scaling, self-healing và cấu hình.

13 thg 7, 2026 3

Bản chất DevOps: phá bỏ rào cản Dev–Ops, vòng lặp vô tận, CALMS, và vì sao tự động hoá.

13 thg 7, 2026 3