Module 22 Advanced 5 labs

Monitoring, Logging, Metrics và Tracing

Xây dựng observability toàn diện với Prometheus (metrics), Grafana (dashboard), Loki (logs), OpenTelemetry (traces) và Alertmanager — biến hệ thống từ "hộp đen" thành "hộp thủy tinh" để phát hiện lỗi trước khi user báo cáo.

Công cụ thực hành Prometheus, Grafana, Loki, Promtail, OpenTelemetry Collector, Alertmanager, Docker Compose
Nền tảng Linux/WSL2, Docker Desktop, Kubernetes (optional)
Thời điểm phát hành 23/05/2026
Ngày biên soạn 23/05/2026
Người biên soạn Trần Văn Hòa — Microsoft Certified Trainer (MCT)

Mục tiêu học tập

1. Lý thuyết cốt lõi

1.1. Observability vs. Monitoring

Monitoring trả lời câu hỏi "hệ thống có đang hoạt động không?" — check predefined thresholds. Observability trả lời "tại sao hệ thống hoạt động theo cách đó?" — khả năng suy luận về trạng thái internal từ external outputs. Google SRE Book (Ch. 6 — Monitoring Distributed Systems) định nghĩa: "Monitoring is collecting, processing, aggregating, and displaying real-time quantitative data about a system." Một hệ thống observable cho phép bạn debug các vấn đề chưa từng xảy ra trước đây — không chỉ alert khi biết trước lỗi gì.

Ba trụ cột (Three Pillars of Observability): Metrics (số liệu tổng hợp theo thời gian), Logs (sự kiện rời rạc có context), Traces (luồng request qua nhiều service). Chúng bổ sung nhau: metric báo "có vấn đề", log cho biết "chi tiết lỗi", trace chỉ ra "lỗi xảy ra ở đâu trong chuỗi microservice".

1.2. The Four Golden Signals (Google SRE)

Google SRE Book Ch. 6 — 4 tín hiệu vàng cần monitor mọi service:

  • Latency — thời gian xử lý request. Phân biệt latency của successful requests vs. failed requests. Theo dõi percentile (p50, p95, p99), không chỉ average.
  • Traffic — lượng yêu cầu hệ thống đang xử lý (requests/sec, queries/sec, transactions/sec).
  • Errors — tỉ lệ request thất bại (5xx, timeout, business logic error). Phân loại explicit (HTTP 500) vs. implicit (HTTP 200 nhưng nội dung sai).
  • Saturation — mức độ "đầy" của service: CPU%, memory%, disk I/O queue, connection pool. Saturation cao dự báo latency tăng trước khi service sập.

1.3. RED Method và USE Method

Method Dùng cho Metrics
RED Services / microservices (request-driven) Rate · Errors · Duration
USE Infrastructure resources (CPU, disk, network) Utilization · Saturation · Errors

1.4. Prometheus và Pull Model

Prometheus dùng pull model: server chủ động scrape /metrics endpoint từ target mỗi scrape_interval (mặc định 15s). Ngược với push model (StatsD, InfluxDB Telegraf), pull model cho phép: (1) phát hiện target chết khi không scrape được, (2) kiểm soát tải tập trung tại Prometheus. Dữ liệu lưu dạng time-series: mỗi series là chuỗi (timestamp, value) kèm labels. PromQL là ngôn ngữ query mạnh nhưng có learning curve — cần nắm rate(), sum() by(), histogram_quantile().

1.5. Distributed Tracing và OpenTelemetry

Trong kiến trúc microservice, một request HTTP có thể đi qua 10–20 service. Khi latency tăng hoặc lỗi xảy ra, cần biết đúng service nào gây ra. Distributed tracing gắn trace ID duy nhất cho mỗi request, lan truyền qua header (W3C Trace Context: traceparent). Mỗi service tạo span — đơn vị công việc có start time, duration, attributes, events. OpenTelemetry (OTel) là standard thống nhất: SDK → Collector → backend (Jaeger, Tempo, Zipkin). Không vendor lock-in.

1.6. Alert Design — Symptom-based vs. Cause-based

