Bỏ qua đến nội dung chính
log aggregationLokiELKobservabilityDevOps

Log Aggregation: Loki vs ELK — Chọn Stack Nào?

Log aggregation với Loki vs ELK Stack: so sánh kiến trúc, cost, query, scale. Setup Docker Compose, structured log Node.js. Khi nào dùng cái nào?

Xuất bản 8 phút đọc

Log aggregation đúng setup khác nhau giữa "tools đẹp" và "thực sự dùng được". Bài này so sánh Loki vs ELK, code structured logging Node.js, setup Docker Compose chạy local.

Vì sao cần aggregate log?

SSH vào 10 server tail log: không scale. Log rotate mất. Crash mới biết bug. Aggregate vào 1 nơi → search, alert, correlate cross-service.

Mục tiêu: từ "production có lỗi" → biết chính xác user nào, request ID nào, stack trace gì, < 1 phút.

Kiến trúc 2 stack

Loki StackELK Stack
StorageLoki (chunk + label index)Elasticsearch (full-text index)
ShipperPromtail / Fluent BitFilebeat / Logstash
UIGrafana (LogQL)Kibana (KQL/Lucene)
Index storageChỉ label — nhỏFull content — lớn
RAM yêu cầu~1GB cho 100GB log~10-30GB cho 100GB log
QueryFilter + grepFull-text + complex
CostRẻ (S3 backend)Đắt scale

Loki + Promtail setup

# compose.yml
services:
  loki:
    image: grafana/loki:latest
    ports: ["3100:3100"]
    volumes:
      - loki-data:/loki

  promtail:
    image: grafana/promtail:latest
    volumes:
      - ./promtail.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

  grafana:
    image: grafana/grafana:latest
    ports: ["3001:3000"]

volumes:
  loki-data:
# promtail.yml — scrape Docker container log
server:
  http_listen_port: 9080

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

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
    relabel_configs:
      - source_labels: ['__meta_docker_container_name']
        target_label: 'container'

Structured logging Node.js

npm install pino pino-pretty
import pino from 'pino'
import { randomUUID } from 'node:crypto'

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  formatters: {
    level: (label) => ({ level: label }),  // string thay vì number
  },
  timestamp: pino.stdTimeFunctions.isoTime,
})

// Middleware: gắn request_id + log mỗi request
app.use((req, res, next) => {
  req.id = req.headers['x-request-id'] || randomUUID()
  req.log = logger.child({ request_id: req.id, user_id: req.user?.id })
  res.on('finish', () => {
    req.log.info({
      method: req.method,
      url: req.url,
      status: res.statusCode,
      duration: Date.now() - req.startTime,
    }, 'http_request')
  })
  next()
})

// Trong handler
app.post('/orders', requireAuth, async (req, res) => {
  req.log.info({ amount: req.body.amount }, 'creating order')
  try {
    const order = await createOrder(req.user.id, req.body)
    req.log.info({ order_id: order.id }, 'order created')
    res.json(order)
  } catch (err) {
    req.log.error({ err }, 'order creation failed')
    res.status(500).json({ error: 'INTERNAL' })
  }
})

Output JSON:

{"level":"info","time":"2026-05-07T10:23:45.123Z","request_id":"a3f...","user_id":42,"amount":1000,"msg":"creating order"}

Loki parse JSON tự động — query theo field.

LogQL query

# Tất cả log container 'api'
{container="api"}

# Filter level error
{container="api"} |= "error"

# Parse JSON, filter status 5xx
{container="api"} | json | status >= 500

# Lỗi của user cụ thể
{container="api"} | json | user_id="42" | level="error"

# Rate of error logs
sum(rate({container="api"} |= "error" [5m])) by (level)

# Trace 1 request qua nhiều service
{namespace="prod"} | json | request_id="a3f..."

ELK setup nhanh

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
    environment:
      discovery.type: single-node
      xpack.security.enabled: 'false'
      ES_JAVA_OPTS: "-Xms1g -Xmx1g"
    ports: ["9200:9200"]
    volumes: [es-data:/usr/share/elasticsearch/data]

  kibana:
    image: docker.elastic.co/kibana/kibana:8.13.0
    ports: ["5601:5601"]
    environment:
      ELASTICSEARCH_HOSTS: http://elasticsearch:9200

  filebeat:
    image: docker.elastic.co/beats/filebeat:8.13.0
    user: root
    volumes:
      - ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro

volumes:
  es-data:

Khi nào dùng cái nào?

Chọn Loki nếu:

  • Đã có Grafana cho Prometheus → tận dụng UI
  • Storage cost quan trọng
  • Query đơn giản (filter, grep, count) đủ
  • Team nhỏ, không có chuyên gia Elasticsearch

Chọn ELK nếu:

  • Cần full-text search phức tạp (regex, wildcard, fuzzy)
  • Có audit/security log cần query 1+ năm
  • Team có RAM/storage budget
  • Cần Kibana visualization phong phú (geo map, ML anomaly)

Kết luận

Log aggregation đúng giảm MTTR (mean time to recover) từ giờ xuống phút. Loki + Grafana là default tốt cho team Kubernetes; ELK cho team cần search mạnh. Quan trọng nhất: structured JSON log + request_id correlation — đó là 80% giá trị, dù chọn stack nào. Đọc thêm Observability để kết hợp log với metric + trace.

Zalo