Log, metric, trace (và profile): mỗi thứ trả lời câu hỏi gì

4 câu hỏi phỏng vấn~25 phútTừ khóa: observability, monitoring, three pillars, profiles, structured logging, JSON log, logstash-logback-encoder, log level, MDC, traceId, correlation, cardinality, sampling, retention, cost

Bắt đầu chương Observability nhé. shop-app giờ chạy trên Kubernetes với order, payment, inventory rồi, mình nên quan sát nó thế nào?

Câu hỏi gốc của observability là: khi có chuyện, mình trả lời được "chuyện gì, ở đâu, vì sao" mà không cần deploy thêm code không? Để làm được, cần bốn loại tín hiệu, mỗi loại trả lời một câu hỏi khác nhau.

1. Bài toán

Thứ Sáu 20:00, chạy khuyến mãi. Người dùng báo checkout chậm, có lúc lỗi 500. shop-app có 3 service, mỗi service 4 Pod:

Ingress → order-service (4 Pod) → payment-service (4 Pod)
                               → inventory-service (4 Pod) → postgres

Thứ đang có: System.out.println rải rác và kubectl logs. Không có dashboard, không có alert. Người phát hiện sự cố là khách hàng.

2. Vấn đề

3. Giải pháp: 3 trụ cột + profile

Tín hiệuTrả lời câu hỏiDạng dữ liệuCông cụ hay gặp
MetricCó chuyện gì không? Nặng cỡ nào? (error rate 5%, p99 3s)Số theo thời gian, đã gộpMicrometer → Prometheus → Grafana
TraceChậm/lỗi ở service nào, bước nào?Cây span của một requestOpenTelemetry → Tempo/Jaeger
LogChính xác đã xảy ra gì? (exception, giá trị biến)Sự kiện rời rạc, chi tiếtJSON log → Loki / Elasticsearch
ProfileDòng code nào tốn CPU/memory/lock?Stack trace lấy mẫuJFR, async-profiler, Pyroscope

Monitoring là theo dõi những thứ mình đã biết sẽ hỏng (CPU, disk). Observability là khả năng hỏi những câu chưa biết trước nhờ dữ liệu đủ giàu và nối được với nhau. Chất keo nối chúng là traceId.

4. Code từng bước

Bước 1: Dependency (mỗi service)

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>

Phần tracing chi tiết đã có ở Distributed tracing, actuator metrics ở Actuator. Bài này tập trung vào log và cách ghép bốn tín hiệu.

Bước 2: Structured JSON log

Spring Boot 3.4+ có sẵn structured logging, không cần thư viện thêm:

# application.yml
spring:
  application:
    name: order-service
logging:
  structured:
    format:
      console: ecs        # hoặc logstash, gelf
  level:
    root: INFO
    com.shop: INFO
    org.hibernate.SQL: WARN
management:
  tracing:
    sampling:
      probability: ${TRACE_SAMPLING:0.1}

Một dòng log ra stdout trông như thế này (đã xuống dòng cho dễ đọc):

{"@timestamp":"2026-10-09T20:03:11.482Z","log.level":"ERROR",
 "service.name":"order-service","process.thread.name":"http-nio-8080-exec-7",
 "log.logger":"com.shop.order.OrderService",
 "message":"Payment failed","orderId":"1234","customerTier":"GOLD",
 "traceId":"4bf92f3577b34da6a3ce929d0e0e4736","spanId":"00f067aa0ba902b7",
 "error.type":"java.net.SocketTimeoutException","error.stack_trace":"..."}

Bước 3: Code Java, log có ngữ cảnh

@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {

    private final PaymentClient paymentClient;

    public Order checkout(CheckoutRequest req) {
        // Gắn thuộc tính nghiệp vụ vào MDC: mọi dòng log trong request đều mang theo
        try (var ignored = MDC.putCloseable("orderId", req.orderId())) {
            log.info("Checkout started items={}", req.items().size());
            try {
                paymentClient.charge(req.orderId(), req.amount());
            } catch (PaymentTimeoutException e) {
                // Log 1 lần ở nơi xử lý, kèm exception; không log rồi rethrow
                log.atError()
                   .addKeyValue("amount", req.amount())
                   .setCause(e)
                   .log("Payment failed");
                throw e;
            }
            return Order.confirmed(req.orderId());
        }
    }
}

traceId/spanId do Micrometer Tracing tự đặt vào MDC, mình không phải làm gì. orderId mình tự thêm để tìm theo nghiệp vụ.

Bước 4: Kiểm tra

curl -s -X POST localhost:8080/orders -H 'Content-Type: application/json' \
     -d '{"orderId":"1234","amount":99.0,"items":[{"sku":"A1","qty":1}]}'

