diff --git a/AGENTS.md b/AGENTS.md index 1f46f91..3e3d457 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,6 +50,8 @@ ## 10. 代码与流程硬性规范(压缩后仍需遵守) - 🔴 **文件 ≤500 行、函数 ≤80 行**;**模块按职责拆成多个源文件,严禁把整个模块塞进一个超大文件**;**三次复制必须抽取**共享。 - 🔴 禁 `interface{}`(用 `any`)、禁包级全局变量(除 `var ErrXxx`)、禁裸 `return err`、禁魔法数字;错误一律 `%w` wrap;SQL 全参数化、表名单数、主键 `id`、时间戳 `created_at/updated_at`、索引 `idx_{table}_{col}`;敏感信息打日志。 +- 🔴 **日志必须标来源**:模块/子功能用 `internal/logger.WithSource`/`Scoped`(`source=module/subfeature`)标注,跨模块可追溯定位;**禁止无来源裸日志**。 +- 🔴 **客户端 API 契约先行**:REST `/api/v1` + WS `/ws` + admin `/api/v1/admin/*` 统一**版本化契约**,供跨平台客户端(Web/移动/IM bridge)接入与治理;JSON 响应 `{data}` / `{error:{code,message}}`。 - 🔴 每阶段完成(或里程碑归档)后:**`git push origin dev`** 同步远端。 - 🔴 **遇到开发文档冲突 / 意义不明 / 规格矛盾时**:**阻塞会话**,用 `AskUserQuestion` 询问主人,**不得自行猜测**;文档清、不冲突则自主推进。 - 🔴 阶段验证:后端 `go build ./...` + `go vet ./...` + `go test ./...` 全绿 + windows/darwin 交叉编译;前端(M9+)`tsc --noEmit` + `vite build` 通过。 diff --git a/internal/logger/logger.go b/internal/logger/logger.go index a373f2b..d737c7c 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -68,3 +68,24 @@ func ParseLevel(s string) slog.Level { return slog.LevelInfo } } + +// WithSource returns a child logger that tags every emitted record with a +// source so issues can be traced back to the owning module/sub-feature. source +// is conventionally "module/subfeature", e.g. "runtime/loop" or +// "channel/adapter". Every module that logs should call this once on its base +// logger so all records carry provenance. +func WithSource(l *slog.Logger, source string) *slog.Logger { + if l == nil { + return nil + } + return l.With(slog.String("source", source)) +} + +// Scoped returns a child logger tagged with module and subfeature, for finer +// provenance when a module contains several subsystems. +func Scoped(l *slog.Logger, module, subfeature string) *slog.Logger { + if l == nil { + return nil + } + return l.With(slog.String("module", module), slog.String("sub", subfeature)) +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go index 5451a2f..6d7e8d9 100644 --- a/internal/logger/logger_test.go +++ b/internal/logger/logger_test.go @@ -91,3 +91,28 @@ func TestNew_DefaultsToErrAndText(t *testing.T) { t.Fatal("New(Options{}) returned nil logger") } } + +func TestWithSource_TagsRecords(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + base := New(Options{Level: "debug", Format: "json", Out: &buf}) + log := WithSource(base, "runtime/loop") + log.Info("hello") + if !strings.Contains(buf.String(), `"source":"runtime/loop"`) { + t.Fatalf("missing source field, got %q", buf.String()) + } +} + +func TestScoped_TagsModuleAndSub(t *testing.T) { + t.Parallel() + + var buf bytes.Buffer + base := New(Options{Level: "debug", Format: "json", Out: &buf}) + log := Scoped(base, "channel", "adapter") + log.Info("hello") + got := buf.String() + if !strings.Contains(got, `"module":"channel"`) || !strings.Contains(got, `"sub":"adapter"`) { + t.Fatalf("missing module/sub fields, got %q", got) + } +}