Google SRE Book phân biệt: "Alert on symptoms, not causes." Alert error_rate > 1% (symptom) tốt hơn alert disk_fill_rate > X (cause) vì symptom trực tiếp ảnh hưởng user. Tránh alert noise: mỗi alert phải actionable — nếu oncall không làm gì khác ngoài acknowledge thì alert đó nên xóa. Mastering SRE (Hoeppner) nhấn mạnh: alert fatigue làm oncall bỏ qua alert thật, nguy hiểm hơn không có alert.

2. Thực hành (Labs)

LAB-106

Prometheus Scrape App Metrics

Prometheus · Docker Compose · PromQL

🎯 Mục tiêu: Chạy Prometheus scrape metrics từ một app Python (Flask + prometheus-client), viết PromQL query tính request rate và error rate.

🧰 Công cụ / nền tảng: Docker Compose, Python 3, prometheus-client library.

📦 Chuẩn bị: Docker Desktop đang chạy; tạo thư mục lab-106.

▶️ Các bước:

mkdir lab-106 && cd lab-106

# Bước 1: Tạo app Python expose /metrics
cat > app.py <<'PYEOF'
from flask import Flask, Response
from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
import random, time

app = Flask(__name__)

REQUEST_COUNT = Counter('app_requests_total',
    'Total requests', ['method', 'endpoint', 'status'])
REQUEST_LATENCY = Histogram('app_request_latency_seconds',
    'Request latency', ['endpoint'],
    buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5])

@app.route('/')
def index():
    start = time.time()
    # Giả lập 5% error rate
    status = '500' if random.random() < 0.05 else '200'
    time.sleep(random.uniform(0.01, 0.3))
    REQUEST_COUNT.labels('GET', '/', status).inc()
    REQUEST_LATENCY.labels('/').observe(time.time() - start)
    if status == '500':
        return 'Error', 500
    return 'Hello Observability!', 200

@app.route('/metrics')
def metrics():
    return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
PYEOF

# Bước 2: Tạo Dockerfile cho app
cat > Dockerfile <<'EOF'
FROM python:3.11-slim
RUN pip install flask prometheus-client
COPY app.py .
CMD ["python","app.py"]
EOF

# Bước 3: Tạo cấu hình Prometheus
cat > prometheus.yml <<'EOF'
global:
  scrape_interval: 10s
  evaluation_interval: 10s

scrape_configs:
  - job_name: 'flask-app'
    static_configs:
      - targets: ['flask-app:8000']
    metrics_path: /metrics
EOF

# Bước 4: Tạo docker-compose.yml
cat > docker-compose.yml <<'EOF'
version: '3.8'
services:
  flask-app:
    build: .
    ports:
      - "8000:8000"
    container_name: flask-app

  prometheus:
    image: prom/prometheus:v2.51.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.enable-lifecycle'
    container_name: prometheus
EOF

# Bước 5: Khởi động
docker compose up -d --build
docker compose ps

# Bước 6: Tạo traffic để có data
for i in {1..50}; do curl -s http://localhost:8000/ > /dev/null; done
# Hoặc dùng load generator liên tục
watch -n 0.5 'curl -s http://localhost:8000/ > /dev/null' &

# Bước 7: Mở Prometheus UI http://localhost:9090
# Kiểm tra target: Status → Targets
# flask-app phải ở trạng thái "UP"

# Bước 8: Chạy PromQL queries trong UI

# --- Query 1: Request rate trong 1 phút ---
# rate(app_requests_total[1m])

# --- Query 2: Error rate (%) ---
# sum(rate(app_requests_total{status="500"}[1m])) /
# sum(rate(app_requests_total[1m])) * 100

# --- Query 3: p95 latency ---
# histogram_quantile(0.95, sum(rate(app_request_latency_seconds_bucket[5m])) by (le))

# --- Query 4: Total requests by status ---
# sum by(status) (rate(app_requests_total[5m]))

✅ Kết quả mong đợi: Prometheus UI tại http://localhost:9090 hiển thị target flask-app trạng thái UP; query rate(app_requests_total[1m]) trả về time-series có giá trị; error rate dao động quanh 5% (đúng với code giả lập); p95 latency < 350ms.

