ในระบบ 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

