Lesson 0009 学了 Prometheus + Grafana 做指标监控——你知道 CPU 飙升了。但为什么飙升?哪个请求触发的?哪个函数不响应了?答案是日志。
Metrics(指标)告诉你发生了什么——"CPU 90%"
Logs(日志)告诉你为什么发生——"handling POST /api/upload, file=400MB.zip"
两者是监控的左右手。缺一个,你就是在盲飞。
你现在查日志的方式:
ssh server1
tail -f /var/log/nginx/access.log
# Ctrl+C
ssh server2
tail -f /var/log/nginx/error.log
# ... 已经过去 5 分钟了
问题:
解决方案:集中式日志系统——所有服务器的日志汇集到一个地方,一个搜索框查全部。
ELK(Elasticsearch + Logstash + Kibana)是最知名的日志方案。但 Elasticsearch 是内存怪兽——至少 4GB 起步。你的 2GB VPS 跑不动。
Loki(Grafana Labs 出品)是为云原生设计的轻量替代:
| 组件 | 作用 | 类比 |
|---|---|---|
| Promtail | 日志采集 agent,装在每台服务器/每个 Pod 上,收集日志发给 Loki | Node Exporter(但收日志而非指标) |
| Loki | 日志存储和查询引擎,只索引元数据(标签),不索引日志全文→超省资源 | Prometheus(但存日志而非指标) |
| Grafana | 查询和可视化界面——你已经在 0009 里用了 | 同一个 Grafana,切到 Explore 页就能查日志 |
应用/Nginx/Docker ──→ Promtail ──→ Loki ──→ Grafana Explore
(产生日志) (采集+加标签) (存储+查询) (搜索+可视化)
用 Docker Compose 在你的 VPS 上一键部署:
mkdir -p /opt/loki && cd /opt/loki
# 1. Loki 配置
cat > loki-config.yaml << 'EOF'
auth_enabled: false
server:
http_listen_port: 3100
ingester:
lifecycler:
ring:
kvstore:
store: inmemory
replication_factor: 1
chunk_idle_period: 5m
max_chunk_age: 1h
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: loki_index_
period: 24h
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-index
cache_location: /loki/tsdb-cache
filesystem:
directory: /loki/chunks
EOF
# 2. Promtail 配置——采集 Docker 容器的日志
cat > promtail-config.yaml << 'EOF'
server:
http_listen_port: 9080
grpc_listen_port: 0
clients:
- url: http://loki:3100/loki/api/v1/push # 发给 Loki
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
- source_labels: [__meta_docker_container_image]
target_label: image
pipeline_stages: # 解析时间戳
- docker: {}
- job_name: system
static_configs:
- targets: [localhost]
labels:
job: varlogs
__path__: /var/log/*.log
EOF
# 3. Docker Compose
cat > docker-compose.yml << 'EOF'
services:
loki:
image: grafana/loki:latest
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
promtail:
image: grafana/promtail:latest
volumes:
- ./promtail-config.yaml:/etc/promtail/config.yml
- /var/run/docker.sock:/var/run/docker.sock
- /var/log:/var/log:ro
command: -config.file=/etc/promtail/config.yml
volumes:
loki_data:
EOF
docker compose up -d
# 验证
curl http://localhost:3100/ready
# 输出: Ready
你已经在 0009 里部署了 Grafana。现在给它加上 Loki 数据源:
# Grafana 加 Loki 数据源
# 方法一:Web UI(推荐)
# 打开 Grafana → Connections → Data sources → Add → Loki
# URL: http://loki:3100
# Save & test
# 方法二:API(自动化用)
curl -X POST http://admin:admin@localhost:3000/api/datasources \
-H 'Content-Type: application/json' \
-d '{"name":"Loki","type":"loki","url":"http://loki:3100","access":"proxy"}'
然后在 Grafana → Explore → 选 Loki 数据源 → 输入查询:
{container=~".*"} # 查所有 Docker 容器的日志
{container="nginx"} # 只看 nginx 容器
{container="app"} |= "error" # app 容器日志中包含 "error" 的
{container="app"} |= "POST" | json # 解析 JSON 日志,提取字段
Loki 的查询语言叫 LogQL:{标签选择器} | 过滤 | 解析。语法类似 PromQL,学过 0009 的话上手只需要 5 分钟。
这是 DevOps 面试和实际工作中最关键的一个概念。
糟糕的日志(你肯定写过):
console.log("User " + userId + " logged in at " + new Date())
// → "User 42 logged in at Sun Jul 19 2026 15:23:01"
问题:你想查"今天所有 userId=42 的操作"——做不到。你要正则匹配 "User 42",漏掉所有换行、时间格式差异。
结构化日志:日志写成 JSON,每个字段都是可查询的维度:
console.log(JSON.stringify({
level: "info",
userId: 42,
action: "login",
ip: "1.2.3.4",
timestamp: new Date().toISOString(),
message: "User logged in"
}))
// → {"level":"info","userId":42,"action":"login","ip":"1.2.3.4","timestamp":"2026-07-19T07:23:01Z","message":"User logged in"}
然后在 Loki 里可以这样查:
{container="app"} | json | userId=42 # 查所有 userId=42 的日志
{container="app"} | json | level="error" # 只看错误
{container="app"} | json | action="payment" | line_format "{{.message}}" # 只看 payment 的消息
结构化日志 = 把日志从"需要 Ctrl+F"升级到"可以 SQL 查询"。
任务:用 Docker Compose 起一个 Node.js 应用 + Loki + Promtail,在 Grafana 里搜到结构化日志。
cd /tmp && mkdir log-demo && cd log-demo
# 1. Node.js 应用——输出结构化日志
cat > server.js << 'EOF'
const http = require('http');
function log(level, msg, extra = {}) {
const entry = {
level,
message: msg,
timestamp: new Date().toISOString(),
...extra
};
console.log(JSON.stringify(entry)); // 注意:console.log 输出到 stdout
}
http.createServer((req, res) => {
log('info', 'request received', {
method: req.method,
path: req.url,
userAgent: req.headers['user-agent']?.slice(0, 40)
});
if (req.url === '/error') {
log('error', 'simulated error', { path: '/error', code: 'E_SIMULATED' });
res.writeHead(500);
return res.end('Internal error');
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('OK. Visit /error to trigger a log line.\n');
}).listen(3000, () => log('info', 'server started', { port: 3000 }));
EOF
# 2. 极简 Dockerfile
cat > Dockerfile << 'EOF'
FROM node:22-alpine
WORKDIR /app
COPY server.js .
CMD ["node", "server.js"]
EOF
# 3. 本地 Docker Compose——加 Loki + Promtail
cat > docker-compose.yml << 'EOF'
services:
app:
build: .
ports: ["3000:3000"]
container_name: log-demo-app
loki:
image: grafana/loki:latest
ports: ["3100:3100"]
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml
- loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml
promtail:
image: grafana/promtail:latest
volumes:
- ./promtail-config.yaml:/etc/promtail/config.yml
- /var/run/docker.sock:/var/run/docker.sock
command: -config.file=/etc/promtail/config.yml
volumes:
loki_data:
EOF
# 4. Loki 配置(同上,简化版)
cat > loki-config.yaml << 'EOF'
auth_enabled: false
server: { http_listen_port: 3100 }
ingester:
lifecycler: { ring: { kvstore: { store: inmemory }, replication_factor: 1 } }
chunk_idle_period: 5m
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index: { prefix: loki_index_, period: 24h }
storage_config:
tsdb_shipper: { active_index_directory: /loki/tsdb-index, cache_location: /loki/tsdb-cache }
filesystem: { directory: /loki/chunks }
EOF
cat > promtail-config.yaml << 'EOF'
server: { http_listen_port: 9080, grpc_listen_port: 0 }
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
EOF
# 5. 启动
docker compose up -d
# 6. 产生一些日志
curl http://localhost:3000
curl http://localhost:3000/error
curl http://localhost:3000?user=42
# 多跑几次
# 7. 在 Loki 里查(直接用 curl 调用 Loki API)
curl -s 'http://localhost:3100/loki/api/v1/query_range' \
--data-urlencode 'query={container="log-demo-app"}' \
--data-urlencode 'limit=10' | python3 -m json.tool | head -40
# 看到你的结构化日志了吗?每条都是 JSON,带着 level、method、path 等字段
{container="log-demo-app"} | json | level="error" 搜到那条 error 日志。| Loki | ELK | |
|---|---|---|
| 适用场景 | 容器/云原生环境,配合 Prometheus | 全文搜索需求强,日志量极巨大 |
| 资源消耗 | ~200MB 内存起步 | 4GB+ 内存起步 |
| 全文搜索 | 可以,但需要扫描(慢) | 索引全文,秒级搜索 |
| 学习曲线 | 低(LogQL ≈ PromQL) | 高(Elasticsearch DSL) |
结论:90% 的场景 Loki 够用。当你的日志量达到每天 TB 级、需要毫秒级全文搜索时,再考虑 ELK。
下一课是课程体系的收尾:GitOps——ArgoCD(0019)。你会把 CI/CD(0006)+ K8s(0014–0016)+ Git(0003)串在一起,实现 Git push → 自动部署到 K8s 的完整自动化。