🧹 Cleanup:

kill %1   # dừng watch loop nếu có
docker compose down -v
cd .. && rm -rf lab-106
LAB-107

Grafana Dashboard — RED Method

Grafana · Prometheus datasource · Dashboard JSON

🎯 Mục tiêu: Xây Grafana dashboard theo RED method (Rate, Errors, Duration) với 4 panel: request rate, error rate, p50/p95/p99 latency, request count by status.

🧰 Công cụ / nền tảng: Grafana OSS, Prometheus datasource (từ LAB-106). Extend docker-compose.yml.

📦 Chuẩn bị: LAB-106 đang chạy. Chạy lệnh trong thư mục lab-106.

▶️ Các bước:

# Bước 1: Thêm Grafana vào docker-compose.yml
cat >> docker-compose.yml <<'EOF'

  grafana:
    image: grafana/grafana:10.4.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana-provisioning:/etc/grafana/provisioning
    container_name: grafana
    depends_on:
      - prometheus

volumes:
  grafana-data:
EOF

# Bước 2: Tạo provisioning để auto-add Prometheus datasource
mkdir -p grafana-provisioning/datasources
cat > grafana-provisioning/datasources/prometheus.yml <<'EOF'
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090
    isDefault: true
    access: proxy
EOF

# Bước 3: Tạo dashboard JSON tự động
mkdir -p grafana-provisioning/dashboards
cat > grafana-provisioning/dashboards/dashboard.yml <<'EOF'
apiVersion: 1
providers:
  - name: 'Default'
    folder: ''
    type: file
    options:
      path: /etc/grafana/provisioning/dashboards
EOF

cat > grafana-provisioning/dashboards/red-dashboard.json <<'JSONEOF'
{
  "title": "Flask App — RED Method",
  "uid": "flask-red",
  "time": {"from":"now-15m","to":"now"},
  "refresh": "10s",
  "panels": [
    {
      "id": 1,
      "title": "Request Rate (req/sec)",
      "type": "timeseries",
      "gridPos": {"x":0,"y":0,"w":12,"h":8},
      "targets": [{
        "expr": "sum(rate(app_requests_total[1m]))",
        "legendFormat": "req/sec"
      }]
    },
    {
      "id": 2,
      "title": "Error Rate (%)",
      "type": "timeseries",
      "gridPos": {"x":12,"y":0,"w":12,"h":8},
      "targets": [{
        "expr": "sum(rate(app_requests_total{status='500'}[1m])) / sum(rate(app_requests_total[1m])) * 100",
        "legendFormat": "error %"
      }],
      "fieldConfig": {"defaults":{"color":{"fixedColor":"red","mode":"fixed"}}}
    },
    {
      "id": 3,
      "title": "Latency Percentiles (sec)",
      "type": "timeseries",
      "gridPos": {"x":0,"y":8,"w":12,"h":8},
      "targets": [
        {
          "expr": "histogram_quantile(0.50, sum(rate(app_request_latency_seconds_bucket[5m])) by (le))",
          "legendFormat": "p50"
        },
        {
          "expr": "histogram_quantile(0.95, sum(rate(app_request_latency_seconds_bucket[5m])) by (le))",
          "legendFormat": "p95"
        },
        {
          "expr": "histogram_quantile(0.99, sum(rate(app_request_latency_seconds_bucket[5m])) by (le))",
          "legendFormat": "p99"
        }
      ]
    },
    {
      "id": 4,
      "title": "Requests by Status",
      "type": "bargauge",
      "gridPos": {"x":12,"y":8,"w":12,"h":8},
      "targets": [{
        "expr": "sum by(status) (increase(app_requests_total[5m]))",
        "legendFormat": "{{status}}"
      }]
    }
  ]
}
JSONEOF

# Bước 4: Restart để load grafana
docker compose up -d

# Bước 5: Tiếp tục tạo traffic
for i in {1..100}; do curl -s http://localhost:8000/ > /dev/null; done

# Bước 6: Truy cập http://localhost:3000
# Login: admin / admin
# Dashboards → Flask App — RED Method

