This repository has been archived on 2026-07-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
codespace/docs/superpowers/specs/2026-07-02-slog-logging-design.md
2026-07-02 16:34:42 +08:00

351 lines
8.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# slog Structured Logging Design
日期:2026-07-02
## 目标
为 codespace 后端增加基于 Go 标准库 `log/slog` 的结构化日志。目标是建立清晰、低噪音、不会泄露用户内容的日志边界,而不是到处撒日志。
本次目标:
- 使用 `log/slog` 替代当前 `log.Logger`
- 默认输出 JSON 日志。
- 支持日志 level 和 format 配置。
- 为 Gin 增加 request logging middleware。
- 记录 workspace/process 生命周期关键事件。
- 建立全局约束:后续所有应用日志都必须使用 `log/slog`
- 不记录文件内容、请求 body、token、环境变量、OpenCode 输出、宿主机绝对路径。
## 当前状态
当前代码:
- `pkg/logger.New()` 返回 `*log.Logger`
- `cmd/server/main.go` 使用 `log.Fatalf``lg.Printf`
- `internal/api.NewRouter(...)` 没有 logger 参数。
- Gin router 使用 `gin.New()` + `gin.Recovery()`
- service/process/workspace 层基本没有日志。
## 非目标
本次不做:
- 不引入第三方日志库。
- 不接入 OpenTelemetry、Prometheus、ELK、Loki。
- 不实现 request ID / trace ID。
- 不记录文件内容、请求 body、OpenCode stdout/stderr。
- 不记录 workspace root 绝对路径。
- 不在高频文件读写路径里打业务日志。
- 不增加认证、CORS、rate limit、Git、LSP、Agent 功能。
## 全局日志约束
从本次改动开始,后续所有应用日志必须走 `log/slog`
禁止在应用代码中重新引入这些旁路日志方式:
- `log.Printf` / `log.Println` / `log.Fatal` / `log.Fatalf`,除非是在 logger 初始化失败前的最后兜底。
- `fmt.Print*` 用作日志。
- `gin.Logger()``gin.Default()` 隐式 request logger。
- 第三方日志库。
允许:
- 测试中使用 `t.Log*`
- CLI/工具脚本中显式面向用户的 stdout 输出,但不能混进服务端运行路径。
- logger 初始化失败之前,`cmd/server/main.go` 可以使用标准库 `log.Fatalf` 作为兜底,因为此时 slog logger 尚未存在。
验证方式:
```sh
grep -R "log\.Printf\|log\.Println\|log\.Fatal\|fmt\.Print\|gin\.Default\|gin\.Logger" cmd internal pkg || true
```
预期只允许出现 logger 初始化失败前的兜底 `log.Fatalf`,不允许出现运行期旁路日志。
## 配置设计
`configs/config.yaml` 增加:
```yaml
log:
level: "info"
format: "json"
```
`pkg/config` 增加:
```go
type LogConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
}
```
`Config` 增加:
```go
type Config struct {
Server ServerConfig `yaml:"server"`
Workspace WorkspaceConfig `yaml:"workspace"`
Process ProcessConfig `yaml:"process"`
Log LogConfig `yaml:"log"`
}
```
默认值:
- `level`: `info`
- `format`: `json`
环境变量:
- `CODESPACE_LOG_LEVEL`
- `CODESPACE_LOG_FORMAT`
支持 level
- `debug`
- `info`
- `warn`
- `error`
支持 format
- `json`
- `text`
虽然默认使用 JSON,但保留 text 是低成本本地调试能力。
## Logger 包设计
`pkg/logger` 改为创建 `*slog.Logger`
```go
func New(cfg config.LogConfig) (*slog.Logger, error)
```
行为:
- `format=json` 使用 `slog.NewJSONHandler(os.Stdout, opts)`
- `format=text` 使用 `slog.NewTextHandler(os.Stdout, opts)`
- `level` 解析成 `slog.LevelDebug/Info/Warn/Error`
- 非法 level 或 format 返回 error。
- 成功创建后调用 `slog.SetDefault(lg)`
不保留旧 `*log.Logger` 包装层。项目还小,直接切干净。
## main.go 设计
启动流程:
1. 先加载 config。
2.`logger.New(cfg.Log)` 初始化 slog。
3. 初始化失败时使用标准库 `log.Fatalf`,因为此时结构化 logger 还不存在。
4. 后续全部使用 `lg.Info/Warn/Error`
启动日志:
```go
lg.Info("server starting",
"addr", cfg.Server.Addr,
"workspace_root", cfg.Workspace.Root,
"read_timeout", cfg.Server.ReadTimeout.String(),
"write_timeout", cfg.Server.WriteTimeout.String(),
"idle_timeout", cfg.Server.IdleTimeout.String(),
"max_header_bytes", cfg.Server.MaxHeaderBytes,
)
```
注意:启动日志可以记录配置里的 workspace root,因为这是服务启动配置;但 request/process/workspace lifecycle 日志不记录宿主机绝对路径。
错误日志:
```go
lg.Error("server failed", "error", err)
os.Exit(1)
```
`slog` 没有 `Fatal`,不要自己造复杂 wrapper。
## Gin request logging middleware
新增 middleware
```go
func requestLogger(lg *slog.Logger) gin.HandlerFunc
```
`api.NewRouter` 签名改为:
```go
func NewRouter(
workspaces *service.WorkspaceService,
files *service.FileService,
processes *service.ProcessService,
lg *slog.Logger,
) *gin.Engine
```
如果 `lg == nil`,使用 `slog.Default()`
middleware 字段:
- `method`
- `path`
- `status`
- `latency_ms`
- `client_ip`
- `error`
level 规则:
- 2xx/3xx`Info`
- 4xx`Warn`
- 5xx`Error`
`/healthz` 不记录,避免健康检查刷屏。
request logging 不记录:
- request body
- response body
- query string 中的敏感内容
`path` 使用路由模板或 URL path,不包含 query string。
## 业务日志边界
### WorkspaceService
增加 logger 字段:
```go
type WorkspaceService struct {
workspaces workspace.Manager
processes process.Manager
logger *slog.Logger
}
```
构造函数接受 logger
```go
func NewWorkspaceService(workspaces workspace.Manager, processes process.Manager, lg *slog.Logger) *WorkspaceService
```
如果 `lg == nil`,使用 `slog.Default()`
记录:
- create 成功:`workspace created`,字段 `workspace_id`
- delete 成功:`workspace deleted`,字段 `workspace_id`
- delete 前停止进程失败:`failed to stop workspace process before delete`,字段 `workspace_id``error`
不记录 workspace root。
### ProcessService
增加 logger 字段:
```go
type ProcessService struct {
workspaces workspace.Manager
processes process.Manager
logger *slog.Logger
}
```
构造函数接受 logger
```go
func NewProcessService(workspaces workspace.Manager, processes process.Manager, lg *slog.Logger) *ProcessService
```
如果 `lg == nil`,使用 `slog.Default()`
记录:
- start 成功:`process started`,字段 `workspace_id``pid`
- start 失败:`process start failed`,字段 `workspace_id``error`
- stop 成功:`process stopped`,字段 `workspace_id`
- stop 失败:`process stop failed`,字段 `workspace_id``error`
- restart 成功:`process restarted`,字段 `workspace_id``pid`
- restart 失败:`process restart failed`,字段 `workspace_id``error`
不记录 command、env、workspace root。
### FileService
第一版不增加业务日志。文件操作高频且容易泄露用户路径/内容。request logging 已足够覆盖 HTTP 访问。
## API 兼容性
现有 API 路径、method、响应 JSON 不变。
唯一函数签名变化是内部构造函数:
- `api.NewRouter(..., lg *slog.Logger)`
- `service.NewWorkspaceService(..., lg *slog.Logger)`
- `service.NewProcessService(..., lg *slog.Logger)`
测试可传 `nil`,由实现回退到 `slog.Default()`
## 测试策略
### logger tests
覆盖:
- `logger.New(config.LogConfig{Level: "info", Format: "json"})` 成功。
- `logger.New(config.LogConfig{Level: "debug", Format: "text"})` 成功。
- 非法 level 返回 error。
- 非法 format 返回 error。
### config tests
覆盖:
- 默认 log config 是 `info/json`
- YAML 能覆盖 log config。
- 环境变量能覆盖 log config。
### API tests
现有 `httptest` 行为不变。
额外覆盖:
- `api.NewRouter(..., nil)` 不 panic。
- `/healthz` 正常返回,不受 logging middleware 影响。
### 全局验证
必须通过:
```sh
gofmt -w cmd internal pkg
go test ./...
go run ./cmd/server
curl http://localhost:8080/healthz
```
## 风险与处理
- 风险:日志泄露用户内容。处理:禁止记录 body、文件内容、OpenCode 输出、env、token。
- 风险:日志噪音过大。处理:不记录 `/healthz`,不记录 FileService 高频业务日志。
- 风险:logger 注入让构造函数变多。处理:只给 router、workspace service、process service 加 loggerFileService 不加。
- 风险:测试输出被 JSON 日志污染。处理:测试传 nil 或使用 `slog.New(slog.NewTextHandler(io.Discard, nil))`
## 完成标准
完成后满足:
- `pkg/logger` 使用 `log/slog`
- `cmd/server/main.go` 不再使用 `lg.Printf`
- `api.NewRouter` 接受 logger 并安装 request logging middleware。
- workspace/process 生命周期有结构化日志。
- log 配置支持 YAML 和环境变量。
- 全局约束检查中无运行期旁路日志;只允许 logger 初始化失败前的兜底 `log.Fatalf`
- server smoke test 通过。