Cybersecurity

Distributed Tracing Monitoring และ Alerting

distributed tracing monitoring และ alerting
Distributed Tracing Monitoring และ Alerting | SiamCafe Blog
2025-11-12· อ. บอม — SiamCafe.net· 1,529 คำ

ทำไม Monitoring และ Alerting ถึงสำคัญสำหรับ Distributed Tracing

ระบบ microservices ที่มี distributed tracing เก็บข้อมูลมหาศาล แต่ถ้าไม่มี monitoring และ alerting ที่ดี ข้อมูลเหล่านั้นก็ไม่มีประโยชน์ ต้องสร้างระบบที่แปลง trace data ให้เป็น actionable alerts ได้อัตโนมัติ เช่น แจ้งเตือนเมื่อ P99 latency ของ service ใด service หนึ่งพุ่งสูงกว่าปกติ หรือ error rate เกินค่าที่ยอมรับได้

สถาปัตยกรรมของระบบ Trace Monitoring

ข้อมูล trace จาก application ส่งผ่าน OpenTelemetry Collector ซึ่งสร้าง span metrics (RED metrics) แล้วส่งไปยัง Prometheus สำหรับ alerting ส่วน raw trace ส่งไป Jaeger สำหรับ drill-down

# docker-compose.yml - Full monitoring stack
version: '3.8'
services:
  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.96.0
    command: ["--config=/etc/otel-config.yaml"]
    volumes:
      - ./otel-config.yaml:/etc/otel-config.yaml
    ports:
      - "4317:4317"
      - "4318:4318"
      - "8889:8889"

  jaeger:
    image: jaegertracing/all-in-one:1.54
    ports:
      - "16686:16686"
    environment:
      - COLLECTOR_OTLP_ENABLED=true

  prometheus:
    image: prom/prometheus:v2.50.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
      - ./alert-rules.yml:/etc/prometheus/alert-rules.yml

  alertmanager:
    image: prom/alertmanager:v0.27.0
    ports:
      - "9093:9093"
    volumes:
      - ./alertmanager.yml:/etc/alertmanager/alertmanager.yml

  grafana:
    image: grafana/grafana:10.3.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana

volumes:
  grafana_data:

ตั้งค่า OpenTelemetry Collector สร้าง Span Metrics

spanmetrics connector แปลง trace data เป็น Prometheus metrics อัตโนมัติ ได้ทั้ง request count, duration histogram และ error count

# otel-config.yaml
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

connectors:
  spanmetrics:
    histogram:
      explicit:
        buckets: [5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, 2.5s, 5s, 10s]
    dimensions:
      - name: http.method
      - name: http.status_code
      - name: http.route
    exemplars:
      enabled: true
    metrics_flush_interval: 15s

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024
  memory_limiter:
    check_interval: 1s
    limit_mib: 2048

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889
    namespace: traces

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [spanmetrics, otlp/jaeger]
    metrics/spanmetrics:
      receivers: [spanmetrics]
      exporters: [prometheus]

Prometheus Alert Rules สำหรับ Trace Metrics

สร้าง alert rules ครอบคลุม 3 สถานการณ์หลัก: latency สูง, error rate สูง และ throughput ตก