# Bước 7: Tùy chỉnh panel thủ công nếu cần
# Panel → Edit → thay đổi PromQL, color thresholds, units
# Units → Time → seconds; Rate → requests/sec

# Bước 8: Export dashboard để lưu vào Git
# Dashboard → Share → Export → Save to file
# Lưu thành grafana-provisioning/dashboards/red-dashboard.json (overwrite)

✅ Kết quả mong đợi: Grafana dashboard tại http://localhost:3000 hiển thị 4 panel live — request rate ~2 req/sec, error rate ~5%, p95 latency < 0.35s. Dashboard tự load sau restart (provisioning).

🧹 Cleanup: Giữ stack chạy cho LAB-108 và LAB-110. Hoặc docker compose down -v.

LAB-108

Log Aggregation với Loki + Promtail

Loki · Promtail · Grafana LogQL

🎯 Mục tiêu: Thu thập log từ tất cả container Docker bằng Promtail, đẩy vào Loki, query bằng LogQL trên Grafana — xem log lỗi của flask-app.

🧰 Công cụ / nền tảng: Loki (log database), Promtail (log shipper), Grafana (đã có từ LAB-107).

📦 Chuẩn bị: Stack từ LAB-107 đang chạy trong thư mục lab-106.

▶️ Các bước:

# Bước 1: Tạo cấu hình Loki
cat > loki-config.yml <<'EOF'
auth_enabled: false

server:
  http_listen_port: 3100

ingester:
  wal:
    enabled: false
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1

schema_config:
  configs:
    - from: 2024-01-01
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  boltdb_shipper:
    active_index_directory: /loki/index
    cache_location: /loki/cache
  filesystem:
    directory: /loki/chunks

limits_config:
  reject_old_samples: false
EOF

# Bước 2: Tạo cấu hình Promtail — đọc log Docker container
cat > promtail-config.yml <<'EOF'
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: docker-containers
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        target_label: container
      - source_labels: ['__meta_docker_container_log_stream']
        target_label: stream
      - source_labels: ['__meta_docker_container_image']
        target_label: image
EOF

# Bước 3: Thêm Loki và Promtail vào docker-compose.yml
cat >> docker-compose.yml <<'EOF'

  loki:
    image: grafana/loki:2.9.4
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yml:/etc/loki/config.yml
      - loki-data:/loki
    command: -config.file=/etc/loki/config.yml
    container_name: loki

  promtail:
    image: grafana/promtail:2.9.4
    volumes:
      - ./promtail-config.yml:/etc/promtail/config.yml
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/log:/var/log:ro
    command: -config.file=/etc/promtail/config.yml
    container_name: promtail
    depends_on:
      - loki
EOF

# Thêm loki-data vào volumes section
# (edit docker-compose.yml — thêm "loki-data:" vào volumes:)
sed -i 's/volumes:/volumes:\n  loki-data:/' docker-compose.yml

# Bước 4: Thêm Loki datasource vào Grafana
cat > grafana-provisioning/datasources/loki.yml <<'EOF'
apiVersion: 1
datasources:
  - name: Loki
    type: loki
    url: http://loki:3100
    access: proxy
EOF

# Bước 5: Restart toàn bộ stack
docker compose up -d
sleep 5
docker compose ps

# Bước 6: Sinh traffic để có log
for i in {1..30}; do
  curl -s http://localhost:8000/ > /dev/null
  sleep 0.2
done

# Bước 7: Query log trong Grafana
# Explore → Datasource: Loki

# --- LogQL Query 1: Xem tất cả log của flask-app ---
# {container="flask-app"}

# --- LogQL Query 2: Chỉ log lỗi (HTTP 500) ---
# {container="flask-app"} |= "500"

# --- LogQL Query 3: Parse structured log + count lỗi ---
# count_over_time({container="flask-app"} |= "500" [5m])

# --- LogQL Query 4: Tất cả containers theo job ---
# {job="docker-containers"} | logfmt | line_format "{{.container}}: {{.msg}}"

