Khi logs và metrics không đủ để debug production
Mình đã từng mất 3 tiếng debug một request chậm trên hệ thống microservices — đủ logs từ Loki, đủ metrics từ Prometheus, nhưng vẫn không tìm ra service nào là bottleneck. Logs chỉ cho thấy “có gì đó sai”, metrics cho thấy “latency tăng”. Nhưng không có gì nối chúng lại: request đi qua service nào? Mất bao lâu ở từng bước? Bị lỗi ở đâu?
Đó là lúc mình bắt đầu tìm hiểu distributed tracing và setup Grafana Tempo. Bài này đi thẳng vào cài đặt: Tempo từ đầu, kết nối với Loki và Prometheus, rồi cấu hình để từ một trace bạn nhảy thẳng sang logs liên quan và metrics của service đó — tất cả ngay trong một Grafana.
Distributed Tracing là gì và tại sao chọn Tempo
Cách đơn giản nhất để hình dung: distributed tracing như việc gắn GPS vào từng request. Khi request đi qua service-auth, service-order, service-payment — mỗi bước tạo ra một “span”. Tất cả span ghép lại thành một “trace” duy nhất, cho thấy toàn bộ hành trình: đi đâu, mắc kẹt bao lâu, fail ở đâu.
Grafana Tempo lưu trace thẳng vào object storage (local, S3, GCS) mà không cần index — nghĩa là không phải maintain Elasticsearch hay Cassandra như khi dùng Jaeger. Điểm quyết định với mình: Tempo được build để chạy cùng Loki và Prometheus trong một Grafana, nên jump từ trace sang logs sang metrics là native, không cần cài thêm plugin hay viết link thủ công.
Cài đặt Tempo với Docker Compose
Chuẩn bị cấu trúc thư mục
mkdir -p tempo-stack/{tempo,grafana/provisioning/datasources}
cd tempo-stack
File cấu hình Tempo
Tạo file tempo/tempo.yaml:
stream_over_http_enabled: true
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
jaeger:
protocols:
thrift_http:
endpoint: 0.0.0.0:14268
ingester:
max_block_duration: 5m
compactor:
compaction:
block_retention: 24h # tăng lên 168h cho production
storage:
trace:
backend: local
local:
path: /tmp/tempo/blocks
wal:
path: /tmp/tempo/wal
metrics_generator:
registry:
external_labels:
source: tempo
cluster: docker-compose
storage:
path: /tmp/tempo/generator/wal
remote_write:
- url: http://prometheus:9090/api/v1/write
send_exemplars: true
overrides:
defaults:
metrics_generator:
processors: [service-graphs, span-metrics]
generate_native_histograms: both
Docker Compose
Tạo docker-compose.yml:
version: "3.8"
services:
tempo:
image: grafana/tempo:latest
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./tempo/tempo.yaml:/etc/tempo.yaml
- tempo-data:/tmp/tempo
ports:
- "3200:3200" # Tempo HTTP API
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "14268:14268" # Jaeger HTTP
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--enable-feature=remote-write-receiver" # bắt buộc để Tempo push metrics
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
ports:
- "9090:9090"
restart: unless-stopped
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
restart: unless-stopped
grafana:
image: grafana/grafana:latest
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
- grafana-data:/var/lib/grafana
ports:
- "3000:3000"
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
restart: unless-stopped
volumes:
tempo-data:
prometheus-data:
grafana-data:
Lưu ý quan trọng: Prometheus phải bật flag --enable-feature=remote-write-receiver để nhận metrics từ Tempo metrics generator. Mình đã quên cái này lần đầu và mất khá lâu mới tìm ra tại sao service-graph không hiện gì trong Grafana.
Cấu hình Datasources để Correlation hoạt động
Đây là bước nhiều người bỏ qua sau khi cài xong Tempo — rồi thắc mắc tại sao click vào trace lại không nhảy được sang logs. Tạo file grafana/provisioning/datasources/datasources.yaml:
apiVersion: 1
datasources:
- name: Tempo
type: tempo
uid: tempo
url: http://tempo:3200
jsonData:
httpMethod: GET
tracesToLogsV2:
datasourceUid: loki
spanStartTimeShift: "-1m"
spanEndTimeShift: "1m"
filterByTraceID: true
filterBySpanID: false
tracesToMetrics:
datasourceUid: prometheus
spanStartTimeShift: "-1m"
spanEndTimeShift: "1m"
tags:
- key: service.name
value: service
queries:
- name: Request Rate
query: rate(traces_spanmetrics_calls_total{$$__tags}[5m])
- name: Error Rate
query: rate(traces_spanmetrics_calls_total{$$__tags,status_code="STATUS_CODE_ERROR"}[5m])
- name: Duration P95
query: histogram_quantile(0.95, sum(rate(traces_spanmetrics_duration_milliseconds_bucket{$$__tags}[5m])) by (le))
serviceMap:
datasourceUid: prometheus
nodeGraph:
enabled: true
lokiSearch:
datasourceUid: loki
- name: Loki
type: loki
uid: loki
url: http://loki:3100
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: '"trace_id":"(\w+)"'
name: TraceID
url: "$${__value.raw}"
urlDisplayLabel: "View Trace in Tempo"
- name: Prometheus
type: prometheus
uid: prometheus
url: http://prometheus:9090
isDefault: true
Config này làm ba việc chính:
- tracesToLogsV2: Từ một span trong Tempo, tự động query Loki lấy logs cùng khoảng thời gian của span đó — không cần nhớ timestamp hay lọc thủ công
- tracesToMetrics: Từ trace nhảy sang Prometheus xem request rate, error rate, P95 latency của service
- Loki derivedFields: Từ log entry có chứa
trace_id, tạo link trực tiếp sang trace tương ứng trong Tempo
Gửi trace từ ứng dụng vào Tempo
Tempo nhận trace qua nhiều protocol — Jaeger, Zipkin, OTLP. Dùng OTLP là lựa chọn tốt nhất vì đây là chuẩn mở và hầu hết framework hiện đại đều hỗ trợ sẵn. Ví dụ với Python:
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc opentelemetry-instrumentation-fastapi
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
resource = Resource.create({
"service.name": "my-api",
"service.version": "1.0.0",
})
provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter(
endpoint="http://localhost:4317",
insecure=True,
)
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# Dùng trong code:
with tracer.start_as_current_span("process-order") as span:
span.set_attribute("order.id", "12345")
span.set_attribute("user.id", "user-abc")
# ... business logic ở đây
Để correlation Traces → Logs hoạt động, logs của ứng dụng phải chứa trace_id. Thêm filter này vào logger:
import logging
from opentelemetry import trace
class TraceIDFilter(logging.Filter):
def filter(self, record):
span = trace.get_current_span()
ctx = span.get_span_context()
record.trace_id = format(ctx.trace_id, "032x") if ctx.is_valid else ""
return True
# JSON format để Loki parse được và Grafana extract trace_id
handler = logging.StreamHandler()
handler.addFilter(TraceIDFilter())
handler.setFormatter(logging.Formatter(
'{"time": "%(asctime)s", "level": "%(levelname)s", "msg": "%(message)s", "trace_id": "%(trace_id)s"}'
))
logging.getLogger().addHandler(handler)
Kiểm tra và sử dụng trong thực tế
Khởi động stack và verify
docker compose up -d
# Kiểm tra Tempo đang chạy
curl http://localhost:3200/status
# Xem traces đã nhận được (sau khi app gửi)
curl "http://localhost:3200/api/search?limit=5"
Query bằng TraceQL
TraceQL là query language riêng của Tempo — cú pháp gần giống PromQL nhưng chạy trên traces thay vì metrics. Vào Grafana → Explore → chọn datasource Tempo để thử:
# Tìm traces của service cụ thể
{ .service.name = "my-api" }
# Tìm traces có lỗi
{ status = error }
# Tìm traces chậm hơn 1 giây
{ duration > 1s }
# Kết hợp: request lỗi của service my-api, chậm hơn 500ms
{ .service.name = "my-api" && status = error && duration > 500ms }
Workflow debug thực tế
Trước đây khi có alert latency tăng, mình thường mở 3-4 tab song song: Prometheus để xem metric, Loki để grep logs, rồi ngồi ghép timestamp để tìm ra request nào bị ảnh hưởng. Từ khi setup xong stack này, quy trình còn lại là:
- Nhận alert latency tăng từ Alertmanager
- Grafana Explore → Tempo, filter
{ duration > 2s }để thấy ngay request nào chậm - Click vào trace, xem span nào chiếm nhiều thời gian nhất
- Click “Logs for this span” → Loki tự động filter logs đúng khoảng thời gian của span đó
- Click “Metrics” → Prometheus hiện request rate và resource usage của service ngay lúc đó
Investigation giờ mất 5 phút thay vì 30 phút, và team bắt đầu tin tưởng vào alert hơn thay vì ignore chúng.
Một số lưu ý khi chạy production
- Sampling: Đừng gửi 100% traces. Head-based sampling 10–20% là đủ cho hầu hết hệ thống. Tail-based sampling nếu muốn giữ lại 100% traces lỗi.
- Object storage: Thay
backend: localbằng S3 hoặc GCS để không mất data khi container restart - Retention: 24h cho dev, 7–30 ngày cho production tùy compliance requirement
# Production: dùng S3 thay local
storage:
trace:
backend: s3
s3:
bucket: my-tempo-traces
endpoint: s3.amazonaws.com
region: ap-northeast-1
access_key: ${S3_ACCESS_KEY}
secret_key: ${S3_SECRET_KEY}
Với setup này, lần tới khi có alert latency tăng, bạn sẽ không phải ngồi mò 3 tiếng như mình nữa. Từ một trace ID, jump sang logs và metrics liên quan chỉ mất vài giây — toàn bộ context của incident nằm ngay trong một màn hình Grafana.

