Grafana/Prometheus/Promtail/Loki/Tempo
监控栈文档:Grafana / Prometheus / Promtail / Loki / Tempo
loop-agent 的本地可观测性栈:链路(trace) + 日志(log) + 指标(metric) 三支柱统一在 Grafana 查询。Jaeger 已下线,由 Tempo 接管链路。
1. 组件与职责
| 组件 | 版本 | 职责 | 地址 | 容器内数据目录 |
|---|---|---|---|---|
| Grafana | 11.0.0 | 统一查询/可视化 UI(三种数据源) | http://localhost:3000 | /var/lib/grafana |
| Tempo | 2.5.0 | 链路(trace)后端,收 OTLP | 查询 :3200 / OTLP :4317,:4318 | /var/tempo |
| Loki | 2.9.8 | 日志(log)后端 | :3100 | /loki |
| Promtail | 2.9.8 | 日志采集, 推给 Loki | (无 UI) :9080 | /promtail |
| Prometheus | 2.52.0 | 指标(metric)后端,抓应用指标 | :9090 | /prometheus |
| Pushgateway | 1.9.0 | 短命 CLI 批处理任务推指标的中转 | :9091 | (无状态) |
角色一句话:Tempo 存链路、Loki 存日志、Prometheus 存指标、Promtail 送日志、Grafana 统一看。
2. 数据流图

3. 组件如何相互作用
- 应用 → Tempo(链路) :应用用 OpenTelemetry 走 OTLP gRPC 发到
localhost:4317(OTEL_EXPORTER_OTLP_ENDPOINT)。每个 span 带gen_ai.*(模型/token/prompt/completion)等属性;根 spanturn.N带run_id/session_id/user_id/model。trace_id 带年月日(见第 7 节)。 - 应用 → trace_archive → Promtail → Loki(日志) :应用把每步事件写成
./trace_archive/<run_id>.jsonl(每行一个 JSON 事件)。Promtail 挂载该目录、tail 新增行,解析 JSON 后把run_id、etype(事件类型)提为标签,推到 Loki 的/loki/api/v1/push。 - Kong → Prometheus(指标) :应用本身不推业务指标;有价值的是 Kong 的
/metrics(各 key 的请求数/延迟/带宽,按 consumer 拆分)。Prometheus 按prometheus.yml抓取。 - Grafana → 三个数据源 :Grafana 用 provisioning 预置了 Tempo / Loki / Prometheus 三个数据源(
observability/grafana/provisioning/datasources/),开箱即用。 - trace ↔ log 关联 :关联键是
run_id------它既是 Tempo 里turn.Nspan 的属性,又是 Loki 里日志的标签。Tempo 数据源配了tracesToLogsV2,在一条 trace 的根 span 上点 "Logs for this span" 会自动用{run_id="..."}跳到 Loki。
4. 快速部署
前置
-
Docker Desktop。
-
镜像(国内 Docker Hub 常拉不动,用 daocloud 镜像再 retag):
for i in grafana/tempo:2.5.0 grafana/loki:2.9.8 grafana/promtail:2.9.8 \ grafana/grafana:11.0.0 prom/prometheus:v2.52.0 prom/pushgateway:v1.9.0; do docker pull docker.m.daocloud.io/$i && docker tag docker.m.daocloud.io/$i $i done
启动
cd <repo>
# 只起监控组件(不启动没在用的 kong-dp-ai)
docker compose up -d tempo loki promtail prometheus grafana
# 应用把链路发到 Tempo:确认 .env
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# LOOP_TRACE_DIR=./trace_archive
# 启动应用
uv run --env-file .env python -m service.app
验证
docker compose ps # 组件是否 Up
curl -s http://localhost:3200/ready # Tempo ready
curl -s http://localhost:3100/ready # Loki ready
curl -s "http://localhost:3200/api/search?q=%7B%7D&limit=3" # 有没有 trace
curl -s -G "http://localhost:3100/loki/api/v1/query_range" \
--data-urlencode 'query={service="catalystm-loop-agent"}' # 有没有 log
停止 / 清理
docker compose stop tempo loki promtail prometheus grafana # 停,保留数据
docker compose down -v # 停并删卷(清空数据,谨慎)
5. 数据持久化
本地(当前这套)
用 docker 具名卷 落盘,stop/restart 不丢数据:
| 卷 | 组件 | 存什么 | 保留期 |
|---|---|---|---|
| tempo-data | Tempo | trace 块(parquet) + WAL | 48h(block_retention) |
| loki-data | Loki | 日志 chunks + 索引(boltdb-shipper) | 30 天(retention_period) |
| prometheus-data | Prometheus | TSDB 块 | 15 天(默认) |
| grafana-data | Grafana | SQLite(仪表盘/设置) | 永久 |
| promtail-positions | Promtail | 读取位点(读到哪) | --- |
保留期各自在 config 里调:observability/tempo/tempo.yaml、observability/loki/loki-config.yaml、Prometheus 启动参数 --storage.tsdb.retention.time。
备份某个卷:
docker run --rm -v catalystm-loop-agent_loki-data:/d -v "$PWD/backup":/b \
alpine tar czf /b/loki-data.tgz -C /d .
生产上正常怎么存
- Tempo / Loki :把块和索引放对象存储(S3 / GCS / Azure Blob) ,本地只留 WAL。→ 天然持久、可水平扩展、成本低。Tempo 配
storage.trace.backend: s3|gcs|azure;Loki 配storage_config+schema_config指向对象存储(新版用 TSDB +common.storage.<object>)。 - Prometheus :数据盘用 PV/PVC ;长期存储 + 降采样用 remote_write → Thanos / Mimir / Cortex(底层也是对象存储)。
- Grafana :用外部数据库(Postgres/MySQL) 替代 SQLite,支持多副本共享;数据源/仪表盘用 provisioning 版本化(本仓库已这么做)。
- k8s:各组件挂 PVC,或直接用官方 Helm chart(Tempo/Loki 的 distributed 版 + 对象存储 backend)。
6. 如何使用
Grafana 匿名是 Viewer,看不到 Explore 。先右上角 Sign in → admin / admin。
6.1 查链路 trace(Explore → Tempo,TraceQL)
{ resource.service.name = "catalystm-loop-agent" } # 全部 agent 链路
{ span.run_id = "run-20260717-151046-141652900-20f4b8" } # 某次 run
{ span.session_id = "sess-20260717-..." } # 某个会话(多轮)
{ name = "llm.chat" && span.gen_ai.request.model = "gpt-4o" } # 某模型的 LLM 调用
{ span.gen_ai.usage.output_tokens > 500 } # 大输出的调用
{ name =~ "tool\\..*" } # 所有工具检索
点开 trace 看瀑布图 → 点 llm.chat span → 右侧看 model / tier / input/output tokens / prompt / completion。

