Workshop: สร้าง Complete Monitoring Stack (Prometheus + Loki + Jaeger + Grafana)

ในระบบ production ยุคปัจจุบัน การมีแค่ metrics ไม่เพียงพอต่อการแก้ปัญหาซับซ้อน ทีม DevOps และ SRE จึงต้องรวม 3 เสาหลักของ observability เข้าด้วยกัน นั่นคือ metrics, logs และ traces เพื่อให้มองเห็นระบบแบบ 360 องศา เมื่อเกิดปัญหาก็สามารถไล่หาสาเหตุได้ตั้งแต่ตัวเลขภาพรวม ลงไปถึง log บรรทัดที่ error และสืบย้อนไปยัง request ต้นทางที่เสียเวลาจริง

บทความนี้เป็น workshop ที่จะพาคุณติดตั้ง complete monitoring stack ด้วย Prometheus, Loki, Jaeger และ Grafana บน Docker Compose ชุดเดียว จบครบใน 1 ไฟล์ พร้อมทดสอบกับแอปตัวอย่างเพื่อดูว่าทั้ง 3 pillar เชื่อมโยงกันอย่างไร

ทำความรู้จักกับ 3 Pillars of Observability

ก่อนเริ่มตั้งค่า ควรเข้าใจก่อนว่าแต่ละตัวทำหน้าที่อะไร เพราะแต่ละเครื่องมือตอบคำถามคนละแบบและใช้ทดแทนกันไม่ได้

  • Metrics (Prometheus): ตัวเลขเชิงปริมาณที่เก็บเป็น time series เช่น CPU, memory, request rate, error rate ตอบคำถามว่า “ตอนนี้ระบบเป็นยังไง”
  • Logs (Loki): ข้อความที่แอปพลิเคชันเขียนออกมาเมื่อเกิดเหตุการณ์ต่าง ๆ ตอบคำถามว่า “เกิดอะไรขึ้น ทำไมถึง error”
  • Traces (Jaeger): บันทึกเส้นทางของ request ที่วิ่งผ่าน service หลายตัว ตอบคำถามว่า “request นี้ช้าที่ step ไหน”
  • Grafana: ตัว visualization ที่นำทั้ง 3 แหล่งมารวมในหน้าเดียว ทำให้สืบสวนปัญหาได้จากจุดเริ่มต้นถึงราก

Prerequisites ก่อนเริ่ม Workshop

ตรวจให้แน่ใจว่าเครื่องของคุณพร้อม เพราะ stack นี้ใช้ RAM พอสมควร แนะนำขั้นต่ำ 4 GB RAM และพื้นที่ว่าง 10 GB

  • Docker Engine 24+ และ Docker Compose v2+
  • Port ที่ต้องว่าง: 3000 (Grafana), 9090 (Prometheus), 3100 (Loki), 16686 (Jaeger UI)
  • Linux, macOS หรือ Windows ที่มี WSL2
  • พื้นฐานการอ่านไฟล์ YAML และคำสั่ง Docker เบื้องต้น

ขั้นตอนที่ 1: เตรียมโครงสร้างโฟลเดอร์

สร้างโฟลเดอร์หลักของโปรเจค พร้อมโฟลเดอร์ย่อยสำหรับ config แต่ละตัว เพื่อให้ง่ายต่อการแก้ไขและ version control

mkdir -p observability-stack/{prometheus,loki,promtail,grafana/provisioning/datasources}
cd observability-stack
touch docker-compose.yml
touch prometheus/prometheus.yml
touch loki/loki-config.yml
touch promtail/promtail-config.yml
touch grafana/provisioning/datasources/datasources.yml

ขั้นตอนที่ 2: สร้าง Docker Compose

ไฟล์นี้คือหัวใจของ workshop ทุก service จะสื่อสารกันผ่าน network เดียวกัน ชื่อ service จะใช้เป็น hostname ในการเชื่อมต่อระหว่างกัน

services:
  prometheus:
    image: prom/prometheus:v2.53.0
    container_name: prometheus
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=15d'
    networks:
      - observability

  loki:
    image: grafana/loki:3.0.0
    container_name: loki
    ports:
      - "3100:3100"
    volumes:
      - ./loki/loki-config.yml:/etc/loki/local-config.yaml
      - loki_data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - observability

  promtail:
    image: grafana/promtail:3.0.0
    container_name: promtail
    volumes:
      - /var/log:/var/log
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - ./promtail/promtail-config.yml:/etc/promtail/config.yml
    command: -config.file=/etc/promtail/config.yml
    networks:
      - observability
    depends_on:
      - loki

  jaeger:
    image: jaegertracing/all-in-one:1.57
    container_name: jaeger
    ports:
      - "16686:16686"
      - "4317:4317"
      - "4318:4318"
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    networks:
      - observability

  grafana:
    image: grafana/grafana:11.0.0
    container_name: grafana
    ports:
      - "3000:3000"
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning
      - grafana_data:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
    networks:
      - observability
    depends_on:
      - prometheus
      - loki
      - jaeger