kubectl logs deploy/order-service -n shop | jq 'select(.orderId=="1234")'

# Trong Grafana Loki: tìm toàn bộ log của một request qua cả 3 service
{namespace="shop"} | json | traceId="4bf92f3577b34da6a3ce929d0e0e4736"

5. Cách hoạt động: từ alert tới dòng code

[Metric]  Grafana: p99 checkout 3.2s, error 4%     ← alert kêu
     │      click vào điểm trên đồ thị (exemplar có traceId)
     ▼
[Trace]   Tempo: trace 4bf92f...
          order-service  POST /orders       3.1s
            └ payment-service POST /charge  3.0s   ← chậm ở đây
                 └ HTTP bank-gateway        3.0s timeout
     │      "Logs for this span" (lọc theo traceId)
     ▼
[Log]     Loki: ERROR Payment failed orderId=1234 SocketTimeoutException
     │      "Profile for this span"
     ▼
[Profile] Pyroscope: 60% CPU ở Jackson serialize trong RetryInterceptor

Đường ống trên Kubernetes:
Pod stdout ──► node agent (Alloy / Fluent Bit) ──► Loki / Elasticsearch
/actuator/prometheus ◄── Prometheus scrape (pull)
OTLP spans ──► OTel Collector ──► Tempo / Jaeger

6. So sánh: chi phí và cardinality

MetricLogTraceProfile
Chi phí theoSố time series (label)Số byte ghi + indexSố span lưuThấp (lấy mẫu ~100Hz)
Chi tiết từng requestKhôngCóCóKhông (gộp)
Giữ bao lâu (hay gặp)13 tháng (downsample)7–30 ngày3–14 ngày7–30 ngày
Dùng để alertRất hợpHạn chếKhôngKhông
BẫyLabel userId → nổ seriesLog DEBUG trên prod → hóa đơn to100% sampling → tốnÍt

Cardinality = số giá trị khác nhau. orderId cho vào log thì được (log là sự kiện), cho vào label metric thì mỗi đơn hàng thành một time series riêng, Prometheus sẽ chết. Chi tiết ở bài 19.3.

7. Khi nào nên dùng / không nên dùng

Log level thì mình hay phân vân. Payment trả về "thẻ bị từ chối" thì log ERROR hay WARN nhỉ?

WARN, hoặc thậm chí INFO. Quy tắc dễ nhớ: ERROR là thứ cần người can thiệp. Thẻ bị từ chối là kết quả nghiệp vụ bình thường, hệ thống vẫn làm đúng việc.

LevelNghĩaVí dụ shop-app
ERRORHệ thống hỏng, cần người xemKhông kết nối được postgres, bug NPE, gọi payment timeout sau hết retry
WARNBất thường nhưng tự xử lý đượcRetry lần 2 mới thành công, circuit breaker mở, config deprecated
INFOSự kiện nghiệp vụ quan trọngĐơn tạo xong, thẻ bị từ chối, app start
DEBUGChi tiết cho devRequest body, SQL, giá trị trung gian
TRACERất chi tiếtTừng vòng lặp; gần như không bật trên prod

Nếu ERROR bị lạm dụng, dashboard "số dòng ERROR" luôn đỏ và mọi người học cách phớt lờ nó. Đó là lúc lỗi thật bị chìm.

Hai lỗi phổ biến nữa:

Log JSON tốn chỗ hơn text mà. Mỗi ngày shop-app ra khoảng 200GB log, sếp hỏi sao hóa đơn Elasticsearch cao vậy. Giảm kiểu gì?

Bạn hiểu gần đúng, JSON có dài hơn một chút, nhưng nén rất tốt và không phải thủ phạm chính. Thủ phạm thường là log những thứ không ai đọc và index mọi field. Đi theo thứ tự này:

1. Đo trước: service nào, logger nào ra nhiều nhất?
   Loki:  topk(10, sum by (app, logger) (bytes_over_time({namespace="shop"}[1h])))
2. Cắt nguồn:
   - Health check / readiness log mỗi 5s × 12 Pod → tắt
   - Log request body ở INFO → hạ xuống DEBUG
   - Log trong vòng lặp → log 1 dòng tổng kết
