← กลับ
LoggingMonitoringSelf-hosted

Centralized Logging ด้วย Loki + Grafana — รวม log ทุก service ที่เดียว

ssh เข้าทุก server แล้ว tail -f ทำได้แค่บอกได้ตอนเล็ก พอ service เยอะต้องรวม log ที่เดียว Loki ทำได้เบาและฟรี

2026-04-18อ่าน 6 นาทีใหม่

ปัญหาที่ต้องเจอตอน 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

  1. เปิด http://your-server:3000 (default user admin password ที่ตั้งใน env)
  2. Connections → Data sources → Add data source → Loki
  3. URL: http://loki:3100
  4. 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 เกินค่า:

  1. Alerting → Alert rules → New alert rule
  2. Query: sum(rate({level="error"}[5m])) > 1
  3. แจ้งทาง 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 ภายใน

← ดูบทความอื่น