# alert-rules.yml
groups:
  - name: trace_latency_alerts
    rules:
      - alert: HighP99Latency
        expr: |
          histogram_quantile(0.99,
            sum(rate(traces_spanmetrics_duration_seconds_bucket{span_kind="SPAN_KIND_SERVER"}[5m])) by (le, service_name)
          ) > 2.0
        for: 3m
        labels:
          severity: critical
          team: platform
        annotations:
          summary: "{{ $labels.service_name }} P99 latency สูงกว่า 2 วินาที"
          description: "P99 latency ปัจจุบัน {{ $value | humanizeDuration }}"
          runbook: "https://wiki.internal/runbooks/high-latency"

      - alert: HighP50Latency
        expr: |
          histogram_quantile(0.5,
            sum(rate(traces_spanmetrics_duration_seconds_bucket{span_kind="SPAN_KIND_SERVER"}[5m])) by (le, service_name)
          ) > 0.5
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.service_name }} median latency สูงกว่า 500ms"

  - name: trace_error_alerts
    rules:
      - alert: HighErrorRate
        expr: |
          sum(rate(traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}[5m])) by (service_name)
          /
          sum(rate(traces_spanmetrics_calls_total[5m])) by (service_name)
          > 0.05
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.service_name }} error rate {{ $value | humanizePercentage }}"

      - alert: ErrorRateSpike
        expr: |
          sum(rate(traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}[5m])) by (service_name)
          /
          sum(rate(traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}[1h])) by (service_name)
          > 3
        for: 1m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.service_name }} error rate เพิ่มขึ้น 3 เท่าจากชั่วโมงที่ผ่านมา"

  - name: trace_throughput_alerts
    rules:
      - alert: LowThroughput
        expr: |
          sum(rate(traces_spanmetrics_calls_total{span_kind="SPAN_KIND_SERVER"}[5m])) by (service_name)
          < 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "{{ $labels.service_name }} throughput ต่ำกว่า 1 req/s อาจมีปัญหา"

      - alert: ThroughputDrop
        expr: |
          sum(rate(traces_spanmetrics_calls_total{span_kind="SPAN_KIND_SERVER"}[5m])) by (service_name)
          /
          sum(rate(traces_spanmetrics_calls_total{span_kind="SPAN_KIND_SERVER"}[1h])) by (service_name)
          < 0.3
        for: 3m
        labels:
          severity: critical
        annotations:
          summary: "{{ $labels.service_name }} throughput ลดลงมากกว่า 70%"

ตั้งค่า Alertmanager ส่งแจ้งเตือนไป Slack และ LINE

# alertmanager.yml
global:
  resolve_timeout: 5m
  slack_api_url: 'https://hooks.slack.com/services/T00000/B00000/XXXXXXX'

route:
  receiver: 'slack-default'
  group_by: ['alertname', 'service_name']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - match:
        severity: critical
      receiver: 'slack-critical'
      group_wait: 10s
      repeat_interval: 1h
    - match:
        severity: warning
      receiver: 'slack-warning'

receivers:
  - name: 'slack-default'
    slack_configs:
      - channel: '#alerts-platform'
        title: '{{ .GroupLabels.alertname }}'
        text: |
          *Service:* {{ .GroupLabels.service_name }}
          *Severity:* {{ .CommonLabels.severity }}
          {{ range .Alerts }}
          - {{ .Annotations.summary }}
          {{ end }}

  - name: 'slack-critical'
    slack_configs:
      - channel: '#alerts-critical'
        title: 'CRITICAL: {{ .GroupLabels.alertname }}'
        color: '#ff0000'
        text: |
          {{ range .Alerts }}
          *{{ .Annotations.summary }}*
          {{ .Annotations.description }}
          Runbook: {{ .Annotations.runbook }}
          {{ end }}
    webhook_configs:
      - url: 'https://notify-api.line.me/api/notify'
        http_config:
          authorization:
            type: Bearer
            credentials: 'LINE_NOTIFY_TOKEN'

  - name: 'slack-warning'
    slack_configs:
      - channel: '#alerts-warning'
        color: '#ffa500'

Prometheus Config สำหรับ Scrape Metrics

# prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

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

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

scrape_configs:
  - job_name: 'otel-collector'
    static_configs:
      - targets: ['otel-collector:8889']
    scrape_interval: 10s

  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

Instrument Application ด้วย OpenTelemetry

# ติดตั้ง OpenTelemetry สำหรับ Go application
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
// main.go - Go service พร้อม OpenTelemetry
package main

import (
    "context"
    "log"
    "net/http"
    "time"

    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func initTracer() (*sdktrace.TracerProvider, error) {
    ctx := context.Background()

    exporter, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("otel-collector:4317"),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter,
            sdktrace.WithBatchTimeout(5*time.Second),
            sdktrace.WithMaxExportBatchSize(512),
        ),
        sdktrace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName("order-service"),
            semconv.ServiceVersion("1.0.0"),
            attribute.String("environment", "production"),
        )),
        sdktrace.WithSampler(sdktrace.ParentBased(
            sdktrace.TraceIDRatioBased(0.1),
        )),
    )

    otel.SetTracerProvider(tp)
    return tp, nil
}