3. Lọc ở pipeline (Alloy / Fluent Bit): drop DEBUG, drop log của /actuator/*
4. Lấy mẫu log thành công, giữ 100% log lỗi
5. Phân tầng lưu trữ: 7 ngày nóng (tìm nhanh) → 90 ngày S3 (rẻ, chậm)
6. Loki chỉ index label (app, namespace, level), không index nội dung → rẻ hơn ES nhiều

Câu hỏi nên hỏi với mỗi dòng log: "Lúc sự cố, mình có đọc dòng này không?" Không thì bỏ, hoặc chuyển thành metric (counter rẻ hơn một triệu dòng log "processed item").

Phỏng vấn hỏi về log, metric, trace thì trả lời sao?

Q1. Ba trụ cột của observability là gì, mỗi cái dùng khi nào?

🇻🇳 Tiếng Việt:

Metric là số đã gộp theo thời gian, rẻ, dùng cho dashboard và alert: nó cho biết có chuyện và nặng cỡ nào. Trace theo một request đi qua các service, cho biết chậm hay lỗi ở bước nào. Log là sự kiện chi tiết, cho biết chính xác chuyện gì xảy ra. Nhiều nơi thêm profile làm trụ thứ tư để biết dòng code nào tốn CPU hay memory. Quy trình của em là metric báo động, trace khoanh vùng, log và profile tìm nguyên nhân, nối với nhau bằng traceId.

🇬🇧 English:

Metrics are cheap aggregated numbers over time, used for dashboards and alerting: they tell you something is wrong and how bad it is. Traces follow one request across services and tell you which hop is slow or failing. Logs are detailed events that tell you exactly what happened. Many teams add continuous profiling as a fourth signal to see which code burns CPU or memory. My flow is: metrics alert, traces narrow it down, logs and profiles explain the cause, all linked by the trace ID.

Q2. Structured logging là gì, vì sao nên dùng?

🇻🇳 Tiếng Việt:

Là ghi log dạng key-value, thường là JSON, thay vì câu text tự do. Hệ thống log parse được nên em lọc được theo orderId, traceId, level, service mà không phải viết regex. Trên Kubernetes em log JSON ra stdout, agent trên node gom về Loki hoặc Elasticsearch. Spring Boot 3.4 hỗ trợ sẵn qua logging.structured.format.console.

🇬🇧 English:

It means logging key-value data, usually JSON, instead of free text. The log backend can parse it, so I can filter by orderId, traceId, level or service without regex. On Kubernetes I write JSON to stdout and a node agent ships it to Loki or Elasticsearch. Spring Boot 3.4 supports it out of the box via logging.structured.format.console.

Q3. Làm sao gom log của một request đi qua nhiều service?

🇻🇳 Tiếng Việt:

Dùng distributed tracing: Micrometer Tracing hoặc OpenTelemetry sinh traceId ở service đầu tiên, truyền qua header W3C traceparent, và đặt traceId vào MDC để mọi dòng log đều có nó. Khi điều tra em tìm theo traceId trong hệ thống log là ra toàn bộ log của request trên mọi service, và từ trace trong Tempo nhảy thẳng sang log.

🇬🇧 English:

Distributed tracing: Micrometer Tracing or OpenTelemetry creates a trace ID at the first service, propagates it in the W3C traceparent header, and puts it in the MDC so every log line carries it. When investigating I search the log backend by trace ID to get every log line for that request across all services, and I can jump from a trace in Tempo straight to its logs.

Q4. Cardinality là gì, tại sao quan trọng?

🇻🇳 Tiếng Việt:

Là số giá trị khác nhau của một field. Với metric, mỗi tổ hợp label là một time series riêng, nên label như userId hay orderId làm số series tăng theo số người dùng và làm Prometheus hết memory. Giá trị cardinality cao thì để trong log hoặc trace attribute, còn label metric chỉ dùng giá trị có giới hạn như method, status, route template.

🇬🇧 English:

It's the number of distinct values a field can take. For metrics, every label combination is a separate time series, so labels like userId or orderId grow with your user base and can run Prometheus out of memory. High-cardinality values belong in logs or span attributes; metric labels should be bounded values like method, status, or route template.

Cách nhớ ngắn gọn


Tự kiểm tra

Muốn alert "tỷ lệ lỗi checkout > 2%", dùng tín hiệu nào?

Metric (counter request theo status). Log và trace không hợp để alert.

Vì sao không ghi log ra file trong container?

Pod restart là mất file, khó gom. Ghi stdout để runtime và agent trên node thu gom.

orderId nên đặt ở log, label metric hay span attribute?

Log (MDC) và span attribute. Không đặt làm label metric vì cardinality không giới hạn.

Thẻ khách bị ngân hàng từ chối: log level nào?

INFO hoặc WARN; không phải ERROR vì hệ thống không hỏng.

Ai đặt traceId vào MDC?

Micrometer Tracing (hoặc OTel agent) tự đặt khi có span hiện tại.