# 统一日志规范

> Canonical path: `specs/observability/logging.md`
> Status: living-spec
> Scope: AgentTeam Server、Gateway、Desktop、Web/Electron、内嵌组件与本地诊断文件的长期日志合同。
> Related specs: [`observability.md`](./observability.md)、[`metrics.md`](./metrics.md)、[`daemon.md`](../daemon/daemon.md)、[`daemon-transport.md`](../daemon/daemon-transport.md)。
> Non-goals: 指定日志供应商、告警阈值、Dashboard 布局或上传原始客户端日志。

---

## 1. 原则

日志用于定位一次具体操作的状态与失败，不是业务数据副本。

- 同一进程只有一条 operational logging pipeline；内嵌组件复用宿主 handler 和 writer，必要的本地 tee 只编码一次。
- `service.name` 由部署、日志流或导出器作为外层 metadata 提供，不在每条记录中重复。
- 记录使用业务运行时 `domain`，不按旧产品名、源码目录、Go package 或调用栈推断归属。
- 日志必须有界、可脱敏、可关联；不得为产生日志额外查询数据库或同步调用远端服务。
- 新增或修改的日志必须符合本规范；存量调用按触达范围收口，不用兼容层永久掩盖。

## 2. 记录合同

字段名统一使用 `snake_case`。Go 代码使用 `log/slog`；其他语言保持相同语义。

| 字段 | 要求 | 说明 |
| --- | --- | --- |
| `time` | 必填 | 含时区的 RFC 3339 时间；由 handler 生成 |
| `level` | 必填 | `DEBUG`、`INFO`、`WARN`、`ERROR` |
| `msg` | 必填 | 稳定、简短的事件名称，不拼接动态正文 |
| `domain` | 必填 | 当前行为的业务 owner；bootstrap/基础设施使用 `system` |
| `component` | 可选 | domain 内固定低基数子组件，如 `mirror_worker` |
| `error` | 失败时 | 脱敏后的错误摘要；同一错误只在责任边界记录一次 |
| `request_id` | 请求内 | AgentTeam request correlation |
| `trace_id`、`span_id` | 有有效 span 时 | 与 OpenTelemetry 使用同一标识 |
| `stacktrace` | 见 §4 | 本地产生且有界的调用链或 panic stack |
| `stacktrace_omitted` | 可选 | `rate_limited` 或 `unavailable` |

时间长度使用带单位字段，如 `duration_ms`；不要使用单位不明确的 `duration`。业务 ID 仅在排障必要时进入 JSON/text body，不能提升为 collector label。

```json
{"time":"2026-08-18T09:00:00+08:00","level":"ERROR","msg":"source mirror failed","domain":"kb","component":"mirror_worker","request_id":"req_01","error":"context deadline exceeded","stacktrace":"..."}
```

### 2.1 Domain

`domain` 是代码控制的低基数枚举。典型值包括：

`system`、`auth`、`workspace`、`channel`、`message`、`task`、`agent`、`workflow`、`job`、`schedule`、`kb`、`memory`、`skill`、`role`、`automation`、`ags`。

归属按真正执行行为的 owner 决定：message 触发 job 时，message 服务记录 `domain=message`，job worker 记录 `domain=job`。workspace、user、route、agent、文件路径和任意 ID 都不是 domain。新增 domain 必须是稳定业务边界，并加入 owning runtime 的常量或注册表。

## 3. 级别与记录位置

- `DEBUG`：仅用于显式启用的开发诊断；不能依赖其完成生产排障。
- `INFO`：重要生命周期或低频状态转换；不得为每行、每 token、每轮询 tick 记录。
- `WARN`：已降级或可恢复异常，当前操作仍能继续。
- `ERROR`：当前责任边界确认操作失败，需要调查或向上返回失败。

错误应在最了解操作语义、且决定重试/降级/返回失败的边界记录一次。下层返回 error、上层再记录同一 error 的做法会制造重复事件和重复 stack，必须避免。高频丢弃、重试和背压使用计数汇总或限频日志，而不是逐项 WARN/ERROR。

