Observability cho Backend — OTel, Metrics & Tracing thực chiến
💡 Vấn đề
Hồi mới lên production, mình debug kiểu "thấy 500 là mở log ra tìm". Nhưng log chỉ trả lời được câu chuyện gì đã xảy ra, còn tại sao chậm, request nào đang kẹt thì mù tịt. Lần đó service latency tăng vọt lúc 2h sáng, log thì sạch trơn — vì bottleneck nằm ở chỗ không ai ngờ: connection pool Postgres bị cạn do một query chậm. Muốn biết chuyện đó, phải có metrics và tracing, không chỉ log.
Đó là lúc mình nhận ra sự khác biệt giữa monitoring và observability.
🔍 Monitoring vs Observability
- Monitoring: hỏi "hệ thống có đang hoạt động?" — alert khi CPU cao, disk đầy, service down.
- Observability: hỏi "vì sao nó không hoạt động?" — tái dựng hành trình của request qua nhiều service để tìm root cause.
Ba trụ cột: Logs (chuyện gì xảy ra), Metrics (con số đo được), Traces (request đi qua đâu). OpenTelemetry (OTel) là chuẩn chung giúp gom cả ba về một chỗ — code một lần, đổi backend (Jaeger, Prometheus, Grafana) thoải mái, hết vendor lock-in.
🧪 Code / Demo
Khởi tạo OTel SDK trong Go, xuất trace qua OTLP:
func initTracer() (*sdktrace.TracerProvider, error) {
exporter, err := otlptracehttp.New(ctx,
otlptracehttp.WithEndpoint("otel-collector:4318"),
otlptracehttp.WithInsecure())
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("order-service"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
Span — đơn vị của trace, mỗi thao tác quan trọng một span:
func handleOrder(ctx context.Context, orderID string) {
ctx, span := tracer.Start(ctx, "handleOrder")
defer span.End()
if err := charge(ctx, orderID); err != nil { // span con tự nối vào
span.RecordError(err)
span.SetAttributes(attribute.String("order.id", orderID))
span.SetStatus(codes.Error, err.Error())
return
}
}
Metrics — đếm request, đo latency bằng histogram:
reqCounter := metric.Int64Counter("api.requests.total",
metric.WithDescription("Total requests"))
latency := metric.Float64Histogram("api.latency",
metric.WithUnit("ms"))
reqCounter.Add(ctx, 1,
metric.WithAttributes(attribute.String("route", r.URL.Path)))
latency.Record(ctx, float64(elapsed.Milliseconds()))
Chỗ hay quên nhất: propagate context. Trace chỉ nối được khi bạn truyền ctx xuyên suốt — cắt ở đâu là trace đứt ở đó. Gọi DB, gọi API khác, sinh goroutine đều phải mang theo ctx.
📝 Kết luận
Kinh nghiệm thực tế: đừng đo mọi thứ ngay từ đầu. Bắt đầu với 3-4 metrics lõi (latency, error rate, saturation theo ý tưởng RED method) + tracing cho các endpoint quan trọng là đủ cứu mạng. OTel SDK gần như miễn phí về performance nhờ sampling, và collector cho phép đổi cả hệ thống quan sát mà không đụng vào code. Observability không phải là "thêm dashboard cho đẹp" — nó là khả năng hỏi hệ thống vì sao và nhận câu trả lời trong vài phút, không phải vài ngày.