From ea0c6a8b163f18ff3103b2f4c4e34b02f013d77d Mon Sep 17 00:00:00 2001 From: huang Date: Tue, 11 Nov 2025 18:15:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=B8=80=E4=B8=8B=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=80=E9=83=A8=E5=88=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .specify/memory/constitution.md | 188 +++++++++++++++++++++++---- .specify/templates/plan-template.md | 9 ++ .specify/templates/tasks-template.md | 6 + configs/config.dev.yaml | 4 +- internal/handler/health.go | 3 + pkg/logger/logger.go | 25 +++- pkg/logger/middleware.go | 33 ++++- 7 files changed, 234 insertions(+), 34 deletions(-) diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 01b7581..c06ce35 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,42 +1,42 @@ # 君鸿卡管系统 Constitution @@ -933,6 +933,134 @@ README.md # 项目主文档 --- +### VIII. Access Logging Standards (访问日志规范) + +**规则 (RULES):** + +- **所有** HTTP 请求 **MUST** 被记录到 `access.log`,无例外 +- 访问日志 **MUST** 记录完整的请求参数(query 参数 + request body) +- 访问日志 **MUST** 记录完整的响应参数(response body) +- 请求/响应 body **MUST** 限制大小为 50KB,超过部分截断并标注 `... (truncated)` +- 访问日志 **MUST** 通过统一的 Logger 中间件(`pkg/logger/Middleware()`)记录 +- **任何中间件** 的短路返回(认证失败、限流拒绝、参数验证失败等)**MUST NOT** 绕过访问日志 +- 访问日志 **MUST** 包含以下字段(最低要求): + - `method`: HTTP 方法 + - `path`: 请求路径 + - `query`: Query 参数字符串 + - `status`: HTTP 状态码 + - `duration_ms`: 请求耗时(毫秒) + - `request_id`: 请求唯一 ID + - `ip`: 客户端 IP + - `user_agent`: 用户代理 + - `user_id`: 用户 ID(认证后有值,否则为空) + - `request_body`: 请求体(限制 50KB) + - `response_body`: 响应体(限制 50KB) +- 访问日志 **SHOULD** 使用 JSON 格式,便于日志分析和监控 +- 访问日志文件 **MUST** 配置自动轮转(基于大小或时间) + +**正确的访问日志示例:** + +```json +{ + "level": "info", + "timestamp": "2025-11-11T17:45:03.186+0800", + "message": "", + "method": "POST", + "path": "/api/v1/users", + "query": "page=1&size=10", + "status": 400, + "duration_ms": 0.035, + "request_id": "f1d8b767-dfb3-4588-9fa0-8a97e5337184", + "ip": "127.0.0.1", + "user_agent": "curl/8.7.1", + "user_id": "", + "request_body": "{\"username\":\"testuser\",\"email\":\"test@example.com\"}", + "response_body": "{\"code\":1001,\"data\":null,\"msg\":\"缺失认证令牌\",\"timestamp\":\"2025-11-11T17:45:03+08:00\"}" +} +``` + +**Logger 中间件实现要求:** + +```go +// pkg/logger/middleware.go +func Middleware() fiber.Handler { + return func(c *fiber.Ctx) error { + startTime := time.Now() + + // 在 c.Next() 之前读取请求 body + requestBody := truncateBody(c.Body(), MaxBodyLogSize) + queryParams := string(c.Request().URI().QueryString()) + + // 处理请求(可能被中间件短路返回) + err := c.Next() + + // 在 c.Next() 之后读取响应 body(无论是否短路) + responseBody := truncateBody(c.Response().Body(), MaxBodyLogSize) + + // 记录完整的访问日志(包含请求和响应参数) + accessLogger.Info("", + zap.String("method", c.Method()), + zap.String("path", c.Path()), + zap.String("query", queryParams), + zap.Int("status", c.Response().StatusCode()), + zap.Float64("duration_ms", time.Since(startTime).Seconds()*1000), + zap.String("request_id", getRequestID(c)), + zap.String("ip", c.IP()), + zap.String("user_agent", c.Get("User-Agent")), + zap.String("user_id", getUserID(c)), + zap.String("request_body", requestBody), + zap.String("response_body", responseBody), + ) + + return err + } +} +``` + +**禁止的做法:** + +```go +// ❌ 在中间件中跳过日志记录 +func AuthMiddleware() fiber.Handler { + return func(c *fiber.Ctx) error { + if !isAuthenticated(c) { + // ❌ 直接返回,没有记录到 access.log + return c.Status(401).JSON(fiber.Map{"error": "unauthorized"}) + } + return c.Next() + } +} + +// ❌ 只在部分路由记录日志 +app.Use("/api/v1/users", logger.Middleware()) // ❌ 其他路由没有日志 + +// ❌ 不记录请求/响应 body +accessLogger.Info("", + zap.String("method", c.Method()), + zap.String("path", c.Path()), + zap.Int("status", c.Response().StatusCode()), + // ❌ 缺少 request_body 和 response_body +) +``` + +**理由 (RATIONALE):** + +完整的访问日志是系统可观测性的基础,对于以下场景至关重要: + +1. **问题排查**:当用户报告错误时,通过 request_id 可以追溯完整的请求/响应数据,快速定位问题 +2. **安全审计**:记录所有请求(包括认证失败、参数验证失败等)可以追踪潜在的安全攻击 +3. **性能分析**:通过 duration_ms 和请求参数可以分析慢查询和性能瓶颈 +4. **合规要求**:某些行业(金融、医疗等)要求完整的操作审计日志 +5. **用户行为分析**:通过 user_id 和请求参数可以分析用户行为模式 +6. **无例外原则**:确保没有请求"逃脱"日志记录,避免日志盲点 + +通过强制在 Logger 中间件中统一记录,确保: +- 中间件的短路返回(auth 失败、rate limit 等)不会绕过日志 +- 所有请求的日志格式统一,便于日志分析工具处理 +- 50KB 限制平衡了日志完整性和存储成本 + +--- + ## Development Workflow (开发工作流程) ### 分支管理 @@ -964,6 +1092,7 @@ README.md # 项目主文档 - 性能影响可接受 - 代码通过 `gofmt`、`go vet`、`golangci-lint` 检查 - **文档已按规范更新(docs/ 和 README.md)** + - **访问日志记录符合规范(所有请求都被记录,包含请求/响应 body)** --- @@ -990,6 +1119,9 @@ README.md # 项目主文档 - [ ] **使用 Go 惯用并发模式**(goroutines/channels,无线程池类) - [ ] **功能总结文档已创建**(在 `docs/{feature-id}/` 目录下,中文命名和内容) - [ ] **README.md 已更新**(添加功能简短描述,2-3 句话) +- [ ] **访问日志验证通过**(所有请求被记录到 access.log,包含完整请求/响应参数) +- [ ] **访问日志格式正确**(包含所有必需字段:method, path, query, status, duration_ms, request_id, ip, user_agent, user_id, request_body, response_body) +- [ ] **中间件不绕过日志**(认证失败、限流等短路返回也被记录) ### 上线前检查 @@ -998,6 +1130,7 @@ README.md # 项目主文档 - [ ] 数据库迁移在 staging 环境验证通过 - [ ] 监控和告警配置完成 - [ ] 回滚方案已准备 +- [ ] 访问日志轮转配置正确(防止日志文件过大) --- @@ -1025,12 +1158,17 @@ README.md # 项目主文档 - 特别关注 Go 惯用法原则,拒绝 Java 风格的代码 - 特别关注常量使用规范,拒绝不必要的硬编码 - 特别关注文档规范,确保每个功能都有完整的中文文档 +- 特别关注访问日志规范,确保所有请求都被完整记录 - 任何复杂性增加(新依赖、新架构层)**MUST** 在设计文档中明确说明必要性 ### 运行时开发指导 -开发时参考本宪章确保一致性。如有疑问,优先遵守原则,再讨论例外情况。记住:**写 Go 代码,不是用 Go 语法写 Java**。记住:**定义常量是为了使用,不是为了装饰**。记住:**写文档是为了团队协作,不是为了应付检查**。 +开发时参考本宪章确保一致性。如有疑问,优先遵守原则,再讨论例外情况。记住: +- **写 Go 代码,不是用 Go 语法写 Java** +- **定义常量是为了使用,不是为了装饰** +- **写文档是为了团队协作,不是为了应付检查** +- **记录日志是为了可观测性,不能有例外** --- -**Version**: 2.2.0 | **Ratified**: 2025-11-10 | **Last Amended**: 2025-11-11 +**Version**: 2.3.0 | **Ratified**: 2025-11-10 | **Last Amended**: 2025-11-11 diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md index 0b7d137..36518b5 100644 --- a/.specify/templates/plan-template.md +++ b/.specify/templates/plan-template.md @@ -108,6 +108,15 @@ - [ ] Uses `context.Context` for timeout control - [ ] Uses `sync.Pool` for frequently allocated objects +**Access Logging Standards** (Constitution Principle VIII): +- [ ] ALL HTTP requests logged to access.log without exception +- [ ] Request parameters (query + body) logged (limited to 50KB) +- [ ] Response parameters (body) logged (limited to 50KB) +- [ ] Logging happens via centralized Logger middleware (pkg/logger/Middleware()) +- [ ] No middleware bypasses access logging (including auth failures, rate limits) +- [ ] Body truncation indicates "... (truncated)" when over 50KB limit +- [ ] Access log includes all required fields: method, path, query, status, duration_ms, request_id, ip, user_agent, user_id, request_body, response_body + ## Project Structure ### Documentation (this feature) diff --git a/.specify/templates/tasks-template.md b/.specify/templates/tasks-template.md index 9cf5030..8e15964 100644 --- a/.specify/templates/tasks-template.md +++ b/.specify/templates/tasks-template.md @@ -201,6 +201,12 @@ Foundational tasks for 君鸿卡管系统 tech stack: - [ ] TXXX Quality Gate: Verify feature summary docs created in docs/{feature-id}/ with Chinese filenames - [ ] TXXX Quality Gate: Verify summary doc content uses Chinese - [ ] TXXX Quality Gate: Verify README.md updated with brief feature description (2-3 sentences) +- [ ] TXXX Quality Gate: Verify ALL HTTP requests logged to access.log (no exceptions) +- [ ] TXXX Quality Gate: Verify access log includes request parameters (query + body, limited to 50KB) +- [ ] TXXX Quality Gate: Verify access log includes response parameters (body, limited to 50KB) +- [ ] TXXX Quality Gate: Verify logging via centralized Logger middleware (pkg/logger/Middleware()) +- [ ] TXXX Quality Gate: Verify no middleware bypasses logging (test auth failures, rate limits, etc.) +- [ ] TXXX Quality Gate: Verify access log has all required fields (method, path, query, status, duration_ms, request_id, ip, user_agent, user_id, request_body, response_body) --- diff --git a/configs/config.dev.yaml b/configs/config.dev.yaml index 6c8896b..94d4776 100644 --- a/configs/config.dev.yaml +++ b/configs/config.dev.yaml @@ -33,8 +33,8 @@ logging: compress: false middleware: - enable_auth: false # 开发环境可选禁用认证 - enable_rate_limiter: false + enable_auth: true # 开发环境可选禁用认证 + enable_rate_limiter: true rate_limiter: max: 1000 expiration: "1m" diff --git a/internal/handler/health.go b/internal/handler/health.go index 04ba1a7..241794b 100644 --- a/internal/handler/health.go +++ b/internal/handler/health.go @@ -4,12 +4,15 @@ import ( "time" "github.com/gofiber/fiber/v2" + "go.uber.org/zap" + "github.com/break/junhong_cmp_fiber/pkg/logger" "github.com/break/junhong_cmp_fiber/pkg/response" ) // HealthCheck 健康检查处理器 func HealthCheck(c *fiber.Ctx) error { + logger.GetAppLogger().Info("我还活着!!!!", zap.String("time", time.Now().Format(time.RFC3339))) return response.Success(c, fiber.Map{ "status": "healthy", "timestamp": time.Now().Format(time.RFC3339), diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go index 1b03323..740c9c9 100644 --- a/pkg/logger/logger.go +++ b/pkg/logger/logger.go @@ -1,6 +1,8 @@ package logger import ( + "os" + "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" @@ -25,17 +27,17 @@ func InitLoggers( // 创建编码器配置 encoderConfig := zapcore.EncoderConfig{ - TimeKey: "timestamp", + TimeKey: "time", LevelKey: "level", NameKey: "logger", CallerKey: "caller", - MessageKey: "message", + MessageKey: "msg", StacktraceKey: "stacktrace", LineEnding: zapcore.DefaultLineEnding, - EncodeLevel: zapcore.LowercaseLevelEncoder, - EncodeTime: zapcore.ISO8601TimeEncoder, // RFC3339 格式 + EncodeLevel: zapcore.CapitalColorLevelEncoder, // 使用彩色级别编码器 + EncodeTime: zapcore.ISO8601TimeEncoder, // 2025-11-11T17:50:52.830+0800 格式 EncodeDuration: zapcore.SecondsDurationEncoder, - EncodeCaller: zapcore.ShortCallerEncoder, + EncodeCaller: zapcore.ShortCallerEncoder, // 输出 middleware/trace.go:58 格式 } // 选择编码器(开发模式使用控制台,生产使用 JSON) @@ -46,10 +48,21 @@ func InitLoggers( encoder = zapcore.NewJSONEncoder(encoderConfig) } + // 创建应用日志写入器 + appWriter := zapcore.AddSync(newLumberjackLogger(appLogConfig)) + + // 开发模式下同时输出到控制台 + if development { + appWriter = zapcore.NewMultiWriteSyncer( + appWriter, + zapcore.AddSync(os.Stdout), + ) + } + // 创建应用日志核心 appCore := zapcore.NewCore( encoder, - zapcore.AddSync(newLumberjackLogger(appLogConfig)), + appWriter, zapLevel, ) diff --git a/pkg/logger/middleware.go b/pkg/logger/middleware.go index a59b01b..2a3ec07 100644 --- a/pkg/logger/middleware.go +++ b/pkg/logger/middleware.go @@ -8,14 +8,39 @@ import ( "go.uber.org/zap" ) +const ( + // MaxBodyLogSize 限制记录的请求/响应 body 大小为 50KB + MaxBodyLogSize = 50 * 1024 +) + +// truncateBody 截断 body 到指定大小 +func truncateBody(body []byte, maxSize int) string { + if len(body) == 0 { + return "" + } + + if len(body) <= maxSize { + return string(body) + } + + // 超过限制,截断并添加提示 + return string(body[:maxSize]) + "... (truncated)" +} + // Middleware 创建 Fiber 日志中间件 -// 记录所有 HTTP 请求到访问日志 +// 记录所有 HTTP 请求到访问日志(包括请求和响应 body) func Middleware() fiber.Handler { return func(c *fiber.Ctx) error { // 记录请求开始时间 startTime := time.Now() c.Locals(constants.ContextKeyStartTime, startTime) + // 获取请求 body(在 c.Next() 之前读取) + requestBody := truncateBody(c.Body(), MaxBodyLogSize) + + // 获取 query 参数 + queryParams := string(c.Request().URI().QueryString()) + // 处理请求 err := c.Next() @@ -34,17 +59,23 @@ func Middleware() fiber.Handler { userID = uid.(string) } + // 获取响应 body + responseBody := truncateBody(c.Response().Body(), MaxBodyLogSize) + // 记录访问日志 accessLogger := GetAccessLogger() accessLogger.Info("", zap.String("method", c.Method()), zap.String("path", c.Path()), + zap.String("query", queryParams), zap.Int("status", c.Response().StatusCode()), zap.Float64("duration_ms", float64(duration.Microseconds())/1000.0), zap.String("request_id", requestID), zap.String("ip", c.IP()), zap.String("user_agent", c.Get("User-Agent")), zap.String(constants.ContextKeyUserID, userID), + zap.String("request_body", requestBody), + zap.String("response_body", responseBody), ) return err