networks:
  observability:
    driver: bridge

volumes:
  prometheus_data:
  loki_data:
  grafana_data:

ขั้นตอนที่ 3: ตั้งค่า Prometheus

สร้างไฟล์ prometheus/prometheus.yml โดยกำหนด scrape target ทั้งของ Prometheus เองและ container ตัวอย่างที่จะเพิ่มในขั้นตอนท้าย

global:
  scrape_interval: 15s
  evaluation_interval: 15s

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

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'sample-app'
    static_configs:
      - targets: ['sample-app:8080']

ขั้นตอนที่ 4: ตั้งค่า Loki และ Promtail

Loki เป็นตัวรับและเก็บ log ส่วน Promtail คือ agent ที่วิ่งอ่าน log จาก Docker container แล้วส่งไปให้ Loki ทั้งคู่ทำงานประสานกัน

ไฟล์ loki/loki-config.yml สำหรับ single-node development:

auth_enabled: false

server:
  http_listen_port: 3100

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 168h

ไฟล์ promtail/promtail-config.yml เก็บ log จาก Docker container ทุกตัวในเครื่อง:

server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

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

scrape_configs:
  - job_name: docker
    static_configs:
      - targets:
          - localhost
        labels:
          job: docker
          __path__: /var/lib/docker/containers/*/*-json.log

ขั้นตอนที่ 5: เชื่อม Grafana กับ Data Sources ทุกตัว

แทนที่จะไปกดเพิ่มใน UI ทุกครั้ง เราใช้ provisioning file เพื่อให้ Grafana สร้าง data source อัตโนมัติตอนบูต วิธีนี้ทำให้ setup ทำซ้ำได้และเก็บใน Git ได้

ไฟล์ grafana/provisioning/datasources/datasources.yml:

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true

  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100

  - name: Jaeger
    type: jaeger
    access: proxy
    url: http://jaeger:16686

ขั้นตอนที่ 6: Start Stack และตรวจสอบ

เมื่อไฟล์ทุกอันพร้อม สั่ง start ทั้ง stack ด้วยคำสั่งเดียว แล้วตรวจสอบว่าทุก service ขึ้นครบก่อนเข้า UI

docker compose up -d
docker compose ps

เข้าแต่ละ UI ตามนี้เพื่อยืนยันว่าแต่ละบริการทำงานได้:

  • Prometheus: http://localhost:9090 → Status → Targets ต้องเห็นสีเขียวทุกตัว (ยกเว้น sample-app ที่ยังไม่รัน)
  • Loki: http://localhost:3100/ready → ควรตอบ ready
  • Jaeger: http://localhost:16686 → เข้าหน้า Search ได้
  • Grafana: http://localhost:3000 → login ด้วย admin/admin → Connections → Data sources → เห็นทั้ง 3 ตัว

ขั้นตอนที่ 7: ติดตั้งแอปตัวอย่างเพื่อทดสอบ

เพื่อให้เห็นการทำงานครบทั้ง 3 pillar เราเพิ่ม sample app ที่ export metrics ตามมาตรฐาน Prometheus, เขียน log ออก stdout และส่ง trace ไปยัง Jaeger เพิ่ม service ต่อไปนี้ใน docker-compose.yml:

  node-exporter:
    image: prom/node-exporter:v1.8.1
    container_name: node-exporter
    ports:
      - "9100:9100"
    networks:
      - observability

  sample-app:
    image: otel/opentelemetry-collector-contrib:0.102.0
    container_name: sample-app
    command: ["--config=/etc/otel-config.yaml"]
    volumes:
      - ./sample-app/otel-config.yaml:/etc/otel-config.yaml
    ports:
      - "8080:8080"
    networks:
      - observability
    depends_on:
      - jaeger

สร้าง sample-app/otel-config.yaml ที่รับ trace จาก client แล้วส่งต่อไป Jaeger พร้อม expose metrics สำหรับ Prometheus:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8080

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlp/jaeger]
    metrics:
      receivers: [otlp]
      exporters: [prometheus]

ขั้นตอนที่ 8: สร้าง Unified Dashboard

หลัง stack พร้อมใช้งาน ให้สร้าง dashboard แรกที่รวมทุก pillar เข้าไว้หน้าเดียว วิธีที่แนะนำคือเข้า Grafana → Dashboards → New → Import แล้วใช้ ID ของ dashboard สำเร็จรูป เช่น 1860 (Node Exporter Full) เพื่อดู metric ของเครื่อง จากนั้นเพิ่ม panel ใหม่ด้วยตัวเอง

  • Panel Metrics: เลือก Prometheus → query rate(http_requests_total[5m])
  • Panel Logs: เลือก Loki → query {container="sample-app"} → แสดง log แบบ live
  • Panel Traces: เลือก Jaeger → Search service → เห็นเส้นทาง request
  • ใช้ Variables เช่น $service ให้ทุก panel ใน dashboard เปลี่ยน service พร้อมกัน

เคล็ดลับ: Derived Fields เชื่อม Log ไป Trace

ใน Data source ของ Loki เพิ่ม Derived Fields โดย match pattern เช่น trace_id=(\w+) แล้ว link ไปยัง Jaeger ด้วย URL http://localhost:16686/trace/${__value.raw} ผลที่ได้คือเวลาดู log แล้วเจอ trace_id สามารถคลิกได้ทันทีไปเปิด trace ใน Jaeger ช่วยลด context switch ตอน debug

Troubleshooting ที่พบบ่อยใน Workshop

ถ้าเริ่ม stack แล้วมีบางตัวไม่ทำงาน ลองไล่ตามขั้นตอนเหล่านี้ก่อนที่จะ rebuild ใหม่ทั้งหมด

  • เช็ค log แต่ละ container ด้วย docker compose logs -f loki เปลี่ยนชื่อ service ตามที่ต้องการดู
  • ถ้า Prometheus เห็น target เป็น DOWN ให้ตรวจว่า container นั้นอยู่ network เดียวกันหรือไม่ และ port ตรงกับ config
  • ถ้า Loki error เรื่อง schema ให้ลบ volume loki_data แล้ว start ใหม่ เพราะบางเวอร์ชันเปลี่ยน schema format
  • Grafana login ไม่ได้ให้ลบ volume grafana_data แล้ว start ใหม่ ระบบจะ reset กลับเป็น admin/admin
  • Port ชนกับของเดิมในเครื่อง → แก้ที่ mapping "3001:3000" ใน compose แทนการปิดโปรแกรมอื่น

แนวทางการนำไปใช้จริงใน Production

Workshop นี้เหมาะกับการเรียนรู้และ dev environment ถ้าจะใช้จริงใน production ต้องปรับอีกหลายจุดเพื่อให้ระบบรองรับ load และเก็บข้อมูลได้นาน

  • ใช้ object storage (S3 compatible) แทน filesystem สำหรับ Loki และ long-term Prometheus
  • เปลี่ยน Jaeger all-in-one เป็น production deployment ที่ใช้ Cassandra หรือ Elasticsearch
  • เปิด authentication และ TLS ทั้งหน้า Grafana และการส่งข้อมูลระหว่าง component
  • เพิ่ม Alertmanager ต่อจาก Prometheus เพื่อยิง alert ไป Slack, Email หรือ PagerDuty
  • ใช้ Grafana Agent หรือ OpenTelemetry Collector ตัวเดียวส่งทั้ง 3 pillar ลดจำนวน agent ที่ต้องดูแล

สรุป

การรวม Prometheus, Loki, Jaeger และ Grafana เข้าเป็น stack เดียวทำให้ทีมมองเห็นระบบแบบ 360 องศา ตั้งแต่ตัวเลข metric ภาพรวม ลงไปถึง log เฉพาะบรรทัดและ trace ของแต่ละ request ช่วยย่นเวลาในการหาสาเหตุจากหลายชั่วโมงเหลือเพียงไม่กี่นาที workshop นี้ช่วยให้คุณมี foundation พร้อมต่อยอดสู่ production โดยมี architecture เดียวกัน

ขั้นตอนต่อไปที่ควรทำคือติดตั้ง Alertmanager เพื่อส่งแจ้งเตือน ทดลองเขียน query LogQL และ PromQL ให้ชิน และฝึกใช้ Derived Fields เพื่อเชื่อม log กับ trace ให้คล่อง เมื่อใช้จริงกับ service หลายตัวคุณจะเห็นคุณค่าของ observability stack นี้ทันทีเมื่อเกิดปัญหา production