760 lines
20 KiB
Markdown
760 lines
20 KiB
Markdown
# slog Structured Logging Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Add structured application logging using the Go standard library `log/slog`, with JSON output by default and a global slog-only logging rule.
|
|
|
|
**Architecture:** `pkg/logger` becomes the single application logger factory and returns `*slog.Logger`. `cmd/server/main.go` creates one logger from config and injects it into Gin router and services. Gin request logging is implemented as custom middleware; workspace/process lifecycle logs live in service layer.
|
|
|
|
**Tech Stack:** Go 1.25, standard library `log/slog`, Gin, existing YAML config via `gopkg.in/yaml.v3`.
|
|
|
|
## Global Constraints
|
|
|
|
- Use `log/slog` for all application logs from this change forward.
|
|
- Default log output is JSON.
|
|
- Support log `level` and `format` config.
|
|
- Do not introduce any third-party logging library.
|
|
- Do not use `gin.Default()` or `gin.Logger()`.
|
|
- Do not use `fmt.Print*` as application logging.
|
|
- Do not use `log.Printf`, `log.Println`, `log.Fatal`, or `log.Fatalf` in runtime application paths; only `cmd/server/main.go` may use standard library `log.Fatalf` before slog initialization exists.
|
|
- Do not log file contents, request bodies, response bodies, OpenCode stdout/stderr, environment variables, tokens, secrets, or workspace root in request/process/workspace lifecycle logs.
|
|
- Do not add request ID, tracing, OpenTelemetry, Prometheus, ELK, Loki, auth, CORS, rate limiting, Git, LSP, watcher streaming, frontend, or AI Agent features.
|
|
- Do not change existing API paths, methods, or response JSON shapes.
|
|
- Code generation/refactoring must be delegated to Codex MCP using exactly `model: "kimi/kimi-k2.7-code"`, `sandbox: "danger-full-access"`, and `approval-policy: "on-failure"`.
|
|
- Do not commit unless the user explicitly asks.
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
### Modify
|
|
|
|
- `configs/config.yaml` — add `log.level` and `log.format`.
|
|
- `pkg/config/config.go` — add `LogConfig`, defaults, YAML load, env overrides.
|
|
- `pkg/config/config_test.go` — extend config tests for log defaults/YAML/env.
|
|
- `pkg/logger/logger.go` — replace `log.Logger` factory with `slog.Logger` factory.
|
|
- `cmd/server/main.go` — initialize slog logger and use structured logs.
|
|
- `internal/api/router.go` — accept logger, install custom request logging middleware.
|
|
- `internal/api/router_test.go` — update `NewRouter` calls to pass nil logger.
|
|
- `internal/service/workspace_service.go` — inject logger and add workspace lifecycle logs.
|
|
- `internal/service/process_service.go` — inject logger and add process lifecycle logs.
|
|
- Existing tests under `internal/api` and packages that construct services — update constructors to include logger parameter or nil.
|
|
- `README.md` — document log config and slog-only logging rule.
|
|
|
|
### Create
|
|
|
|
- `pkg/logger/logger_test.go` — tests logger factory success/failure.
|
|
- `internal/api/logging_middleware.go` — Gin request logging middleware.
|
|
|
|
---
|
|
|
|
## Task 1: Add log config
|
|
|
|
**Files:**
|
|
- Modify: `configs/config.yaml`
|
|
- Modify: `pkg/config/config.go`
|
|
- Modify: `pkg/config/config_test.go`
|
|
|
|
**Interfaces:**
|
|
- Produces: `config.LogConfig` with fields `Level string` and `Format string`.
|
|
- Produces: `Config.Log LogConfig`.
|
|
- Produces env overrides `CODESPACE_LOG_LEVEL` and `CODESPACE_LOG_FORMAT`.
|
|
|
|
- [ ] **Step 1: Extend config tests first**
|
|
|
|
Update `pkg/config/config_test.go` to add these assertions to existing tests:
|
|
|
|
```go
|
|
func TestDefaultIncludesLogConfig(t *testing.T) {
|
|
cfg := Default()
|
|
if cfg.Log.Level != "info" {
|
|
t.Fatalf("Log.Level = %q, want info", cfg.Log.Level)
|
|
}
|
|
if cfg.Log.Format != "json" {
|
|
t.Fatalf("Log.Format = %q, want json", cfg.Log.Format)
|
|
}
|
|
}
|
|
|
|
func TestLoadParsesLogConfigFromYAML(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.yaml")
|
|
data := []byte(`log:
|
|
level: "debug"
|
|
format: "text"
|
|
`)
|
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if cfg.Log.Level != "debug" {
|
|
t.Fatalf("Log.Level = %q, want debug", cfg.Log.Level)
|
|
}
|
|
if cfg.Log.Format != "text" {
|
|
t.Fatalf("Log.Format = %q, want text", cfg.Log.Format)
|
|
}
|
|
}
|
|
|
|
func TestLoadAppliesLogEnvironmentOverrides(t *testing.T) {
|
|
t.Setenv("CODESPACE_LOG_LEVEL", "warn")
|
|
t.Setenv("CODESPACE_LOG_FORMAT", "json")
|
|
|
|
cfg, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if cfg.Log.Level != "warn" {
|
|
t.Fatalf("Log.Level = %q, want warn", cfg.Log.Level)
|
|
}
|
|
if cfg.Log.Format != "json" {
|
|
t.Fatalf("Log.Format = %q, want json", cfg.Log.Format)
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run config tests to see failure**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
go test ./pkg/config
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Fails because `Config.Log` and `LogConfig` do not exist yet.
|
|
|
|
- [ ] **Step 3: Implement log config**
|
|
|
|
Modify `pkg/config/config.go`:
|
|
|
|
```go
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server"`
|
|
Workspace WorkspaceConfig `yaml:"workspace"`
|
|
Process ProcessConfig `yaml:"process"`
|
|
Log LogConfig `yaml:"log"`
|
|
}
|
|
|
|
type LogConfig struct {
|
|
Level string `yaml:"level"`
|
|
Format string `yaml:"format"`
|
|
}
|
|
```
|
|
|
|
Update `rawConfig`:
|
|
|
|
```go
|
|
type rawConfig struct {
|
|
Server rawServerConfig `yaml:"server"`
|
|
Workspace WorkspaceConfig `yaml:"workspace"`
|
|
Process ProcessConfig `yaml:"process"`
|
|
Log LogConfig `yaml:"log"`
|
|
}
|
|
```
|
|
|
|
Update defaults:
|
|
|
|
```go
|
|
Log: LogConfig{Level: "info", Format: "json"},
|
|
```
|
|
|
|
Update `applyRaw`:
|
|
|
|
```go
|
|
if raw.Log.Level != "" {
|
|
cfg.Log.Level = raw.Log.Level
|
|
}
|
|
if raw.Log.Format != "" {
|
|
cfg.Log.Format = raw.Log.Format
|
|
}
|
|
```
|
|
|
|
Update `applyEnv`:
|
|
|
|
```go
|
|
if v := os.Getenv("CODESPACE_LOG_LEVEL"); v != "" {
|
|
cfg.Log.Level = v
|
|
}
|
|
if v := os.Getenv("CODESPACE_LOG_FORMAT"); v != "" {
|
|
cfg.Log.Format = v
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Update config YAML**
|
|
|
|
Modify `configs/config.yaml`:
|
|
|
|
```yaml
|
|
log:
|
|
level: "info"
|
|
format: "json"
|
|
```
|
|
|
|
Keep existing `server`, `workspace`, and `process` blocks unchanged.
|
|
|
|
- [ ] **Step 5: Verify config task**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w pkg/config
|
|
go test ./pkg/config
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Config tests pass.
|
|
|
|
---
|
|
|
|
## Task 2: Replace logger package with slog
|
|
|
|
**Files:**
|
|
- Modify: `pkg/logger/logger.go`
|
|
- Create: `pkg/logger/logger_test.go`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `config.LogConfig`.
|
|
- Produces: `logger.New(cfg config.LogConfig) (*slog.Logger, error)`.
|
|
- Produces: slog-only application logging factory.
|
|
|
|
- [ ] **Step 1: Write logger tests first**
|
|
|
|
Create `pkg/logger/logger_test.go`:
|
|
|
|
```go
|
|
package logger
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"codespace/pkg/config"
|
|
)
|
|
|
|
func TestNewJSONInfoLogger(t *testing.T) {
|
|
lg, err := New(config.LogConfig{Level: "info", Format: "json"})
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
if lg == nil {
|
|
t.Fatal("logger is nil")
|
|
}
|
|
}
|
|
|
|
func TestNewTextDebugLogger(t *testing.T) {
|
|
lg, err := New(config.LogConfig{Level: "debug", Format: "text"})
|
|
if err != nil {
|
|
t.Fatalf("New returned error: %v", err)
|
|
}
|
|
if lg == nil {
|
|
t.Fatal("logger is nil")
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsInvalidLevel(t *testing.T) {
|
|
if _, err := New(config.LogConfig{Level: "verbose", Format: "json"}); err == nil {
|
|
t.Fatal("expected invalid level error")
|
|
}
|
|
}
|
|
|
|
func TestNewRejectsInvalidFormat(t *testing.T) {
|
|
if _, err := New(config.LogConfig{Level: "info", Format: "xml"}); err == nil {
|
|
t.Fatal("expected invalid format error")
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Run logger tests to see failure**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
go test ./pkg/logger
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Fails because `logger.New` still has the old signature and returns `*log.Logger`.
|
|
|
|
- [ ] **Step 3: Implement slog logger factory**
|
|
|
|
Replace `pkg/logger/logger.go` with:
|
|
|
|
```go
|
|
package logger
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"codespace/pkg/config"
|
|
)
|
|
|
|
func New(cfg config.LogConfig) (*slog.Logger, error) {
|
|
level, err := parseLevel(cfg.Level)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
opts := &slog.HandlerOptions{Level: level}
|
|
var handler slog.Handler
|
|
switch strings.ToLower(cfg.Format) {
|
|
case "json", "":
|
|
handler = slog.NewJSONHandler(os.Stdout, opts)
|
|
case "text":
|
|
handler = slog.NewTextHandler(os.Stdout, opts)
|
|
default:
|
|
return nil, fmt.Errorf("invalid log format %q", cfg.Format)
|
|
}
|
|
|
|
lg := slog.New(handler)
|
|
slog.SetDefault(lg)
|
|
return lg, nil
|
|
}
|
|
|
|
func parseLevel(value string) (slog.Level, error) {
|
|
switch strings.ToLower(value) {
|
|
case "debug":
|
|
return slog.LevelDebug, nil
|
|
case "info", "":
|
|
return slog.LevelInfo, nil
|
|
case "warn":
|
|
return slog.LevelWarn, nil
|
|
case "error":
|
|
return slog.LevelError, nil
|
|
default:
|
|
return slog.LevelInfo, fmt.Errorf("invalid log level %q", value)
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Verify logger package**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w pkg/logger
|
|
go test ./pkg/logger
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Logger tests pass.
|
|
|
|
---
|
|
|
|
## Task 3: Add Gin request logging middleware
|
|
|
|
**Files:**
|
|
- Create: `internal/api/logging_middleware.go`
|
|
- Modify: `internal/api/router.go`
|
|
- Modify: `internal/api/router_test.go`
|
|
|
|
**Interfaces:**
|
|
- Consumes: `*slog.Logger`.
|
|
- Produces: `requestLogger(lg *slog.Logger) gin.HandlerFunc`.
|
|
- Produces: `api.NewRouter(..., lg *slog.Logger) *gin.Engine`.
|
|
|
|
- [ ] **Step 1: Update router test setup**
|
|
|
|
Modify `internal/api/router_test.go` setup to pass nil logger:
|
|
|
|
```go
|
|
return NewRouter(wsSvc, fileSvc, procSvc, nil)
|
|
```
|
|
|
|
Existing tests must keep the same response assertions.
|
|
|
|
- [ ] **Step 2: Implement request logging middleware**
|
|
|
|
Create `internal/api/logging_middleware.go`:
|
|
|
|
```go
|
|
package api
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func requestLogger(lg *slog.Logger) gin.HandlerFunc {
|
|
if lg == nil {
|
|
lg = slog.Default()
|
|
}
|
|
return func(c *gin.Context) {
|
|
if c.FullPath() == "/healthz" || c.Request.URL.Path == "/healthz" {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
start := time.Now()
|
|
c.Next()
|
|
|
|
status := c.Writer.Status()
|
|
path := c.FullPath()
|
|
if path == "" {
|
|
path = c.Request.URL.Path
|
|
}
|
|
attrs := []any{
|
|
"method", c.Request.Method,
|
|
"path", path,
|
|
"status", status,
|
|
"latency_ms", time.Since(start).Milliseconds(),
|
|
"client_ip", c.ClientIP(),
|
|
}
|
|
if len(c.Errors) > 0 {
|
|
attrs = append(attrs, "error", c.Errors.String())
|
|
}
|
|
|
|
switch {
|
|
case status >= http.StatusInternalServerError:
|
|
lg.Error("http request completed", attrs...)
|
|
case status >= http.StatusBadRequest:
|
|
lg.Warn("http request completed", attrs...)
|
|
default:
|
|
lg.Info("http request completed", attrs...)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Update router signature and middleware**
|
|
|
|
Modify `internal/api/router.go`:
|
|
|
|
```go
|
|
func NewRouter(workspaces *service.WorkspaceService, files *service.FileService, processes *service.ProcessService, lg *slog.Logger) *gin.Engine
|
|
```
|
|
|
|
Add imports:
|
|
|
|
```go
|
|
"log/slog"
|
|
```
|
|
|
|
After `r.Use(gin.Recovery())`, add:
|
|
|
|
```go
|
|
r.Use(requestLogger(lg))
|
|
```
|
|
|
|
Keep `gin.New()` and do not use `gin.Default()` or `gin.Logger()`.
|
|
|
|
- [ ] **Step 4: Verify API package**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w internal/api
|
|
go test ./internal/api
|
|
```
|
|
|
|
Expected:
|
|
|
|
- API tests pass.
|
|
|
|
---
|
|
|
|
## Task 4: Inject slog into services and main
|
|
|
|
**Files:**
|
|
- Modify: `cmd/server/main.go`
|
|
- Modify: `internal/service/workspace_service.go`
|
|
- Modify: `internal/service/process_service.go`
|
|
- Modify tests that call changed constructors.
|
|
|
|
**Interfaces:**
|
|
- Consumes: `*slog.Logger`.
|
|
- Produces: `service.NewWorkspaceService(workspaces workspace.Manager, processes process.Manager, lg *slog.Logger) *WorkspaceService`.
|
|
- Produces: `service.NewProcessService(workspaces workspace.Manager, processes process.Manager, lg *slog.Logger) *ProcessService`.
|
|
|
|
- [ ] **Step 1: Update WorkspaceService**
|
|
|
|
Modify `internal/service/workspace_service.go`:
|
|
|
|
- Add `log/slog` import.
|
|
- Add `logger *slog.Logger` field.
|
|
- Change constructor to accept logger.
|
|
- If nil, use `slog.Default()`.
|
|
- Log successful create/delete.
|
|
- Log failure to stop process before delete.
|
|
|
|
Required log messages:
|
|
|
|
```go
|
|
s.logger.Info("workspace created", "workspace_id", id)
|
|
s.logger.Error("failed to stop workspace process before delete", "workspace_id", id, "error", err)
|
|
s.logger.Info("workspace deleted", "workspace_id", id)
|
|
```
|
|
|
|
Do not log workspace root.
|
|
|
|
- [ ] **Step 2: Update ProcessService**
|
|
|
|
Modify `internal/service/process_service.go`:
|
|
|
|
- Add `log/slog` import.
|
|
- Add `logger *slog.Logger` field.
|
|
- Change constructor to accept logger.
|
|
- If nil, use `slog.Default()`.
|
|
- Log start/stop/restart success and failure.
|
|
|
|
Required log messages:
|
|
|
|
```go
|
|
s.logger.Info("process started", "workspace_id", workspaceID, "pid", status.PID)
|
|
s.logger.Error("process start failed", "workspace_id", workspaceID, "error", err)
|
|
s.logger.Info("process stopped", "workspace_id", workspaceID)
|
|
s.logger.Error("process stop failed", "workspace_id", workspaceID, "error", err)
|
|
s.logger.Info("process restarted", "workspace_id", workspaceID, "pid", status.PID)
|
|
s.logger.Error("process restart failed", "workspace_id", workspaceID, "error", err)
|
|
```
|
|
|
|
Do not log command, env, or workspace root.
|
|
|
|
- [ ] **Step 3: Update main wiring**
|
|
|
|
Modify `cmd/server/main.go`:
|
|
|
|
- Keep standard library `log` only for config/logger initialization failure before slog exists.
|
|
- Initialize logger:
|
|
|
|
```go
|
|
lg, err := logger.New(cfg.Log)
|
|
if err != nil {
|
|
log.Fatalf("failed to initialize logger: %v", err)
|
|
}
|
|
```
|
|
|
|
- Pass logger into services/router:
|
|
|
|
```go
|
|
workspaceSvc := service.NewWorkspaceService(workspaces, processes, lg)
|
|
fileSvc := service.NewFileService(workspaces)
|
|
processSvc := service.NewProcessService(workspaces, processes, lg)
|
|
router := api.NewRouter(workspaceSvc, fileSvc, processSvc, lg)
|
|
```
|
|
|
|
- Replace `lg.Printf` with slog calls:
|
|
|
|
```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,
|
|
)
|
|
```
|
|
|
|
- Replace server goroutine fatal path:
|
|
|
|
```go
|
|
lg.Error("server failed", "error", err)
|
|
os.Exit(1)
|
|
```
|
|
|
|
- Replace shutdown logs:
|
|
|
|
```go
|
|
lg.Info("server shutting down")
|
|
lg.Error("server forced shutdown", "error", err)
|
|
os.Exit(1)
|
|
```
|
|
|
|
- [ ] **Step 4: Update constructor call sites**
|
|
|
|
Update all call sites in tests and code:
|
|
|
|
```go
|
|
service.NewWorkspaceService(wsMgr, procMgr, nil)
|
|
service.NewProcessService(wsMgr, procMgr, nil)
|
|
api.NewRouter(wsSvc, fileSvc, procSvc, nil)
|
|
```
|
|
|
|
Run this grep to find remaining old signatures:
|
|
|
|
```sh
|
|
grep -R "NewWorkspaceService\|NewProcessService\|NewRouter" -n cmd internal pkg | grep -v vendor
|
|
```
|
|
|
|
Expected:
|
|
|
|
- All constructor calls include logger where required.
|
|
|
|
- [ ] **Step 5: Verify packages**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w cmd internal pkg
|
|
go test ./internal/service ./internal/api ./cmd/server
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Tests/compilation pass.
|
|
|
|
---
|
|
|
|
## Task 5: Documentation and global slog-only validation
|
|
|
|
**Files:**
|
|
- Modify: `README.md`
|
|
- Modify: docs/spec already updated; no new spec edits needed unless implementation differs.
|
|
|
|
**Interfaces:**
|
|
- Produces documented logging config and slog-only rule.
|
|
|
|
- [ ] **Step 1: Update README**
|
|
|
|
Add a logging configuration section:
|
|
|
|
```markdown
|
|
## Logging
|
|
|
|
codespace uses the Go standard library `log/slog` for application logs.
|
|
|
|
Default config:
|
|
|
|
```yaml
|
|
log:
|
|
level: "info"
|
|
format: "json"
|
|
```
|
|
|
|
Environment overrides:
|
|
|
|
- `CODESPACE_LOG_LEVEL` (`debug`, `info`, `warn`, `error`)
|
|
- `CODESPACE_LOG_FORMAT` (`json`, `text`)
|
|
|
|
All application logs must go through `log/slog`. Do not use `fmt.Print*`, `log.Printf`, `gin.Logger()`, or third-party logging libraries in server runtime code.
|
|
```
|
|
|
|
- [ ] **Step 2: Run final validation**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
gofmt -w cmd internal pkg
|
|
go mod tidy
|
|
go test ./...
|
|
```
|
|
|
|
Expected:
|
|
|
|
- All tests pass.
|
|
|
|
- [ ] **Step 3: Check slog-only constraint**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
grep -R "log\.Printf\|log\.Println\|log\.Fatal\|fmt\.Print\|gin\.Default\|gin\.Logger" cmd internal pkg || true
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Only allowed match is the logger-initialization fallback in `cmd/server/main.go`, if present.
|
|
- No `fmt.Print*`, `gin.Default`, `gin.Logger`, or runtime `log.Printf`/`log.Println` matches.
|
|
|
|
- [ ] **Step 4: Run server smoke test**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
(go run ./cmd/server > /tmp/codespace-server.log 2>&1 & pid=$!; \
|
|
for i in $(seq 1 30); do \
|
|
if curl -fsS http://localhost:8080/healthz; then \
|
|
kill $pid; wait $pid 2>/dev/null || true; exit 0; \
|
|
fi; \
|
|
sleep 0.2; \
|
|
done; \
|
|
kill $pid 2>/dev/null || true; \
|
|
wait $pid 2>/dev/null || true; \
|
|
cat /tmp/codespace-server.log; exit 1)
|
|
```
|
|
|
|
Expected:
|
|
|
|
```json
|
|
{"status":"ok"}
|
|
```
|
|
|
|
- [ ] **Step 5: Inspect git diff scope**
|
|
|
|
Run:
|
|
|
|
```sh
|
|
git diff --stat
|
|
git status --short
|
|
```
|
|
|
|
Expected:
|
|
|
|
- Changes are limited to logging config, logger package, API middleware/router signature, workspace/process service logging, docs, and tests.
|
|
- No unrelated auth/CORS/Git/LSP/frontend changes.
|
|
|
|
---
|
|
|
|
## Codex Execution Prompt
|
|
|
|
Use this exact structure when dispatching Codex:
|
|
|
|
```text
|
|
## Context
|
|
- Project root: /Users/taochen/llm/codespace
|
|
- Current branch: main
|
|
- Go baseline: 1.25
|
|
- Existing API router uses Gin and explicit http.Server composition.
|
|
- Design spec: docs/superpowers/specs/2026-07-02-slog-logging-design.md
|
|
- Implementation plan: docs/superpowers/plans/2026-07-02-slog-logging.md
|
|
|
|
## Task
|
|
Add structured logging with Go standard library log/slog.
|
|
Steps:
|
|
1. Add log config (`log.level`, `log.format`) with YAML/env support.
|
|
2. Replace pkg/logger with slog logger factory.
|
|
3. Add Gin request logging middleware, skipping /healthz.
|
|
4. Inject slog logger into main, API router, WorkspaceService, and ProcessService.
|
|
5. Add workspace/process lifecycle logs.
|
|
6. Update README and tests.
|
|
7. Run final validation.
|
|
|
|
## Constraints
|
|
- All application logs must use log/slog from now on.
|
|
- Do not use fmt.Print* as logging.
|
|
- Do not use gin.Default() or gin.Logger().
|
|
- Do not use log.Printf/log.Println/log.Fatal/log.Fatalf in runtime paths; only cmd/server/main.go may use log.Fatalf before slog initialization exists.
|
|
- Do not log file contents, request bodies, response bodies, OpenCode stdout/stderr, environment variables, tokens, secrets, command, env, or workspace root in request/process/workspace lifecycle logs.
|
|
- Do not change API paths, methods, or response JSON shapes.
|
|
- Do not add third-party logging dependencies or unrelated features.
|
|
- Do not commit.
|
|
|
|
## Acceptance
|
|
- gofmt -w cmd internal pkg succeeds.
|
|
- go mod tidy succeeds.
|
|
- go test ./... passes.
|
|
- slog-only grep has no disallowed runtime logging.
|
|
- go run ./cmd/server starts and /healthz returns {"status":"ok"}.
|
|
```
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
- Spec coverage: Plan covers config, logger package, request middleware, service lifecycle logs, main wiring, README, and slog-only validation.
|
|
- Placeholder scan: No TBD/TODO/ambiguous implementation steps remain.
|
|
- Type consistency: `logger.New(config.LogConfig) (*slog.Logger, error)`, `api.NewRouter(..., *slog.Logger)`, `NewWorkspaceService(..., *slog.Logger)`, and `NewProcessService(..., *slog.Logger)` are consistent across tasks.
|
|
- Scope check: Plan is limited to logging. It does not add request IDs, tracing, third-party logging, auth, CORS, Git, LSP, frontend, watcher streaming, or Agent features.
|