Lesson 0018:日志系统——Prometheus 告诉你起火了,日志告诉你为什么

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 分钟了

问题:

解决方案:集中式日志系统——所有服务器的日志汇集到一个地方,一个搜索框查全部。

Loki:轻量级日志系统

ELK(Elasticsearch + Logstash + Kibana)是最知名的日志方案。但 Elasticsearch 是内存怪兽——至少 4GB 起步。你的 2GB VPS 跑不动。

Loki(Grafana Labs 出品)是为云原生设计的轻量替代:

组件作用类比
Promtail日志采集 agent,装在每台服务器/每个 Pod 上,收集日志发给 LokiNode Exporter(但收日志而非指标)
Loki日志存储和查询引擎,只索引元数据(标签),不索引日志全文→超省资源Prometheus(但存日志而非指标)
Grafana查询和可视化界面——你已经在 0009 里用了同一个 Grafana,切到 Explore 页就能查日志
应用/Nginx/Docker  ──→  Promtail  ──→  Loki  ──→  Grafana Explore
  (产生日志)          (采集+加标签)   (存储+查询)   (搜索+可视化)
Loki 的设计哲学:像 Prometheus 但用于日志。日志内容不索引(省空间),只索引标签(类似 Prometheus 的 labels)。查询时用标签缩小范围 + 暴力扫描匹配内容。

第一部分:部署 Loki + Promtail(15 分钟)

用 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

第二部分:Grafana 查日志——和查 PromQL 一样的体验(10 分钟)

你已经在 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 分钟。

第三部分:结构化日志——让日志可搜索(10 分钟)

这是 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 查询"。

生产环境的日志三原则:1. 永远 JSON(不是纯文本)2. 永远带 level(debug/info/warn/error)3. 永远带 traceId(串联一次请求的所有日志)。这些是面试 + 实际工作的高频考点。

动手练习:让你的应用输出结构化日志

任务:用 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 等字段
你的任务:把上面的 log-demo 跑通,用 Loki API 查到 error 级别的日志。
加分题:如果 VPS 上已有 Grafana(0009 装的),加 Loki 数据源,在 Explore 里用 {container="log-demo-app"} | json | level="error" 搜到那条 error 日志。

Loki vs ELK——什么时候升级?

LokiELK
适用场景容器/云原生环境,配合 Prometheus全文搜索需求强,日志量极巨大
资源消耗~200MB 内存起步4GB+ 内存起步
全文搜索可以,但需要扫描(慢)索引全文,秒级搜索
学习曲线低(LogQL ≈ PromQL)高(Elasticsearch DSL)

结论:90% 的场景 Loki 够用。当你的日志量达到每天 TB 级、需要毫秒级全文搜索时,再考虑 ELK。

小测验

Q1:Metrics(指标)和 Logs(日志)的分工是什么?
Q2:Loki 相比 Elasticsearch 的核心设计差异是什么?
Q3:结构化日志最重要的三个字段是?

推荐资源

下一步

下一课是课程体系的收尾:GitOps——ArgoCD(0019)。你会把 CI/CD(0006)+ K8s(0014–0016)+ Git(0003)串在一起,实现 Git push → 自动部署到 K8s 的完整自动化。

Logs + Metrics + Traces = 可观测性三支柱。你现在会了前两个(Prometheus + Loki)。Tracing(Jaeger/Tempo)留到进阶再学,面试和日常工作中 metrics + logs 已经覆盖 90% 的场景。
← Lesson 0017 · 术语表 · Mission