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 Stack | ELK Stack | |
|---|---|---|
| Storage | Loki (chunk + label index) | Elasticsearch (full-text index) |
| Shipper | Promtail / Fluent Bit | Filebeat / Logstash |
| UI | Grafana (LogQL) | Kibana (KQL/Lucene) |
| Index storage | Chỉ label — nhỏ | Full content — lớn |
| RAM yêu cầu | ~1GB cho 100GB log | ~10-30GB cho 100GB log |
| Query | Filter + grep | Full-text + complex |
| Cost | Rẻ (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.