func main() {
    tp, err := initTracer()
    if err != nil {
        log.Fatal(err)
    }
    defer tp.Shutdown(context.Background())

    tracer := otel.Tracer("order-service")

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx, span := tracer.Start(r.Context(), "process-order")
        defer span.End()

        span.SetAttributes(
            attribute.String("order.id", r.URL.Query().Get("id")),
            attribute.String("http.route", "/api/orders"),
        )

        // simulate processing
        time.Sleep(50 * time.Millisecond)
        w.Write([]byte(`{"status":"ok"}`))
        _ = ctx
    })

    wrappedHandler := otelhttp.NewHandler(handler, "order-api")
    log.Println("Starting server on :8080")
    log.Fatal(http.ListenAndServe(":8080", wrappedHandler))
}

Grafana Dashboard สำหรับ Trace Monitoring

# grafana-datasources.yml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090
    isDefault: true
  - name: Jaeger
    type: jaeger
    url: http://jaeger:16686
# PromQL queries สำหรับ dashboard panels

# Panel 1: Request Rate per Service
sum(rate(traces_spanmetrics_calls_total{span_kind="SPAN_KIND_SERVER"}[5m])) by (service_name)

# Panel 2: P50/P95/P99 Latency
histogram_quantile(0.50, sum(rate(traces_spanmetrics_duration_seconds_bucket{span_kind="SPAN_KIND_SERVER"}[5m])) by (le, service_name))
histogram_quantile(0.95, sum(rate(traces_spanmetrics_duration_seconds_bucket{span_kind="SPAN_KIND_SERVER"}[5m])) by (le, service_name))
histogram_quantile(0.99, sum(rate(traces_spanmetrics_duration_seconds_bucket{span_kind="SPAN_KIND_SERVER"}[5m])) by (le, service_name))

# Panel 3: Error Rate %
100 * sum(rate(traces_spanmetrics_calls_total{status_code="STATUS_CODE_ERROR"}[5m])) by (service_name)
/ sum(rate(traces_spanmetrics_calls_total[5m])) by (service_name)

# Panel 4: Top 10 Slowest Operations
topk(10,
  histogram_quantile(0.95,
    sum(rate(traces_spanmetrics_duration_seconds_bucket[5m])) by (le, span_name, service_name)
  )
)

# Panel 5: Active Alerts
ALERTS{alertstate="firing"}

FAQ - คำถามที่พบบ่อย

Q: Span Metrics กับ Application Metrics ต่างกันอย่างไร?

A: Span Metrics สร้างจาก trace data อัตโนมัติ ครอบคลุมทุก service ที่ส่ง trace มาโดยไม่ต้องเขียน code เพิ่ม Application Metrics คือ custom metrics ที่นักพัฒนาเขียนเอง เช่น จำนวน orders, revenue ควรใช้ทั้งสองอย่างเสริมกัน

Q: ตั้ง alert threshold เท่าไหร่ดี?

A: เริ่มจากดู baseline ของระบบก่อน 1-2 สัปดาห์ แล้วตั้ง threshold ที่ 2-3 เท่าของค่า P99 ปกติ สำหรับ error rate เริ่มที่ 5% ปรับตามความเหมาะสมของแต่ละ service อย่าตั้งเข้มเกินไปจนเกิด alert fatigue

Q: ใช้ Grafana Alerting แทน Alertmanager ได้ไหม?

A: ได้ Grafana 10+ มี alerting engine ที่ดีมาก ข้อดีคือตั้ง alert ได้จาก UI โดยไม่ต้องเขียน YAML ข้อเสียคือ Alertmanager มี features ที่ mature กว่า เช่น silencing, inhibition rules และ template ที่ยืดหยุ่นกว่า ถ้าใช้ Prometheus stack อยู่แล้วแนะนำ Alertmanager

Q: ควรเก็บ trace data นานแค่ไหน?

A: Raw traces เก็บ 7-14 วันพอ เพราะใช้สำหรับ debugging เฉพาะ incident ที่เพิ่งเกิด ส่วน span metrics ที่ Prometheus เก็บควรเก็บ 30-90 วัน เพื่อดู trend และทำ capacity planning ถ้าต้องการเก็บ metrics นานกว่านั้นใช้ Thanos หรือ Mimir

📖 บทความที่เกี่ยวข้อง

AWS App Runner Monitoring และ Alertingอ่านบทความ → Distributed Tracing Zero Downtime Deploymentอ่านบทความ → Qwik Resumability Monitoring และ Alertingอ่านบทความ → Distributed Tracing Batch Processing Pipelineอ่านบทความ → Linux Perf Tools Monitoring และ Alertingอ่านบทความ →

📚 ดูบทความทั้งหมด →