# 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 加 logger;FileService 不加。 - 风险:测试输出被 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 通过。