ปัญหาที่ต้องเจอตอน scale
ตอนมี server ตัวเดียว — tail -f /var/log/myapp.log พอ
ตอนมี 5 service บน 3 server — ต้อง ssh เข้าแต่ละเครื่อง search ทีละ log
ปัญหา:
- จะ debug error ตอนตี 3 — ssh ไม่ทันเหตุการณ์
- ลูกค้าแจ้งบัก — ไม่รู้ว่าควรไปดู log ที่ service ไหน
- log rotate แล้ว — เหตุการณ์เก่าหายไป
Centralized logging = รวม log จากทุก service ส่งเข้าระบบเดียว search ได้
Tool ในตลาด
- ELK Stack (Elasticsearch + Logstash + Kibana) — popular แต่กิน RAM เยอะ (3-8GB)
- Loki + Grafana (Grafana Labs) — light กว่ามาก ใช้ได้บน VPS เล็ก
- Datadog / New Relic — paid SaaS ใช้ง่ายแต่แพง
- CloudWatch Logs — ถ้าอยู่ AWS อยู่แล้ว
- Vector + ClickHouse — performance สูงสุด setup ซับซ้อน
บทความนี้ใช้ Loki + Grafana — ตั้ง 10 นาที ฟรี รัน VPS 2GB RAM ได้สบาย
Loki ต่างจาก Elasticsearch ยังไง
Elasticsearch index ทุก field ของ log → search ได้ละเอียด แต่กิน disk + RAM เยอะ
Loki index เฉพาะ label (เช่น service, level, env) ไม่ index content ตอน search → filter ด้วย label ก่อน → grep content (เร็วเพราะ data set เล็กแล้ว)
ผลลัพธ์: ใช้ resource น้อยกว่า ELK 10 เท่า แต่ search complex query ได้น้อยกว่า — เพียงพอสำหรับ 90% ของ use case
Stack ที่เราจะตั้ง
[App service] ──→ [Promtail] ──→ [Loki] ──→ [Grafana UI]
(log shipper) (storage) (search/visualize)
3 component:
- Loki — รับและเก็บ log
- Promtail — agent ที่ตามอ่าน log file ส่งไป Loki
- Grafana — UI ดู/search log
ตั้งด้วย Docker Compose
docker-compose.yml:
services:
loki:
image: grafana/loki:3.0.0
container_name: loki
ports:
- "3100:3100"
volumes:
- ./loki-config.yml:/etc/loki/local-config.yaml
- lokidata:/loki
command: -config.file=/etc/loki/local-config.yaml
restart: unless-stopped
promtail:
image: grafana/promtail:3.0.0
container_name: promtail
volumes:
- ./promtail-config.yml:/etc/promtail/config.yml
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
restart: unless-stopped
grafana:
image: grafana/grafana:11.0.0
container_name: grafana
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
volumes:
- grafanadata:/var/lib/grafana
restart: unless-stopped
volumes:
lokidata:
grafanadata:
loki-config.yml:
auth_enabled: false
server:
http_listen_port: 3100
common:
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: 720h # เก็บ 30 วัน
promtail-config.yml:
server:
http_listen_port: 9080
clients:
- url: http://loki:3100/loki/api/v1/push
positions:
filename: /tmp/positions.yaml
scrape_configs:
# อ่าน log ของ system
- job_name: system
static_configs:
- targets: [localhost]
labels:
job: varlogs
host: ${HOSTNAME}
__path__: /var/log/*.log
# อ่าน log ของ Docker container
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'
- source_labels: ['__meta_docker_container_log_stream']
target_label: 'stream'
start:
docker compose up -d
เปิด Grafana ตั้ง Datasource
- เปิด
http://your-server:3000(default useradminpassword ที่ตั้งใน env) - Connections → Data sources → Add data source → Loki
- URL:
http://loki:3100 - Save & Test
Search log ด้วย LogQL
ไป Explore เลือก datasource Loki
LogQL syntax คล้าย PromQL:
# ทุก log ของ container nginx
{container="nginx"}
# log ที่มีคำ "error"
{container="nginx"} |= "error"
# regex
{container="nginx"} |~ "5\d{2}"
# JSON parse
{container="myapp"} | json | level="error"
# count error per minute
sum by (container) (rate({level="error"}[1m]))
Log แบบ structured ให้ search ง่าย
แทนที่จะ log แบบนี้:
2026-04-18 10:23:45 user 123 logged in from 1.2.3.4
log JSON:
{"ts":"2026-04-18T10:23:45Z","level":"info","msg":"user logged in","user_id":"123","ip":"1.2.3.4"}
ใน Node.js ใช้ Pino:
import pino from 'pino'
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
})
logger.info({ user_id: 123, ip: '1.2.3.4' }, 'user logged in')
logger.error({ err: error }, 'failed to process payment')
ใน LogQL ใช้ JSON parser:
{container="myapp"} | json | user_id="123"
Log Levels — ใช้ให้ถูก
- error — เกิดเหตุที่ไม่คาดคิด ต้องสนใจ
- warn — ผิดปกติแต่ไม่ critical (deprecated API, retry)
- info — เหตุการณ์ปกติ (user login, request)
- debug — รายละเอียดสำหรับ dev (production ไม่เปิด)
อย่า log ทุกอย่างเป็น info — กลายเป็น noise
อย่า log อะไรบ้าง
⚠️ ห้าม log:
- Password (แม้แต่ hashed)
- Credit card / CVV
- API key / token
- Personal data ที่กฎหมาย PDPA / GDPR ไม่อนุญาต
ใส่ filter ลบข้อมูล sensitive ก่อน log ออก:
const safeLogger = pino({
redact: {
paths: ['password', 'token', 'authorization', 'cookie', '*.password'],
censor: '[REDACTED]',
},
})
Alerting จาก log
ตั้ง alert ใน Grafana ถ้า error เกินค่า:
- Alerting → Alert rules → New alert rule
- Query:
sum(rate({level="error"}[5m])) > 1 - แจ้งทาง Slack/Discord/Email
ตัวอย่าง: ถ้า error rate มากกว่า 1 ครั้ง/วินาทีในช่วง 5 นาที → Alert
ส่ง log จาก server อื่นเข้ามา
ถ้ามี server หลายตัว — ตั้ง Promtail บนทุกเครื่อง ส่งไป Loki ที่ central server:
# promtail-config.yml ของแต่ละ edge server
clients:
- url: http://loki.internal:3100/loki/api/v1/push
# หรือผ่าน VPN — อ่าน /blog/tailscale-wireguard-vpn
ใช้ Tailscale หรือ WireGuard เปิด private network — ไม่ต้อง expose Loki public
Retention และ size
Loki เก็บ 30 วันโดย default ตั้ง retention_period ใน config
estimate disk: ~50-100 MB / day / service สำหรับ web app ปกติ
- 5 service × 30 วัน × 80MB ≈ 12 GB
ถ้าน้อย — เพิ่ม retention; ถ้าเยอะ — ลด หรือใช้ S3 backend แทน filesystem
Alternative ถ้ามี Cloudflare
ถ้าอยู่ Cloudflare อยู่แล้ว — Logpush ส่ง log ของ HTTP request ตรงไป R2/S3/Datadog ฟรี (ใน Pro plan ขึ้นไป)
ใช้ตามกัน: Loki รับ app log, Logpush รับ HTTP log
สรุป
ทีมเล็ก project เริ่มต้น — แค่ centralize log ของ app + nginx ก็พอ ใช้ disk ไม่ถึง GB
ลงทุนเวลา 1-2 ชั่วโมง — แต่ครั้งหน้ามีคนถามว่า "เกิดอะไร" ตอน 03:00 น. — เปิด Grafana search 30 วินาทีจบ
อ่านต่อ: Monitoring App ด้วย Uptime Kuma — ใช้คู่กัน Uptime check ภายนอก, Loki ดู log ภายใน