## 4. Stack

- 普通 `ERROR` 在 level 已启用后采集有帧数和字节上限的本地调用链；已有等价 stack 时不重复生成。
- panic/recovery 记录原始 panic stack，并保留 panic 值的安全摘要。
- `INFO` 和 `WARN` 不采集 stack。
- 故障风暴可按调用点限频 stack；记录仍保留，并设置 `stacktrace_omitted=rate_limited`。
- Desktop、Electron、Renderer 的原始 stack 只留在故障发生侧。它不得跨产品 API 上传到 Server；跨边界只传显式定义、脱敏且有大小上限的 observation。

## 5. 各组件输出

| 组件 | 格式与位置 | 额外规则 |
| --- | --- | --- |
| `agentteam-server`、`agentteam-gateway-server` 及其他常驻后端 | 默认 JSON 到 stdout；§8 可选择 text/file/both | 外层提供 `service.name`；request/trace correlation 由 handler 注入 |
| Desktop daemon | 人可读 `slog` text，写轮转 `agentteam.log` 并输出 stderr | 默认单文件 10 MiB、保留 10 份；stderr 已指向同一文件时不得双写；`agentteam journal` 保持可读 |
| Desktop CLI | 正常结果写 stdout，诊断写 stderr | 不能用日志破坏 JSON 或其他机器可读输出 |
| Electron main、Renderer、Web | 本地 console；生产默认不持久化、不上传 | 禁止正文、token 和高频状态；需要持久证据时走明确的 Desktop observation/diagnostic 合同 |
| 内嵌 AGS 或其他库 | 复用宿主 sink，使用自身 domain | 不得覆盖宿主默认 logger、另开 listener 或写第二份 operational log |
| OpenCode 等 runtime diagnostics | 独立、有界、权限受限、已脱敏的 JSONL/text | 不是 operational log；显式本地导出，默认不上传，遵守 `specs/daemon/daemon.md` |

组件可保留适合人的物理格式，但字段语义、domain、隐私和成本规则必须一致。

## 6. 隐私与边界

日志不得包含：message/prompt/tool-input 正文、附件内容、Authorization/Cookie、API key/token、完整环境变量、DSN、带实参 SQL、私有密钥、runtime 数据库内容或 workspace 文件内容。

上游 error 可能携带 URL、header、SQL、路径或凭证；记录前必须分类或脱敏，不能直接信任 `err.Error()`。本地路径只在确有诊断价值且不会跨边界的受限诊断文件中使用；导出 manifest 必须 path-free。日志 collector 只能把 `level`、`service`、`deployment_env`、`instance`、`domain` 的固定 allowlist 提升为 label；correlation 与业务字段留在记录 body 中查询。

## 7. 注入与性能

进程 composition root 创建 base handler，并为长期对象一次性派生 domain logger：

```go
messageLogger := baseLogger.With(slog.String("domain", "message"))
messageService := message.New(..., message.Options{Logger: messageLogger})
```

- logger 保存在 handler/service/worker 上；共享组件由调用方注入 owner logger。
- 禁止每条日志通过 `runtime.Caller`、stack、路径正则、反射、动态 map 或 context 解析 domain。
- 禁止在热路径临时 `With`、重复 JSON 编码、同步远端发送或由业务调用层自行 fan-out；组件表要求的本地 tee 只能在 composition root 配置。
- 先执行 `Enabled`；stack 和其他昂贵字段只在记录确实会输出时构造。
- 普通 stack 使用有界 `runtime.Callers`；完整 `debug.Stack()` 仅用于 panic。
- 固定 domain 只增加一次字段序列化；除此之外，正常 INFO/WARN 路径不得引入额外分类工作。

日志基础设施变更必须提供 `-benchmem` 对比，至少覆盖 disabled level、INFO + domain 和 ERROR + bounded stack。性能检查使用：

```bash
go test -bench=. -benchmem ./internal/observability/logging
```

## 8. 后端环境配置

`agentteam-server`、`agentteam-gateway-server` 和其他常驻后端通过环境变量读取同一配置合同。配置形状沿用 TiDB 的 `log` / `log.file` 分层语义，但采用有界保留默认值：

| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `AGENTTEAM_LOG_LEVEL` | `info` | `debug`、`info`、`warn`、`error` |
| `AGENTTEAM_LOG_FORMAT` | `json` | `json` 或 `text`；生产后端推荐 `json` |
| `AGENTTEAM_LOG_OUTPUT` | `stdout` | `stdout`、`file` 或 `both` |
| `AGENTTEAM_LOG_FILE` | 空 | file 的绝对路径；`file` / `both` 时必填 |
| `AGENTTEAM_LOG_FILE_MAX_SIZE_MB` | `100` | 单个 active file 的最大 MiB，达到上限后 rotate |
| `AGENTTEAM_LOG_FILE_MAX_DAYS` | `7` | rotated file 最长保留天数；`0` 表示不按时间删除 |
| `AGENTTEAM_LOG_FILE_MAX_BACKUPS` | `10` | rotated file 最大数量；`0` 表示不按数量删除 |
| `AGENTTEAM_LOG_FILE_COMPRESSION` | `gzip` | `gzip` 或 `none`，只作用于 rotated file |
| `AGENTTEAM_LOG_FILE_BUFFER_SIZE_KB` | `256` | file writer buffer；`0` 表示不缓冲 |
| `AGENTTEAM_LOG_FILE_FLUSH_INTERVAL` | `1s` | buffer 定时 flush；必须大于 `0` |

配置规则：

- `stdout` 不创建文件且要求 `AGENTTEAM_LOG_FILE` 为空；`file` 只写文件；`both` 对同一条已编码记录 tee，不重复编码。stdout/stderr 已指向同一文件时必须去重。
- rotate 在 active file 达到 size 上限时发生；rotated file 超过 days 或 backups 任一启用的上限即清理。`file` / `both` 要求 days、backups 至少一个大于 `0`，避免无界占盘。
- `AGENTTEAM_LOG_FILE_MAX_SIZE_MB` 必须在 `[1, 4096]`；days、backups 和 buffer size 必须为非负整数。
- file parent 以受限权限创建；文件默认仅服务账号可读写。路径不可写、不是绝对路径或无法创建时，进程启动失败，不静默退回 stdout。
- 配置使用严格解析：未知枚举、非法整数/时长和矛盾组合均启动失败。日志中不得打印完整环境或文件配置。
- file buffer 由单个 writer 管理，不为每条记录创建 goroutine；ERROR、panic 和正常 shutdown 必须 flush。同步 file I/O 不得进入 domain service。

Desktop daemon 继续以 `config.yaml` 的 `log.dir`、`log.file`、`log.level` 作为用户配置面，并使用 §5 的 10 MiB / 10 backups 默认值；GUI/launchd 环境不把 env 当作持久用户设置。其 writer、权限、去重、flush 和有界 rotate 仍遵守本节语义。

## 9. Ownership 与验证

- Server/Gateway 的 handler decoration 由 `internal/observability/logging` 拥有；domain package 只接收 logger，不反向依赖 observability 的业务知识。
- Desktop daemon composition root 拥有本地 sink、轮转与 stderr 去重；runtime diagnostics 保持独立 owner。
- composition root 安装一次默认 logger，并把标准库 `log` 桥到同一 sink；裸全局日志归入 `system`，业务代码不得依赖全局 logger 表达 domain。

测试至少验证：格式可解析、domain 正确、显式/默认/stdlib 日志只有一个 sink、request/trace 字段不回归、非 ERROR 无 stack、ERROR stack 有界、敏感 fixture 不泄漏。新增日志还应人工检查级别、重复记录、基数字段和热路径频率。

实现盘点可用以下命令刷新；目录命中只用于找调用点，不能用于推断 domain：

```bash
rg -n 'slog\.|log\.(Print|Fatal|Panic)|fmt\.Print' cmd internal desktop --glob '*.go'
rg -n 'console\.(debug|info|warn|error)' web/src desktop/electron --glob '*.{ts,tsx,js,mjs}'
```
