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
T
2026-07-02 16:34:42 +08:00

8.7 KiB
Raw Blame History

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.Fatalflg.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 尚未存在。

验证方式:

grep -R "log\.Printf\|log\.Println\|log\.Fatal\|fmt\.Print\|gin\.Default\|gin\.Logger" cmd internal pkg || true

预期只允许出现 logger 初始化失败前的兜底 log.Fatalf,不允许出现运行期旁路日志。

配置设计

configs/config.yaml 增加:

log:
  level: "info"
  format: "json"

pkg/config 增加:

type LogConfig struct {
    Level  string `yaml:"level"`
    Format string `yaml:"format"`
}

Config 增加:

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

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

启动日志:

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 日志不记录宿主机绝对路径。

错误日志:

lg.Error("server failed", "error", err)
os.Exit(1)

slog 没有 Fatal,不要自己造复杂 wrapper。

Gin request logging middleware

新增 middleware

func requestLogger(lg *slog.Logger) gin.HandlerFunc

api.NewRouter 签名改为:

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/3xxInfo
  • 4xxWarn
  • 5xxError

/healthz 不记录,避免健康检查刷屏。

request logging 不记录:

  • request body
  • response body
  • query string 中的敏感内容

path 使用路由模板或 URL path,不包含 query string。

业务日志边界

WorkspaceService

增加 logger 字段:

type WorkspaceService struct {
    workspaces workspace.Manager
    processes  process.Manager
    logger     *slog.Logger
}

构造函数接受 logger

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_iderror

不记录 workspace root。

ProcessService

增加 logger 字段:

type ProcessService struct {
    workspaces workspace.Manager
    processes  process.Manager
    logger     *slog.Logger
}

构造函数接受 logger

func NewProcessService(workspaces workspace.Manager, processes process.Manager, lg *slog.Logger) *ProcessService

如果 lg == nil,使用 slog.Default()

记录:

  • start 成功:process started,字段 workspace_idpid
  • start 失败:process start failed,字段 workspace_iderror
  • stop 成功:process stopped,字段 workspace_id
  • stop 失败:process stop failed,字段 workspace_iderror
  • restart 成功:process restarted,字段 workspace_idpid
  • restart 失败:process restart failed,字段 workspace_iderror

不记录 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 影响。

全局验证

必须通过:

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 通过。