# Bước 8: Tạo log panel trong dashboard
# Grafana → Dashboard flask-red → Add panel → Visualization: Logs
# Query: {container="flask-app"}
# Save dashboard

✅ Kết quả mong đợi: Trong Grafana Explore, datasource Loki query {container="flask-app"} hiển thị log stream real-time; query |= "500" lọc đúng các dòng lỗi; count_over_time trả về số lỗi mỗi 5 phút.

🧹 Cleanup: Giữ stack cho LAB-110. Hoặc docker compose down -v.

LAB-109

Distributed Tracing với OpenTelemetry + Jaeger

OpenTelemetry SDK · OTel Collector · Jaeger

🎯 Mục tiêu: Instrument app Python bằng OpenTelemetry SDK, thu thập qua OTel Collector, xem distributed traces trên Jaeger UI.

🧰 Công cụ / nền tảng: opentelemetry-sdk, opentelemetry-exporter-otlp, OTel Collector, Jaeger.

📦 Chuẩn bị: Tạo thư mục mới lab-109 (độc lập với lab trước).

▶️ Các bước:

mkdir lab-109 && cd lab-109

# Bước 1: App Python với OpenTelemetry instrumentation
cat > app.py <<'PYEOF'
import time, random
from flask import Flask
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.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
import requests as req_lib

# Setup OTel tracer
provider = TracerProvider()
otlp_exporter = OTLPSpanExporter(
    endpoint="otel-collector:4317",
    insecure=True
)
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)

app = Flask(__name__)
FlaskInstrumentor().instrument_app(app)
RequestsInstrumentor().instrument()

@app.route('/')
def index():
    # Span chính — tự động tạo bởi FlaskInstrumentor
    with tracer.start_as_current_span("process-request") as span:
        span.set_attribute("user.id", random.randint(1000, 9999))
        # Gọi "service B" giả lập
        result = call_downstream()
        span.set_attribute("downstream.result", result)
    return f"Trace ID: {format(trace.get_current_span().get_span_context().trace_id, '032x')}"

def call_downstream():
    with tracer.start_as_current_span("call-database") as span:
        # Giả lập DB query 10-100ms
        latency = random.uniform(0.01, 0.1)
        time.sleep(latency)
        span.set_attribute("db.query", "SELECT * FROM users")
        span.set_attribute("db.latency_ms", int(latency * 1000))
        if random.random() < 0.1:
            span.set_status(trace.StatusCode.ERROR, "DB timeout")
            raise Exception("Database timeout")
        return "ok"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)
PYEOF

# Bước 2: Dockerfile
cat > Dockerfile <<'EOF'
FROM python:3.11-slim
RUN pip install flask \
    opentelemetry-sdk \
    opentelemetry-exporter-otlp-proto-grpc \
    opentelemetry-instrumentation-flask \
    opentelemetry-instrumentation-requests \
    requests
COPY app.py .
CMD ["python","app.py"]
EOF

# Bước 3: OTel Collector config
cat > otel-collector-config.yml <<'EOF'
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 1s

exporters:
  jaeger:
    endpoint: jaeger:14250
    tls:
      insecure: true
  logging:
    loglevel: info

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [jaeger, logging]
EOF

# Bước 4: docker-compose.yml
cat > docker-compose.yml <<'EOF'
version: '3.8'
services:
  flask-app:
    build: .
    ports:
      - "8000:8000"
    depends_on:
      - otel-collector

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.98.0
    ports:
      - "4317:4317"
      - "4318:4318"
    volumes:
      - ./otel-collector-config.yml:/etc/otel/config.yml
    command: ["--config=/etc/otel/config.yml"]

  jaeger:
    image: jaegertracing/all-in-one:1.57
    ports:
      - "16686:16686"   # Jaeger UI
      - "14250:14250"   # gRPC collector
    environment:
      - COLLECTOR_OTLP_ENABLED=true
EOF

# Bước 5: Khởi động
docker compose up -d --build
sleep 5

# Bước 6: Tạo traces
for i in {1..20}; do
  curl -s http://localhost:8000/
  sleep 0.3
done

