719 lines
29 KiB
Markdown
719 lines
29 KiB
Markdown
# 局域网服务日志与可观测性实施计划
|
||
|
||
- 状态:待实施
|
||
- 编写日期:2026-07-22
|
||
- 代码基线:`v0.2.0` / `04e80b1`
|
||
- 目标部署机:`192.168.193.237`(实施时必须重新确认)
|
||
- 当前 API:`http://192.168.193.237:39180`
|
||
|
||
## 新会话启动方式
|
||
|
||
新开 Codex 会话后,先发送下面的提示词:
|
||
|
||
```text
|
||
请完整阅读 docs/observability-implementation-plan.md、DEPLOY_5700U.md、
|
||
compose.deploy.yml、server/src/main.rs、server/src/transcription_worker.rs、
|
||
asr-service/app.py 和 utils/api.ts。
|
||
|
||
把 observability-implementation-plan.md 作为本次任务的执行约束。
|
||
先检查 git、目标部署机、Docker 日志驱动、磁盘和现有容器状态,再从文档中
|
||
第一个未完成的阶段开始实施。每完成一个阶段就运行该阶段验收,不要跳过日志
|
||
脱敏、请求 ID、持久化、回滚验证或文档更新。除非我明确要求,不要自行提交、
|
||
推送或修改目标机防火墙。
|
||
```
|
||
|
||
新会话开始后应先更新本文档顶部的代码基线、目标机 IP 和实际组件版本。本文中的
|
||
镜像版本、端口和资源阈值是实施起点,不得替代实施时的现场检查。
|
||
|
||
## 1. 目标
|
||
|
||
本计划要建立一条可从小程序故障追溯到具体代码版本的日志链路:
|
||
|
||
```text
|
||
小程序请求
|
||
-> Rust API 结构化请求日志
|
||
-> 转录队列事件
|
||
-> FunASR 结构化推理日志
|
||
-> Docker 本地日志与轮转
|
||
-> Grafana Alloy 采集
|
||
-> Loki 持久化与检索
|
||
-> Grafana 查询、面板和告警
|
||
```
|
||
|
||
完成后应支持:
|
||
|
||
1. 任意可信局域网电脑通过浏览器访问 Grafana。
|
||
2. 按环境、Compose 项目、服务、容器、日志级别、时间和代码版本筛选日志。
|
||
3. 用 `request_id`、`feedback_session_id`、`lesson_id` 或 `segment_id` 关联 API 与 FunASR。
|
||
4. 判断故障发生在哪个镜像版本和 Git 提交,避免按错误版本修改代码。
|
||
5. 保存至少 14 天日志,并限制 Docker 原始容器日志占用。
|
||
6. 在 Loki/Grafana 不可用时,API 与 FunASR 仍可正常提供业务服务。
|
||
7. 后续可在同一 Grafana 中逐步加入日志告警和 Prometheus 指标。
|
||
|
||
## 2. 非目标
|
||
|
||
第一轮不做以下工作:
|
||
|
||
- 不部署 Kubernetes,也不把现有单机 Compose 改成集群。
|
||
- 不部署 Promtail;它已从当前 Loki 版本线移除,新部署使用 Alloy。
|
||
- 不在应用代码中直接调用 Loki API,应用只写标准输出和标准错误。
|
||
- 不在第一轮部署 Tempo、全链路追踪、Mimir 或 Elasticsearch。
|
||
- 不采集请求体、反馈正文、转录文本、音频内容、Bearer Token 或数据库连接串。
|
||
- 不把 Grafana、Loki、Alloy 或 Docker API 直接暴露到公网。
|
||
- 不用日志替代业务数据库、审计表或音频持久化卷。
|
||
|
||
## 3. 当前状态与缺口
|
||
|
||
### 3.1 已确认状态
|
||
|
||
- `compose.deploy.yml` 只包含 `funasr`、`migrate` 和 `api`。
|
||
- API 暴露宿主机 `39180`,FunASR 只在 Compose 内部网络提供 `10095`。
|
||
- 规划时 `GET /health` 返回健康,数据库、语音和微信鉴权均已配置。
|
||
- Rust API 已使用 `tracing` 和 `tracing-subscriber`,转录 worker 已记录部分
|
||
`worker_id`、`segment_id` 和错误事件。
|
||
- Rust 使用 `TraceLayer::new_for_http()`,但默认请求/响应事件为 `DEBUG`;镜像当前
|
||
`RUST_LOG=info`,不能依赖它提供完整访问日志。
|
||
- Rust 日志当前是文本格式,没有稳定 JSON 字段、请求 ID 或代码版本字段。
|
||
- FunASR 使用 Python `logging`,但 Uvicorn 通过 `--no-access-log` 关闭访问日志;当前主要
|
||
记录模型启动和推理异常,没有成功请求耗时。
|
||
- Compose 没有服务级 `logging` 配置。若目标机未修改 Docker daemon,通常会使用默认
|
||
`json-file` 且没有自动轮转,存在磁盘耗尽风险。
|
||
- `compose.deploy.yml` 中 API 镜像仍标记为 `teaching-feedback-api:0.1.0`,与当前
|
||
`v0.2.0` 不一致。
|
||
- 仓库没有 Loki、Alloy、Grafana、日志持久化卷、数据源自动配置或日志运行手册。
|
||
|
||
### 3.2 必须解决的问题
|
||
|
||
| 问题 | 影响 | 解决阶段 |
|
||
| --- | --- | --- |
|
||
| Docker 日志无显式轮转 | 宿主机磁盘可能被占满 | 阶段 1 |
|
||
| API 请求日志不完整 | 无法定位具体失败请求 | 阶段 2 |
|
||
| API 与 ASR 缺少统一关联 ID | 跨服务排障依赖时间猜测 | 阶段 2 |
|
||
| 日志没有版本元数据 | 无法映射到正确代码 | 阶段 1、2 |
|
||
| 日志仅保存在单机 Docker 中 | 其他电脑无法方便检索历史 | 阶段 3 |
|
||
| 没有保留期和持久化验证 | 重启或磁盘增长不可控 | 阶段 3 |
|
||
| 没有基于实际基线的告警 | 故障只能人工发现 | 阶段 4 |
|
||
| 没有时序指标 | 无法可靠发现“没有日志”的故障 | 阶段 5 |
|
||
|
||
## 4. 目标架构
|
||
|
||
应用栈和可观测性栈使用两份 Compose 文件独立管理:
|
||
|
||
```text
|
||
compose.deploy.yml compose.observability.yml
|
||
|
||
api -------- stdout/stderr ----+ alloy ----> loki ----> grafana:3000
|
||
funasr ----- stdout/stderr -----+ | | |
|
||
migrate ---- stdout/stderr -----+--------+ loki-data grafana-data
|
||
|
|
||
Docker Engine API
|
||
/var/run/docker.sock
|
||
```
|
||
|
||
约束:
|
||
|
||
- `api`、`funasr`、`migrate` 不依赖 Loki、Alloy 或 Grafana 启动。
|
||
- Alloy 通过 Docker Engine API 发现带指定标签的容器并读取日志。
|
||
- Loki 与 Alloy 不发布宿主机端口。
|
||
- 只有 Grafana 发布到可信局域网,建议默认使用 `${GRAFANA_PORT:-39300}:3000`。
|
||
- 局域网外访问使用 WireGuard、Tailscale 或 SSH 隧道,不做公网裸端口。
|
||
- Grafana 必须启用登录;禁止匿名访问。
|
||
- Loki 初始使用单体模式、TSDB 索引和本地文件系统卷,接受单机无高可用的现实。
|
||
- Loki 默认保留 14 天;稳定运行并确认磁盘余量后再考虑 30 天。
|
||
|
||
## 5. 日志字段规范
|
||
|
||
### 5.1 公共字段
|
||
|
||
Rust API 和 FunASR 每条应用日志尽量使用一致字段:
|
||
|
||
| 字段 | 示例 | 说明 |
|
||
| --- | --- | --- |
|
||
| `timestamp` | `2026-07-22T02:00:00.123Z` | RFC 3339 UTC 时间 |
|
||
| `level` | `INFO` | `TRACE/DEBUG/INFO/WARN/ERROR` |
|
||
| `service` | `api` | `api`、`funasr` 或 `migrate` |
|
||
| `environment` | `lan` | 固定低基数字段 |
|
||
| `app_version` | `0.2.0` | 发行版本 |
|
||
| `git_sha` | `04e80b1` | 构建或部署提交 |
|
||
| `event` | `http_request_finished` | 稳定、可查询的事件名 |
|
||
| `request_id` | UUID | 单次 HTTP 请求关联 ID,不作为 Loki 标签 |
|
||
| `message` | 简短英文或中文 | 人类可读说明 |
|
||
|
||
容器、Compose 项目、Compose 服务、镜像等字段由 Alloy 从 Docker 元数据补充。
|
||
|
||
### 5.2 API 请求字段
|
||
|
||
- `method`
|
||
- `route`:优先路由模板,例如 `/api/v1/profiles/{profile_id}`
|
||
- `path`:必要时保留原始路径,但不得包含敏感 query 参数
|
||
- `status`
|
||
- `latency_ms`
|
||
- `request_id`
|
||
- `error_class`:只在失败时记录稳定分类
|
||
|
||
不要记录完整请求头。尤其禁止记录 `Authorization`、Cookie、微信 code、AppSecret 和
|
||
上传内容。
|
||
|
||
### 5.3 语音链路字段
|
||
|
||
- `feedback_session_id`
|
||
- `lesson_id`
|
||
- `segment_id`
|
||
- `asr_request_id`
|
||
- `worker_id`
|
||
- `audio_format`
|
||
- `audio_size_bytes`
|
||
- `audio_duration_ms`
|
||
- `queue_wait_ms`
|
||
- `inference_latency_ms`
|
||
- `provider`
|
||
- `attempt`
|
||
- `status`
|
||
|
||
禁止记录本地音频绝对路径、热词原文、转录文字或反馈正文。
|
||
|
||
### 5.4 Loki 标签规范
|
||
|
||
仅将低基数字段设为 Loki 标签:
|
||
|
||
- `environment`
|
||
- `compose_project`
|
||
- `compose_service`
|
||
- `container`
|
||
- `service`
|
||
- `level`
|
||
- `app_version`
|
||
|
||
以下字段只能保留在 JSON 日志正文中,通过 `| json` 查询,不能成为标签:
|
||
|
||
- `request_id`
|
||
- 用户 ID、学生 ID、会话 ID、课节 ID、片段 ID
|
||
- URL 原始路径
|
||
- 错误消息
|
||
|
||
## 6. 计划新增或修改的文件
|
||
|
||
| 文件 | 动作 | 目的 |
|
||
| --- | --- | --- |
|
||
| `compose.deploy.yml` | 修改 | 日志轮转、容器筛选标签、版本环境变量、镜像版本 |
|
||
| `server/Cargo.toml` | 修改 | 开启 `tracing-subscriber/json` 和 `tower-http/request-id` |
|
||
| `server/src/main.rs` | 修改 | JSON 日志、请求 ID、INFO 请求完成日志、启动版本日志 |
|
||
| `server/src/config.rs` | 修改 | 读取 `LOG_FORMAT`、`APP_VERSION`、`GIT_SHA`、`APP_ENV` |
|
||
| `server/src/transcription_worker.rs` | 修改 | 增加队列等待、尝试次数、ASR 耗时和关联字段 |
|
||
| `server/src/speech.rs` | 修改 | 将 `segment_id`/请求 ID 传给 FunASR |
|
||
| `server/.env.example` | 修改 | 记录非敏感日志配置项 |
|
||
| `utils/api.ts` | 修改 | 保存响应 `X-Request-ID` 到 `ApiRequestError` |
|
||
| 相关小程序错误状态 | 修改 | 在可复制位置显示请求编号,不塞入短 Toast |
|
||
| `asr-service/app.py` | 修改 | 请求 ID 中间件、成功/失败耗时和结构化字段 |
|
||
| `asr-service/logging_config.py` | 新增 | 标准库 JSON formatter 与日志初始化 |
|
||
| `asr-service/Dockerfile` | 修改 | 传入版本环境,继续避免重复 Uvicorn access log |
|
||
| `compose.observability.yml` | 新增 | Alloy、Loki、Grafana 三个服务 |
|
||
| `observability/alloy/config.alloy` | 新增 | Docker 发现、筛选、标签、转发配置 |
|
||
| `observability/loki/loki.yml` | 新增 | 单体 TSDB、文件系统、保留期配置 |
|
||
| `observability/grafana/provisioning/datasources/loki.yml` | 新增 | 自动创建 Loki 数据源 |
|
||
| `observability/grafana/provisioning/dashboards/` | 后续新增 | 阶段 4 面板和告警 |
|
||
| `observability/.env.example` | 新增 | 端口、版本、管理员账号变量说明,不含密码 |
|
||
| `.gitignore` | 修改 | 忽略 `observability/.env`、本地数据和导出文件 |
|
||
| `docs/observability-runbook.md` | 新增 | 启停、查询、备份、升级、故障处理 |
|
||
| `DEPLOY_5700U.md` | 修改 | 把日志栈加入部署和验收流程 |
|
||
|
||
## 7. 分阶段实施
|
||
|
||
### 阶段 0:现场基线与风险检查
|
||
|
||
目标:在修改配置前记录目标机真实状态。
|
||
|
||
在目标部署机执行:
|
||
|
||
```bash
|
||
cd /path/to/teaching-feedback-assistant
|
||
git status --short --branch
|
||
git rev-parse HEAD
|
||
git tag --points-at HEAD
|
||
|
||
docker version
|
||
docker compose version
|
||
docker info --format '{{.LoggingDriver}}'
|
||
docker compose -f compose.deploy.yml ps
|
||
docker compose -f compose.deploy.yml images
|
||
docker compose -f compose.deploy.yml logs --since 1h api funasr
|
||
docker inspect teaching-feedback-api-1 --format '{{json .HostConfig.LogConfig}}' || true
|
||
|
||
df -h
|
||
docker system df
|
||
timedatectl status
|
||
ss -lntp | grep -E ':(39180|39300|3100|12345)\b' || true
|
||
```
|
||
|
||
实施要求:
|
||
|
||
1. 不输出 `server/.env` 内容。
|
||
2. 记录 Docker 当前日志驱动、容器名、镜像 ID、重启次数和日志占用。
|
||
3. 确认 `39300` 是否可用;冲突时选择其他端口并更新本文档。
|
||
4. 确认至少有 10 GB 可用空间给日志卷;不足时先清理或降低保留期。
|
||
5. 确认目标机时钟同步。日志统一为 UTC,Grafana 按浏览器时区显示。
|
||
6. 确认 API、FunASR 和真实转录链路在改造前正常。
|
||
|
||
验收:把基线命令和非敏感结果写入实施记录,任何失败先解释,不进入阶段 1。
|
||
|
||
### 阶段 1:Docker 日志轮转与版本元数据
|
||
|
||
目标:即使集中日志尚未部署,容器日志也不能无限增长。
|
||
|
||
在 `compose.deploy.yml` 增加可复用配置:
|
||
|
||
```yaml
|
||
x-default-logging: &default-logging
|
||
driver: local
|
||
options:
|
||
max-size: "20m"
|
||
max-file: "5"
|
||
```
|
||
|
||
对 `funasr`、`migrate` 和 `api` 使用该配置。优先使用服务级配置,不修改 Docker daemon
|
||
全局默认,避免影响目标机的其他项目。
|
||
|
||
同时增加:
|
||
|
||
```yaml
|
||
labels:
|
||
observability.logs: "true"
|
||
observability.environment: "lan"
|
||
environment:
|
||
APP_ENV: lan
|
||
APP_VERSION: ${APP_VERSION:-0.2.0}
|
||
GIT_SHA: ${GIT_SHA:-unknown}
|
||
LOG_FORMAT: ${LOG_FORMAT:-json}
|
||
```
|
||
|
||
要求:
|
||
|
||
1. API 镜像标签不得继续硬编码为 `0.1.0`。
|
||
2. 实施时从 Git tag/commit 注入 `APP_VERSION` 和 `GIT_SHA`。
|
||
3. 镜像版本必须固定,不使用 `latest`。
|
||
4. `local` 日志驱动的配置值必须是字符串。
|
||
5. 修改日志驱动后必须重建容器;仅 restart 不会更新已有容器的日志驱动。
|
||
|
||
验证:
|
||
|
||
```bash
|
||
docker compose -f compose.deploy.yml config
|
||
docker compose -f compose.deploy.yml up -d --build --force-recreate
|
||
docker compose -f compose.deploy.yml ps
|
||
docker compose -f compose.deploy.yml logs --since 10m api funasr
|
||
docker inspect <api-container> --format '{{json .HostConfig.LogConfig}}'
|
||
docker inspect <funasr-container> --format '{{json .Config.Labels}}'
|
||
curl -fsS http://127.0.0.1:39180/health
|
||
```
|
||
|
||
阶段 1 完成标准:三个服务使用受控日志驱动,API 健康且真实转录仍成功。
|
||
|
||
### 阶段 2:结构化请求日志与跨服务关联
|
||
|
||
#### 2.1 Rust API
|
||
|
||
依赖调整:
|
||
|
||
```toml
|
||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||
tower-http = { version = "0.6", features = ["cors", "trace", "request-id"] }
|
||
```
|
||
|
||
实现要求:
|
||
|
||
1. `LOG_FORMAT=json` 时使用 `tracing_subscriber::fmt().json()`,关闭 ANSI,并保留当前 span。
|
||
2. 本地开发允许 `LOG_FORMAT=pretty`,但生产 Compose 固定 `json`。
|
||
3. 使用 `tower_http::request_id`:
|
||
- 接受合法的客户端 `X-Request-ID`。
|
||
- 缺失时用 `MakeRequestUuid` 生成。
|
||
- 在响应头中传播同一个 `X-Request-ID`。
|
||
4. 中间件顺序按 tower-http 要求设置:先 Set Request ID,再 Trace,最后 Propagate。
|
||
5. `TraceLayer` 的请求开始、请求结束和失败级别显式设置为 `INFO/ERROR`,不依赖默认
|
||
`DEBUG`。
|
||
6. `make_span_with` 不记录 headers;记录 `method`、`route/path` 和 `request_id`。
|
||
7. `on_response` 记录 `status` 和 `latency_ms`。
|
||
8. 5xx/超时记录 `ERROR`,可预期 4xx 不得全部当成系统错误。
|
||
9. 启动时记录 `service`、`environment`、`app_version`、`git_sha`、监听地址和语音提供方。
|
||
10. 不在日志中输出 `DATABASE_URL`、微信密钥、Summary API Key 或任何 Token。
|
||
|
||
补充单元/集成测试:
|
||
|
||
- 无请求 ID 时响应包含合法 UUID。
|
||
- 带合法请求 ID 时响应原样传播。
|
||
- 日志中不包含 Authorization 值。
|
||
- 200、400、401、500 都产生一次完成事件。
|
||
- JSON 日志每行可被 `jq` 解析。
|
||
|
||
#### 2.2 转录 worker 与 Rust -> FunASR
|
||
|
||
1. 在 claim job 时取得 `processing_started_at`、`transcription_attempts` 等必要字段。
|
||
2. 以 `segment_id` 作为跨 API/ASR 的稳定业务关联字段。
|
||
3. Rust 请求 FunASR 时设置 `X-Request-ID`;可直接使用 `segment_id`,或生成新的
|
||
`asr_request_id` 并同时记录两者。
|
||
4. 记录排队等待、音频读取、HTTP 调用、推理完成和数据库更新的分段耗时。
|
||
5. 成功日志不得包含 transcript;失败日志只记录经过清理的错误分类和消息。
|
||
|
||
#### 2.3 FunASR
|
||
|
||
1. 新增标准 JSON formatter,不因日志系统引入另一套应用框架。
|
||
2. 中间件读取/生成 `X-Request-ID`,并写回响应头。
|
||
3. 记录 `asr_request_started`、`asr_request_finished` 和 `asr_request_failed`。
|
||
4. 成功和失败均记录 `status`、`latency_ms`、`audio_size_bytes`、`audio_duration_ms`、
|
||
`audio_format` 和 `request_id`。
|
||
5. 保留 `--no-access-log`,避免 Uvicorn 默认访问日志和自定义日志重复。
|
||
6. 不记录临时路径、文件名、hotword、模型输出文本或上传内容。
|
||
|
||
#### 2.4 小程序错误编号
|
||
|
||
1. `ApiRequestError` 增加可选 `requestId`。
|
||
2. `wx.request` 与 `wx.uploadFile` 都从响应头读取 `X-Request-ID`,兼容响应头大小写。
|
||
3. 短 Toast 不塞入长 UUID;需要持久显示错误的状态区增加“请求编号”与复制入口。
|
||
4. 开发者控制台可以记录请求编号,但不得打印 Token、上传路径或请求正文。
|
||
|
||
阶段 2 验收:
|
||
|
||
```bash
|
||
cd server
|
||
cargo fmt --check
|
||
cargo test
|
||
cargo clippy --all-targets --all-features -- -D warnings
|
||
|
||
cd ..
|
||
npm run typecheck
|
||
docker compose -f compose.deploy.yml config
|
||
```
|
||
|
||
再用一个固定 `X-Request-ID` 调用 `/health` 和一个授权业务接口,确认 API 日志可按该 ID
|
||
检索。使用真实音频验证 `segment_id` 能关联 Rust 与 FunASR,且日志中没有转录正文。
|
||
|
||
### 阶段 3:部署 Alloy、Loki、Grafana
|
||
|
||
#### 3.1 版本选择
|
||
|
||
实施时查阅官方 release notes,选择互相兼容的稳定版本并固定完整版本号:
|
||
|
||
- `grafana/alloy:<version>`
|
||
- `grafana/loki:<version>`
|
||
- `grafana/grafana:<version>`
|
||
|
||
禁止使用 `latest`。将选定版本写入 `observability/.env.example` 和实施记录。
|
||
|
||
#### 3.2 Compose 约束
|
||
|
||
新增 `compose.observability.yml`,项目名建议为 `teaching-feedback-observability`。
|
||
|
||
`loki`:
|
||
|
||
- 单体模式。
|
||
- 配置文件只读挂载。
|
||
- `/var/loki` 使用 `loki-data` 持久化卷。
|
||
- 不发布 `3100` 到宿主机。
|
||
- 提供 readiness 健康检查。
|
||
- `restart: unless-stopped`。
|
||
|
||
`alloy`:
|
||
|
||
- 配置文件只读挂载。
|
||
- `/var/lib/alloy/data` 使用 `alloy-data`,保存 Docker 读取位置。
|
||
- 初始部署读取 `/var/run/docker.sock`。
|
||
- Docker Socket 挂载即使标记只读,也不是完整的 API 权限隔离;容器必须使用固定镜像、
|
||
最小权限且不对外开放 Alloy UI。安全要求提高时改为受限 Docker Socket Proxy。
|
||
- 不发布 Alloy `12345` 到宿主机。
|
||
- `restart: unless-stopped`。
|
||
|
||
`grafana`:
|
||
|
||
- `/var/lib/grafana` 使用 `grafana-data`。
|
||
- 默认发布 `${GRAFANA_PORT:-39300}:3000`。
|
||
- `GF_AUTH_ANONYMOUS_ENABLED=false`。
|
||
- 管理员密码仅放在未提交的 `observability/.env` 或 Docker Secret。
|
||
- 自动挂载 Loki 数据源 provisioning。
|
||
- `restart: unless-stopped`。
|
||
|
||
#### 3.3 Loki 配置
|
||
|
||
`observability/loki/loki.yml` 至少满足:
|
||
|
||
- `auth_enabled: false`,但 Loki 只存在于内部 Docker 网络。
|
||
- 单体 `target=all`。
|
||
- TSDB schema,不使用已弃用 BoltDB index。
|
||
- chunks、rules、WAL 和 compactor 目录全部落在 `/var/loki`。
|
||
- compactor retention 开启。
|
||
- 初始 `retention_period: 336h`(14 天)。
|
||
- 禁用匿名 usage analytics(若当前版本支持对应配置)。
|
||
- 设定合理 ingestion/query 限制,防止一次错误查询拖垮单机。
|
||
|
||
文件系统存储没有副本,也不会按磁盘剩余空间自动删除;保留期之外仍要监控磁盘。
|
||
|
||
#### 3.4 Alloy 配置
|
||
|
||
`observability/alloy/config.alloy` 应包含:
|
||
|
||
1. `discovery.docker` 连接 `unix:///var/run/docker.sock`。
|
||
2. `discovery.relabel` 只保留 `observability.logs=true` 的容器。
|
||
3. 从 Docker 元数据映射 `compose_project`、`compose_service`、`container`、`image`。
|
||
4. `loki.source.docker` 读取容器日志并保存 positions。
|
||
5. `loki.process` 添加 `environment=lan`,解析 JSON 日志级别;解析失败时保留原始行。
|
||
6. 不把 `request_id`、用户或业务 ID 提升为标签。
|
||
7. `loki.write` 发送到 `http://loki:3100/loki/api/v1/push`。
|
||
|
||
#### 3.5 Grafana 自动配置
|
||
|
||
Loki 数据源通过 provisioning 创建:
|
||
|
||
```yaml
|
||
apiVersion: 1
|
||
datasources:
|
||
- name: Loki
|
||
type: loki
|
||
access: proxy
|
||
url: http://loki:3100
|
||
isDefault: true
|
||
editable: false
|
||
```
|
||
|
||
首次只创建数据源和一个最小“日志浏览”面板,不提前加入未经验证的告警阈值。
|
||
|
||
#### 3.6 启动与验证
|
||
|
||
```bash
|
||
docker compose -f compose.observability.yml config
|
||
docker compose -f compose.observability.yml up -d
|
||
docker compose -f compose.observability.yml ps
|
||
docker compose -f compose.observability.yml logs --since 10m alloy loki grafana
|
||
curl -fsS http://127.0.0.1:39300/api/health
|
||
```
|
||
|
||
验收:
|
||
|
||
1. 浏览器可从另一台局域网电脑打开 `http://192.168.193.237:39300`。
|
||
2. 未登录不能查看日志。
|
||
3. Grafana Explore 能看到 `api` 和 `funasr`。
|
||
4. 以下查询可用,实际标签名以 Alloy 配置为准:
|
||
|
||
```logql
|
||
{compose_service="api"} | json
|
||
{compose_service="api"} | json | request_id="<request-id>"
|
||
{compose_service="funasr"} | json | segment_id="<segment-id>"
|
||
{compose_service="api"} | json | status >= 500
|
||
```
|
||
|
||
5. 重启 Alloy 后不大量重复采集旧日志。
|
||
6. 重启整套可观测性 Compose 后,Grafana 数据源、账号配置和 Loki 历史仍存在。
|
||
7. 停止 Loki/Alloy/Grafana 后,API 和 FunASR 继续正常工作。
|
||
8. Loki、Alloy 的宿主机端口不可访问。
|
||
|
||
### 阶段 4:基于实际日志建立面板与告警
|
||
|
||
先运行至少 3–7 天,观察正常流量、错误数量和转录耗时,再设置阈值。
|
||
|
||
首批面板:
|
||
|
||
- API 请求总量、4xx、5xx。
|
||
- API `latency_ms` 的 p50/p95/p99。
|
||
- 各路由错误日志列表。
|
||
- 转录成功、失败和重试数量。
|
||
- ASR 推理耗时和音频时长比值。
|
||
- `ASR service is unavailable` 与恢复事件。
|
||
- 按 `app_version` 对比错误率。
|
||
|
||
首批日志告警候选:
|
||
|
||
- 5 分钟内 API 5xx 达到 3 次。
|
||
- 10 分钟内出现转录失败。
|
||
- 5 分钟内持续出现数据库查询失败。
|
||
- ASR 不可用持续超过 5 分钟。
|
||
- Loki ingestion 或 Alloy target unhealthy。
|
||
|
||
告警要求:
|
||
|
||
1. 每条告警必须提供可直接打开的 Grafana 查询。
|
||
2. 配置 `for` 持续时间,避免瞬时启动日志触发。
|
||
3. 通知内容包含服务、环境、版本和时间范围,不包含用户数据。
|
||
4. 先发送到低风险通知渠道进行一周观察,再升级为正式告警。
|
||
5. 告警规则和 dashboard JSON 纳入 Git,禁止只在网页里手工保存。
|
||
|
||
Grafana 可直接基于 Loki 日志创建告警,但“服务完全无日志”“CPU/内存异常”“磁盘即将
|
||
耗尽”更适合指标,留到阶段 5。
|
||
|
||
### 阶段 5:按实际故障增加指标采集
|
||
|
||
日志稳定后再引入 Prometheus 指标。Alloy 可以抓取指标,但它不是长期指标存储;自托管
|
||
方案需要增加 Prometheus,或把指标 remote write 到已有兼容后端。
|
||
|
||
建议指标:
|
||
|
||
Rust API:
|
||
|
||
- `http_requests_total{route,method,status_class}`
|
||
- `http_request_duration_seconds{route,method}`
|
||
- `transcription_queue_jobs{status}`
|
||
- `transcription_job_duration_seconds`
|
||
- `transcription_job_retries_total`
|
||
- `database_pool_connections{state}`
|
||
- `asr_available`
|
||
|
||
FunASR:
|
||
|
||
- `asr_requests_total{status}`
|
||
- `asr_inference_duration_seconds`
|
||
- `asr_audio_duration_seconds`
|
||
- `asr_inflight_requests`
|
||
- `asr_failures_total{reason}`
|
||
|
||
宿主机与容器:
|
||
|
||
- node-exporter:CPU、内存、磁盘、负载。
|
||
- cAdvisor 或 Alloy Docker integration:容器 CPU、内存、重启和网络。
|
||
- `up`:API、FunASR、Loki、Alloy、Grafana 抓取状态。
|
||
|
||
指标标签禁止包含请求 ID、学生 ID、会话 ID 或原始 URL。路由必须使用模板路径,避免
|
||
高基数。
|
||
|
||
阶段 5 告警优先级:
|
||
|
||
1. 磁盘剩余小于 15%。
|
||
2. API/FunASR `up == 0` 持续 2 分钟。
|
||
3. 转录队列持续增长或最老任务等待超阈值。
|
||
4. API p95 延迟持续异常。
|
||
5. 容器反复重启或内存逼近限制。
|
||
|
||
只有在日志和指标仍无法解释跨服务延迟时,才评估 OpenTelemetry + Tempo tracing。
|
||
|
||
## 8. 安全与隐私要求
|
||
|
||
### 禁止进入日志
|
||
|
||
- `Authorization`、Cookie、微信登录 code、session key。
|
||
- `WECHAT_APP_SECRET`、数据库密码、完整 `DATABASE_URL`。
|
||
- Summary/LLM API Key。
|
||
- 学生姓名、家长称呼、反馈正文、转录全文、hotword。
|
||
- 音频内容、完整本地路径、上传临时文件名。
|
||
- 完整请求/响应 body。
|
||
|
||
### 允许但不作为标签
|
||
|
||
- 内部 UUID:profile/session/lesson/segment。
|
||
- request ID。
|
||
- 清理后的错误消息。
|
||
|
||
### 网络与权限
|
||
|
||
- Grafana 只允许可信局域网 CIDR 或 VPN 访问。
|
||
- Loki、Alloy 和 Docker API 不发布到宿主机。
|
||
- Alloy 的 Docker Socket 权限视为高权限,固定镜像版本并限制可访问人员。
|
||
- Grafana 管理员密码不得提交;首次登录后更换默认密码。
|
||
- Scalar 和 Grafana 都属于运维入口,未来若局域网信任边界扩大,应统一加反向代理、
|
||
HTTPS 和认证。
|
||
|
||
## 9. 容量、保留与备份
|
||
|
||
初始值:
|
||
|
||
- Docker 原始日志:每容器 `20 MB x 5`。
|
||
- Loki:14 天保留。
|
||
- 每日检查:`df -h`、`docker system df`、Loki 卷大小。
|
||
- 预警:磁盘剩余 20% 开始观察,15% 告警,10% 紧急处理。
|
||
|
||
备份:
|
||
|
||
- `grafana-data`:保存数据源、用户和本地状态;虽然 provisioning 在 Git 中,仍需备份。
|
||
- `loki-data`:若历史日志不是关键资产,可不做强一致备份,但必须明确接受丢失风险。
|
||
- 配置文件和 dashboard/alert provisioning 必须进入 Git。
|
||
- 不备份 Alloy positions 时会导致重采或漏采风险,因此 `alloy-data` 应持久化。
|
||
|
||
备份和恢复命令在实施时写入 `docs/observability-runbook.md`,并实际演练一次 Grafana 配置
|
||
恢复和 Loki 重启持久性。
|
||
|
||
## 10. 回滚方案
|
||
|
||
可观测性栈必须可独立回滚:
|
||
|
||
```bash
|
||
docker compose -f compose.observability.yml down
|
||
```
|
||
|
||
该操作不能停止 `compose.deploy.yml` 的业务服务。默认不加 `-v`,避免误删日志和 Grafana
|
||
数据。
|
||
|
||
应用日志回滚:
|
||
|
||
1. 将 `LOG_FORMAT=pretty` 可暂时恢复文本输出,不需要改业务代码。
|
||
2. 若请求中间件引发问题,回退对应提交并重建 API 镜像。
|
||
3. 若 `local` 日志驱动与采集不兼容,回退 Compose 日志块后 `--force-recreate` 容器;
|
||
restart 不足以恢复日志驱动。
|
||
4. 每阶段使用独立提交,禁止把业务功能改动混进日志基础设施提交。
|
||
|
||
删除数据前必须先确认:
|
||
|
||
```bash
|
||
docker volume ls | grep teaching-feedback
|
||
docker compose -f compose.observability.yml down
|
||
# 只有用户明确确认后才允许删除 loki-data/grafana-data/alloy-data
|
||
```
|
||
|
||
## 11. 建议提交拆分
|
||
|
||
1. `ops(logging): configure container log rotation and version labels`
|
||
2. `feat(logging): add structured request and transcription logs`
|
||
3. `ops(observability): add alloy loki and grafana stack`
|
||
4. `docs(observability): add operations and troubleshooting runbook`
|
||
5. `feat(observability): add validated dashboards and alerts`
|
||
6. `feat(metrics): expose service and transcription metrics`(阶段 5,单独实施)
|
||
|
||
每个提交前运行与其影响范围相匹配的测试。部署修改未经目标机验收不得直接打 release tag。
|
||
|
||
## 12. 最终验收清单
|
||
|
||
- [ ] 目标机 Docker 日志驱动和轮转参数已记录。
|
||
- [ ] API、FunASR、migrate 均使用受控日志轮转。
|
||
- [ ] Rust 与 FunASR 每行输出有效 JSON。
|
||
- [ ] API 响应返回 `X-Request-ID`。
|
||
- [ ] 小程序能保留并展示失败请求编号。
|
||
- [ ] 真实录音可通过 `segment_id/request_id` 关联 Rust 与 FunASR。
|
||
- [ ] 日志中未发现 Token、密钥、学生信息、反馈正文或转录全文。
|
||
- [ ] Alloy 只采集明确标记的容器。
|
||
- [ ] Loki 与 Alloy 没有宿主机端口。
|
||
- [ ] Grafana 需要登录且只在可信网络可达。
|
||
- [ ] Loki 数据源由 provisioning 自动创建。
|
||
- [ ] 14 天 retention 生效,磁盘增长已观察。
|
||
- [ ] 重启可观测性栈后历史和配置仍存在。
|
||
- [ ] 停止可观测性栈不影响 API 和 FunASR。
|
||
- [ ] `cargo fmt --check`、`cargo test`、严格 Clippy 和 `npm run typecheck` 通过。
|
||
- [ ] `docs/observability-runbook.md` 包含启停、查询、备份、升级和回滚命令。
|
||
- [ ] 至少 3–7 天基线后再启用正式告警。
|
||
- [ ] 指标阶段明确使用 Prometheus 或兼容存储,不误把 Alloy 当成指标数据库。
|
||
|
||
## 13. 官方参考资料
|
||
|
||
实施时重新核对最新稳定版本和配置语法:
|
||
|
||
- Docker logging drivers:<https://docs.docker.com/engine/logging/configure/>
|
||
- Grafana Alloy 工作方式:<https://grafana.com/docs/alloy/latest/introduction/how-alloy-works/>
|
||
- Alloy Docker 日志采集:<https://grafana.com/docs/alloy/latest/reference/components/loki/loki.source.docker/>
|
||
- Alloy Docker 容器安装:<https://grafana.com/docs/alloy/latest/set-up/install/docker/>
|
||
- Loki Docker/Compose 安装:<https://grafana.com/docs/loki/latest/setup/install/docker/>
|
||
- Loki 存储和 retention:<https://grafana.com/docs/loki/latest/configure/storage/>
|
||
- Loki 配置参考:<https://grafana.com/docs/loki/latest/reference/loki-config-ref/>
|
||
- Grafana Docker 安装:<https://grafana.com/docs/grafana/latest/setup-grafana/installation/docker/>
|
||
- Grafana Alerting:<https://grafana.com/docs/grafana/latest/alerting/>
|
||
- Alloy Prometheus scrape:<https://grafana.com/docs/alloy/latest/reference/components/prometheus/prometheus.scrape/>
|
||
- tower-http request ID:<https://docs.rs/tower-http/0.6.11/tower_http/request_id/>
|
||
- tower-http trace:<https://docs.rs/tower-http/0.6.11/tower_http/trace/>
|
||
|
||
## 14. 实施完成后的交付报告
|
||
|
||
新会话完成每个阶段后应报告:
|
||
|
||
- 实际提交和部署版本。
|
||
- 新增/修改文件与 `git diff --stat`。
|
||
- 目标机日志驱动、容器、端口、网络和卷状态。
|
||
- API、FunASR、Alloy、Loki、Grafana 健康检查结果。
|
||
- 一条真实请求和一条真实转录的关联查询结果。
|
||
- 日志脱敏检查结果。
|
||
- 数据保留、磁盘使用和备份/恢复验证。
|
||
- 已配置告警及其基线依据。
|
||
- 未完成项、已知风险和明确回滚命令。
|
||
|
||
报告不得输出密码、Token、完整数据库地址、学生信息、反馈正文或转录内容。
|