ทำไม 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