# Bước 7: Xem traces trên Jaeger UI
# http://localhost:16686
# Service: flask-app → Find Traces
# Click vào trace → xem timeline, spans, attributes

# Bước 8: Tìm trace có lỗi DB
# Jaeger → Search → Tags: error=true
# Xem span "call-database" với status ERROR

# Bước 9: Xem trace ID trực tiếp từ response
curl http://localhost:8000/
# Output: "Trace ID: abc123..."
# Dán trace ID vào Jaeger → Search by trace ID

✅ Kết quả mong đợi: Jaeger UI tại http://localhost:16686 liệt kê service flask-app; mỗi trace có 2 span (GET /call-database); khoảng 10% trace có span call-database đánh dấu ERROR với message "Database timeout"; có thể click vào xem waterfall view.

🧹 Cleanup:

docker compose down -v
cd .. && rm -rf lab-109
LAB-110

Alert Rule và Notification với Alertmanager

Prometheus Alertmanager · Alert Rules · Webhook

🎯 Mục tiêu: Viết Prometheus alert rule cho error rate và latency; cấu hình Alertmanager route notification qua webhook (dùng webhook.site để test mà không cần email).

🧰 Công cụ / nền tảng: Prometheus Alertmanager, alert rules file, webhook.site (free test endpoint).

📦 Chuẩn bị: Stack từ LAB-106/107 đang chạy. Vào webhook.site lấy URL unique của bạn.

▶️ Các bước:

# (Làm trong thư mục lab-106)

# Bước 1: Tạo alert rules file
cat > alert-rules.yml <<'EOF'
groups:
  - name: flask-app-alerts
    interval: 15s
    rules:

      # Alert 1: Error rate > 3% trong 1 phút
      - alert: HighErrorRate
        expr: |
          sum(rate(app_requests_total{status="500"}[1m])) /
          sum(rate(app_requests_total[1m])) * 100 > 3
        for: 1m        # phải đúng liên tục 1 phút mới fire
        labels:
          severity: warning
          team: backend
        annotations:
          summary: "High error rate on flask-app"
          description: "Error rate is {{ printf \"%.1f\" $value }}% (threshold: 3%)"
          runbook: "https://wiki.example.com/runbooks/high-error-rate"

      # Alert 2: p95 latency > 500ms
      - alert: HighLatency
        expr: |
          histogram_quantile(0.95,
            sum(rate(app_request_latency_seconds_bucket[5m])) by (le)
          ) > 0.5
        for: 2m
        labels:
          severity: critical
          team: backend
        annotations:
          summary: "High p95 latency on flask-app"
          description: "p95 latency is {{ printf \"%.3f\" $value }}s (threshold: 0.5s)"

      # Alert 3: App down (no metrics for 30s)
      - alert: AppDown
        expr: up{job="flask-app"} == 0
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "flask-app is DOWN"
          description: "Prometheus cannot scrape flask-app"
EOF

# Bước 2: Tạo Alertmanager config
# Thay YOUR_WEBHOOK_URL bằng URL từ webhook.site
WEBHOOK_URL="https://webhook.site/YOUR-UUID-HERE"

cat > alertmanager.yml <<EOF
global:
  resolve_timeout: 5m

route:
  receiver: 'webhook-default'
  group_by: ['alertname', 'severity']
  group_wait: 10s
  group_interval: 30s
  repeat_interval: 1h
  routes:
    - match:
        severity: critical
      receiver: 'webhook-critical'
      group_wait: 5s     # critical alert nhanh hơn

receivers:
  - name: 'webhook-default'
    webhook_configs:
      - url: '${WEBHOOK_URL}'
        send_resolved: true
        http_config:
          follow_redirects: true

  - name: 'webhook-critical'
    webhook_configs:
      - url: '${WEBHOOK_URL}'
        send_resolved: true
        title: '[CRITICAL] {{ .GroupLabels.alertname }}'
EOF

# Bước 3: Update prometheus.yml để load rules
cat > prometheus.yml <<'EOF'
global:
  scrape_interval: 10s
  evaluation_interval: 10s

alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

rule_files:
  - /etc/prometheus/alert-rules.yml

