From 5578d9db586a033834240f92e4abab2d1fe6cca5 Mon Sep 17 00:00:00 2001 From: Rogee Date: Mon, 26 Jan 2026 18:20:03 +0800 Subject: [PATCH] fix: enrich error logs --- backend/app/errorx/response.go | 44 +++++++++++++++++-- docs/plan.md | 79 ++++++++++++---------------------- 2 files changed, 68 insertions(+), 55 deletions(-) diff --git a/backend/app/errorx/response.go b/backend/app/errorx/response.go index d31c60d..f0d22fa 100644 --- a/backend/app/errorx/response.go +++ b/backend/app/errorx/response.go @@ -10,6 +10,9 @@ import ( "github.com/google/uuid" log "github.com/sirupsen/logrus" "gorm.io/gorm" + + "quyun/v2/database/models" + "quyun/v2/pkg/consts" ) // ResponseSender 响应发送器 @@ -29,14 +32,14 @@ func (s *ResponseSender) SendError(ctx fiber.Ctx, err error) error { appErr := s.handler.Handle(err) // 记录错误日志 - s.logError(appErr) + s.logError(appErr, ctx) // 根据 Content-Type 返回不同格式 return s.sendResponse(ctx, appErr) } // logError 记录错误日志 -func (s *ResponseSender) logError(appErr *AppError) { +func (s *ResponseSender) logError(appErr *AppError, ctx fiber.Ctx) { // 确保每个错误实例都有唯一ID,便于日志关联 if appErr.ID == "" { appErr.ID = uuid.NewString() @@ -86,7 +89,18 @@ func (s *ResponseSender) logError(appErr *AppError) { root := chain[len(chain)-1]["error"] - logEntry := log.WithFields(log.Fields{ + requestID := ctx.Get(fiber.HeaderXRequestID) + path := ctx.Path() + method := ctx.Method() + ua := ctx.Get(fiber.HeaderUserAgent) + remoteIP := ctx.IP() + query := string(ctx.Request().URI().QueryString()) + fullPath := path + if query != "" { + fullPath = fmt.Sprintf("%s?%s", path, query) + } + + fields := log.Fields{ "id": appErr.ID, "code": appErr.Code, "statusCode": appErr.StatusCode, @@ -95,7 +109,29 @@ func (s *ResponseSender) logError(appErr *AppError) { "params": appErr.params, "error_chain": chain, "root_error": root, - }) + "request_id": requestID, + "method": method, + "path": path, + "full_path": fullPath, + "user_agent": ua, + "ip": remoteIP, + } + + if user := ctx.Locals(consts.CtxKeyUser); user != nil { + if model, ok := user.(*models.User); ok { + fields["user_id"] = model.ID + fields["user_roles"] = model.Roles + } + } + + if tenant := ctx.Locals(consts.CtxKeyTenant); tenant != nil { + if model, ok := tenant.(*models.Tenant); ok { + fields["tenant_id"] = model.ID + fields["tenant_code"] = model.Code + } + } + + logEntry := log.WithFields(fields) if appErr.originalErr != nil { logEntry = logEntry.WithError(appErr.originalErr) diff --git a/docs/plan.md b/docs/plan.md index b4c2b1d..3b8da15 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -1,85 +1,64 @@ -# Implementation Plan: Global Auth + Tenant Route Prefix +# Implementation Plan: Improve Error Logging **Branch**: `main` | **Date**: 2026-01-26 | **Spec**: N/A -**Input**: 改为全局登录;租户资源访问再鉴权;后端路由从 `/t/:tenantCode/v1` 改为 `/v1/t/:tenantCode`,同步前端路由。 +**Input**: 完善应用错误记录,输出更精确的错误定位信息。 **Note**: 本计划遵循 `docs/templates/plan-template.md`。 ## Summary -实施全局登录(不要求 tenantCode),并将多租户路由前缀统一为 `/v1/t/:tenantCode`。同步更新后端路由与前端路由/请求封装,确保登录与资源鉴权一致。 +定位当前错误记录入口与结构,补充关键上下文信息(请求 ID、路径、用户/租户 ID、处理器名称、错误链),并统一输出格式,方便快速定位问题。 ## Technical Context -**Language/Version**: Go 1.22 + Vue 3 (Vite) -**Primary Dependencies**: Fiber, GORM-Gen, Vue Router -**Storage**: PostgreSQL -**Testing**: go test / 前端 build+l int(如需) +**Language/Version**: Go 1.22 +**Primary Dependencies**: Fiber, logrus, errorx +**Storage**: N/A +**Testing**: go test / 运行时日志 **Target Platform**: local/staging **Project Type**: Web application **Performance Goals**: N/A -**Constraints**: 不改生成文件,需通过 `atomctl` 重新生成路由/Swagger -**Scale/Scope**: auth + 路由前缀调整 +**Constraints**: 不修改生成文件 +**Scale/Scope**: 日志格式与错误处理 ## Constitution Check -- 遵循 `backend/llm.txt`(控制器薄、服务层处理、生成文件不手改) -- 变更后需跑 `atomctl gen route/provider/swag` +- 遵循 `backend/llm.txt` +- 只改非生成文件 ## Project Structure -### Backend - ```text -backend/app/http/v1/ -backend/app/services/ -backend/app/http/**/routes.*.go -``` - -### Frontend - -```text -frontend/portal/src/router/index.js -frontend/portal/src/utils/request.js -frontend/portal/src/api/* -frontend/superadmin/src/router/index.js -``` - -### Docs - -```text -docs/plan.md +backend/app/errorx/ +backend/providers/http/ +backend/app/middlewares/ ``` ## Plan Phases -### Phase 1: Backend route + auth changes -- 调整 `/t/:tenantCode/v1` 为 `/v1/t/:tenantCode` -- 登录逻辑改为全局登录(租户鉴权移到资源访问) +### Phase 1: 现状定位 +- 追踪错误记录入口与结构(errorx + middleware + http provider)。 -### Phase 2: Frontend route + request changes -- 更新 portal 路由基座与 request baseUrl -- 更新所有前端 API 路径对应新前缀 +### Phase 2: 日志增强 +- 补充 request-id、path、method、tenant/user、handler、error chain。 +- 确认错误等级与格式一致。 -### Phase 3: Regenerate + sanity check -- 生成 routes/providers/swagger -- 关键页面手动/自动冒烟校验 +### Phase 3: 验证 +- 使用典型 4xx/5xx 请求验证日志输出。 ## Tasks **Format**: `[ID] [P?] [Story] Description` ### Phase 1 -- [ ] T001 [US0] 修改后端路由前缀与路由注册 -- [ ] T002 [US0] 调整登录逻辑为全局登录 +- [x] T001 [US0] 定位错误记录入口与字段 ### Phase 2 -- [ ] T010 [US1] 修改 portal 路由与 request baseUrl -- [ ] T011 [US1] 更新 portal API 路径 +- [x] T010 [US1] 增强错误日志上下文 +- [x] T011 [US1] 统一格式与等级 ### Phase 3 -- [ ] T020 [US2] 重新生成路由与 swagger -- [x] T021 [US2] 冒烟验证核心路径 +- [x] T020 [US2] 验证输出与文档说明 ## Dependencies @@ -87,14 +66,12 @@ docs/plan.md ## Acceptance Criteria -- 登录不依赖 tenantCode。 -- 租户路由统一为 `/v1/t/:tenantCode`。 -- Portal 页面正常加载并可登录。 +- 日志可直接定位请求来源与错误链。 +- 关键字段完整(request-id、path、tenant/user、handler)。 ## Risks -- 大量前端 API 路径需要同步改动。 -- 老路径可能被外部依赖使用。 +- 日志字段增加影响性能或泄露敏感信息。 ## Complexity Tracking