6.2 查日志 log(Explore → Loki,LogQL)
{service="catalystm-loop-agent"} # 全部
{run_id="run-20260717-151046-141652900-20f4b8"} # 某次 run 全过程
{run_id="run-...", etype="decide"} # 只看 decide 事件
{service="catalystm-loop-agent"} |= "美妆" # 全文过滤
{service="catalystm-loop-agent"} | json | line_format "{{.type}} {{.data}}" # 解析 JSON
标签:run_id(带日期的 run 号)、etype(plan/tool/observe/reflect/decide/...)、service、filename。

6.3 查指标 metric(Explore → Prometheus,PromQL)
kong_http_requests_total{service="llm-azure-openai"} # 各路由请求数
sum by (consumer) (kong_http_requests_total) # 各 key(consumer)消耗
histogram_quantile(0.95, sum by (le) (rate(kong_request_latency_ms_bucket[5m]))) # p95 延迟
6.4 trace ↔ log 互跳
- trace → log :Tempo 里打开 trace 的根 span
turn.N(带 run_id)→ "Logs for this span" → 自动{run_id="..."}跳 Loki。 - log → trace :Loki 日志行里点
run_id派生链接,或复制 run_id 去 Tempo 查{ span.run_id = "..." }。
7. trace_id / run_id 约定
- OTEL trace_id (Tempo 里显示,32 hex)带年月日:
YYYYMMDD(8)+HHMMSS(6)+微秒(6)+随机(12),如2026071715104614165201d9ff16e960。每会话稳定不变。 - run_id / session_id (业务号,trace_archive 文件名 / Loki 标签):
run-YYYYMMDD-HHMMSS-<ns>-<rand>。 - 三者时间戳一致,可按时间对齐。
8. 配置文件位置
docker-compose.yml # 所有服务定义 + 卷
prometheus.yml # Prometheus 抓取配置
observability/
tempo/tempo.yaml # Tempo
loki/loki-config.yaml # Loki
promtail/promtail-config.yaml # Promtail(采集 ./trace_archive)
grafana/provisioning/datasources/{tempo,loki,prometheus}.yml # Grafana 数据源(含关联)
dashboards/ # 预置仪表盘(挂载)
9. 排查
docker compose ps # 谁没起来
docker logs catalystm-loop-agent-promtail-1 # promtail 有没有在推
docker logs catalystm-loop-agent-tempo-1
- Explore 里没有 Tempo/Loki → 先登录 admin/admin(Viewer 看不到 Explore)。
- Loki 查不到某次 run → 确认
./trace_archive/<run>.jsonl存在,且LOOP_TRACE_DIR=./trace_archive(promtail 挂的就是这个目录)。 - Tempo 刚跑完查不到 → 等 10--30 秒(ingester 切块后才可搜)。
- 拉不到镜像 → 用
docker.m.daocloud.io/<image>前缀再 retag(Docker Hub 在国内常 EOF)。