scrape_configs:
  - job_name: 'flask-app'
    static_configs:
      - targets: ['flask-app:8000']
  - job_name: 'alertmanager'
    static_configs:
      - targets: ['alertmanager:9093']
EOF

# Bước 4: Thêm Alertmanager vào docker-compose.yml
cat >> docker-compose.yml <<'EOF'

  alertmanager:
    image: prom/alertmanager:v0.27.0
    ports:
      - "9093:9093"
    volumes:
      - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml
    command:
      - '--config.file=/etc/alertmanager/alertmanager.yml'
      - '--storage.path=/alertmanager'
    container_name: alertmanager
EOF

# Mount alert-rules.yml vào prometheus service
# Sửa volumes trong prometheus service (dùng sed hoặc edit thủ công):
# - ./alert-rules.yml:/etc/prometheus/alert-rules.yml

# Bước 5: Restart
docker compose up -d
sleep 5

# Bước 6: Kiểm tra Prometheus đã load rules
# http://localhost:9090/rules
# Mục "flask-app-alerts" phải hiển thị 3 rules

# Bước 7: Xem alerts đang pending/firing
# http://localhost:9090/alerts
# HighErrorRate sẽ vào Pending (app đang có ~5% error)
# Sau 1 phút → chuyển sang FIRING

# Bước 8: Xem Alertmanager
# http://localhost:9093
# Tab Alerts: xem alerts đang active + routing

# Bước 9: Kiểm tra notification trên webhook.site
# Bạn sẽ thấy POST request với payload JSON:
# {
#   "alerts": [{
#     "labels": {"alertname":"HighErrorRate","severity":"warning"},
#     "annotations": {"summary":"High error rate on flask-app","description":"..."}
#   }]
# }

# Bước 10: Test "App Down" alert
docker compose stop flask-app
# Sau 30s, AppDown alert sẽ FIRE → notification đến webhook.site
docker compose start flask-app   # resolved notification

✅ Kết quả mong đợi: Prometheus UI tại /alerts hiển thị HighErrorRate chuyển PENDING → FIRING sau 1 phút; Alertmanager nhận và route; webhook.site hiển thị POST request JSON với thông tin alert đầy đủ; khi dừng flask-app, AppDown FIRE; khi start lại, nhận resolved notification.

🧹 Cleanup:

docker compose down -v
cd .. && rm -rf lab-106

3. Tình huống doanh nghiệp thực tế

Bối cảnh

Một nền tảng thương mại điện tử xử lý 50,000 đơn hàng/ngày. Black Friday đến, traffic tăng 10x. Đêm trước sự kiện, latency checkout bắt đầu tăng nhẹ nhưng error rate vẫn 0% — team Ops không phát hiện vì chỉ monitor error rate. Sáng hôm sau checkout sập hoàn toàn, mất 3 tỷ VND doanh thu trong 2 giờ.

Root Cause Analysis với Observability Stack

  • Metrics (Prometheus): p99 latency checkout tăng từ 200ms lên 4s trong 2 tiếng trước khi sập. Saturation: connection pool database đạt 95%. Alert DBConnectionPoolSaturation > 80% đã tồn tại nhưng bị silence vì "false alarm thường xuyên" (alert debt).
  • Logs (Loki): LogQL query {service="checkout"} |= "connection pool" | json | pool_available < 5 cho thấy warning log từ 11pm — 7 tiếng trước khi sập. Không ai đọc log vì không có alert dựa trên log pattern.
  • Traces (Jaeger): Distributed trace của checkout request cho thấy 90% thời gian nằm ở span payment-service → db-query. Không phải code mới mà là missing index trên bảng order sau migration.
  • Cải tiến sau incident: Thêm alert saturation (USE method) và latency (RED method); tạo alert dựa trên log pattern; weekly trace review để phát hiện slow query sớm. MTTR giảm từ 2h xuống 8 phút nhờ runbook + trace ID trực tiếp trong alert.

📚 Nguồn tham khảo

Module 21: Release Engineering & Progressive Delivery Module 23: SRE, SLA/SLO/SLI & Incident Management
Zalo