Khi Prometheus không đủ để chẩn đoán lỗi
Prometheus và Grafana giúp mình biết rằng có vấn đề — latency tăng đột biến, error rate lên cao. Nhưng khi hệ thống có 8–10 service giao tiếp với nhau, câu hỏi khó hơn là: vấn đề xảy ra ở service nào, ở bước nào trong chuỗi request?
Mình từng mất gần 3 tiếng debug một request chậm 4 giây. Log của từng service thì ổn, metrics không có gì bất thường. Cuối cùng mới phát hiện ra là một service gọi database với N+1 query — nhưng phải grep thủ công qua 5 service mới tìm ra. Đó là lúc mình bắt đầu tìm hiểu distributed tracing.
OpenTelemetry + Jaeger giải quyết đúng bài toán này. Thay vì grep log thủ công, bạn thấy toàn bộ hành trình của request qua các service dưới dạng timeline — mỗi bước được đo millisecond, hiển thị rõ bước nào chậm và chậm bao nhiêu.
Cài đặt Jaeger và OpenTelemetry Collector
Dùng Docker Compose để dựng stack. Kiến trúc ở đây là: ứng dụng gửi traces đến OTel Collector, Collector forward sang Jaeger. Thêm một tầng Collector nghe có vẻ thừa, nhưng nó giúp batch spans trước khi gửi, giảm tải cho Jaeger đáng kể — đặc biệt khi traffic cao.
Tạo file docker-compose.yml
version: '3.8'
services:
jaeger:
image: jaegertracing/all-in-one:1.57
ports:
- "16686:16686" # Jaeger UI
- "14250:14250" # gRPC nhận traces từ Collector
environment:
- COLLECTOR_OTLP_ENABLED=true
otel-collector:
image: otel/opentelemetry-collector-contrib:0.99.0
volumes:
- ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
depends_on:
- jaeger
Cấu hình OpenTelemetry Collector
Tạo file otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
exporters:
otlp/jaeger:
endpoint: jaeger:14250
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger]
Khởi động stack:
docker compose up -d
# Kiểm tra Jaeger UI tại http://localhost:16686
Instrument ứng dụng Python với OpenTelemetry
Ví dụ dưới đây dùng một Flask API đóng vai trò order-service — nhận request từ client, gọi tiếp sang inventory-service và pricing-service. Cài thư viện cần thiết:
pip install opentelemetry-sdk \
opentelemetry-exporter-otlp-proto-grpc \
opentelemetry-instrumentation-flask \
opentelemetry-instrumentation-requests
Khởi tạo tracer trong ứng dụng
# tracing.py
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
def setup_tracing(service_name: str):
resource = Resource.create({"service.name": service_name})
provider = TracerProvider(resource=resource)
exporter = OTLPSpanExporter(
endpoint="http://localhost:4317",
insecure=True
)
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
return trace.get_tracer(service_name)
Tích hợp vào Flask app
# app.py
from flask import Flask, jsonify
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from tracing import setup_tracing
import requests
app = Flask(__name__)
tracer = setup_tracing("order-service")
# Auto-instrument Flask và requests library
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()
@app.route("/order/<int:order_id>")
def get_order(order_id):
with tracer.start_as_current_span("fetch-order-details") as span:
span.set_attribute("order.id", order_id)
# Span con: gọi inventory service
with tracer.start_as_current_span("check-inventory"):
inventory = requests.get(
f"http://inventory-service/stock/{order_id}"
).json()
# Span con: gọi pricing service
with tracer.start_as_current_span("calculate-price"):
price = requests.get(
f"http://pricing-service/price/{order_id}"
).json()
span.set_attribute("order.total", price.get("total", 0))
return jsonify({"order": order_id, "inventory": inventory, "price": price})
if __name__ == "__main__":
app.run(port=5000)
Điểm mấu chốt: khi order-service gọi sang service khác qua HTTP, RequestsInstrumentor tự động nhét trace ID và span ID vào header (traceparent). Service nhận request đọc header đó và nối trace vào đúng chuỗi. Vì vậy tất cả service trong hệ thống đều phải được instrument — bỏ sót một service là chuỗi trace bị đứt ở đó.
Đọc traces trên Jaeger UI
Gửi thử 10–20 request vào API, sau đó mở http://localhost:16686.
Tìm trace của một request
- Chọn Service:
order-service - Click Find Traces
- Chọn một trace có duration cao để phân tích
Jaeger hiển thị timeline dạng waterfall. Nhìn vào đó bạn thấy ngay span nào ngốn nhiều thời gian nhất — ví dụ nếu check-inventory mất 800ms trong tổng 1 giây, đó là chỗ cần xem trước. Không cần đoán, không cần grep.
Thêm attribute để debug dễ hơn
Custom attribute giúp gắn context business logic vào span — cực kỳ hữu ích khi cần tìm trace của một user hoặc order cụ thể:
with tracer.start_as_current_span("query-database") as span:
span.set_attribute("db.query", sql_query)
span.set_attribute("db.rows_returned", len(results))
span.set_attribute("user.id", user_id)
# nếu có lỗi:
span.record_exception(exception)
span.set_status(trace.Status(trace.StatusCode.ERROR))
Lọc trace theo điều kiện
Jaeger hỗ trợ tìm trace theo tag. Ví dụ tìm tất cả request của user 12345, hoặc lọc ra những trace có lỗi:
# Trong Jaeger UI, phần Tags:
user.id=12345
# Hoặc tìm các trace có lỗi:
error=true
Bài học từ thực tế
Lần đầu setup xong, mình bật trace sampling 100% — tức là trace mọi request. Hoạt động tốt lúc test. Lên production với ~500 req/s là Jaeger UI bắt đầu lag, Collector drop spans. Giới hạn sampling rate ngay từ đầu:
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
# Chỉ trace 10% request
sampler = TraceIdRatioBased(0.1)
provider = TracerProvider(resource=resource, sampler=sampler)
Chuyện alert cũng tương tự. Mình từng set cảnh báo cho mọi span chậm hơn 500ms — kết quả là Telegram nhận vài chục alert mỗi giờ, đến mức mình tắt notification luôn. Phải tune lại nhiều lần mới ra ngưỡng có nghĩa. Cách tốt hơn: alert dựa trên end-to-end latency của toàn bộ trace, không phải từng span lẻ. Một span chậm 600ms chưa chắc là vấn đề nếu tổng request vẫn dưới 1 giây.
Còn một thứ nhỏ nhưng tiết kiệm nhiều thời gian: đặt tên span theo pattern verb-noun, ví dụ fetch-user-profile, insert-order-db, send-notification-email. Khi trace có 50 span, tên rõ ràng giúp đọc waterfall chart nhanh hơn nhiều so với những cái tên chung chung như process hay handler.
Tích hợp với Prometheus (tuỳ chọn)
OTel Collector có thể export cả metrics sang Prometheus — một pipeline duy nhất cho cả traces lẫn metrics:
# Thêm vào otel-collector-config.yaml
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
Workflow mình dùng khi có incident: thấy latency spike trên Grafana → xem timestamp xảy ra lúc mấy giờ → sang Jaeger lọc traces trong khoảng đó → tìm trace có duration bất thường → drill down vào từng span. Từ alert đến tìm ra root cause thường mất dưới 5 phút, thay vì 3 tiếng grep log như trước.
