Grafana Tempo: Cài đặt Distributed Tracing và tích hợp Loki + Prometheus trên một Grafana

Monitoring tutorial - IT technology blog
Monitoring tutorial - IT technology blog

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à:

  1. Nhận alert latency tăng từ Alertmanager
  2. Grafana Explore → Tempo, filter { duration > 2s } để thấy ngay request nào chậm
  3. Click vào trace, xem span nào chiếm nhiều thời gian nhất
  4. Click “Logs for this span” → Loki tự động filter logs đúng khoảng thời gian của span đó
  5. 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: local bằ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.

Share: