--- name: Logging System Redesign overview: Replace logrus with zerolog, remove mode config, redesign logLevel/logFile, simplify Error to pure storage, add DBState for atomic DB state access, and provide a clean application-layer logging API. todos: - id: add-zerolog-dep content: 添加 zerolog 依赖到 go.mod,移除 logrus(vendor 中 wechat 等三方库自带的 logrus 不影响) status: completed - id: rewrite-log-package content: 重写 log/ 包:基于 zerolog 实现 Logger 结构体、NewLogger 工厂、CallerMarshalFunc 智能调用者过滤、TemplateFileWriter 带缓冲和模板路径切换 status: completed - id: delete-error-add-dbstate content: 删除 common/error.go(Error 类型不再需要)、新增 common/dbstate.go(DBState 包装 Query+Data+Error)、Logger 内置 ERROR 级别环形缓冲 status: completed - id: update-application content: 改造 application.go:删除 mode 配置、Application.Log 类型变更、SetConfig 中日志初始化用 logLevel 控制一切、DB 缓存始终开启、Cache-Control 跟 logLevel 联动 status: completed - id: update-db content: 改造 db/db.go 和 db/query.go:用 DBState 替换 LastQuery+LastData+LastErr、HoTimeDB.Log 类型变更、SQL 日志格式优化、消除重复打印 status: completed - id: update-var-config content: 更新 var.go 默认配置(删 mode、logLevel 默认 3)和 ConfigNote、example 配置文件 status: completed - id: update-codegen content: 适配 code/makecode.go 中的 Error 使用(移除 Logger 相关) status: completed - id: update-tests content: 适配 testing_helper.go 和 db/transaction_test.go 中的日志创建 status: completed isProject: false --- # HoTime 日志系统全面重写方案 ## 一、现状问题分析 ### 1. logrus 已不适合继续使用 - logrus 已进入 **维护模式**,不再开发新功能,仅修复安全问题 - 性能差:依赖 mutex 锁和反射,在高并发下显著慢于现代替代品 - 当前使用版本 `v1.8.1`([go.mod](go.mod) line 13) ### 2. 现有日志配置形同虚设 - `logLevel` 在 [var.go](var.go) line 59 有定义和注释,但 **从未在代码中接入 logrus** - `mode` 承担了过多职责(0/1/2/3 四种),但代码中实际只区分 `== 0` 和 `!= 0`,且 mode 2/3 无独立分支 ### 3. Error 类型与日志耦合导致重复打印 - `SetError` 内部隐式 `Warn` 打印 + `query.go` defer 再打一次 = **同一错误打印两次** - `LastQuery`/`LastData`/`LastErr` 用不同的锁保护,存在读到不一致状态的风险 --- ## 二、日志库选型:zerolog **推荐 [zerolog](https://github.com/rs/zerolog)**: - 零内存分配,性能远超 logrus(快 5-10 倍) - 内置 `Caller()`、`ConsoleWriter`(彩色)、`MultiLevelWriter`(多输出) - 无 Go 版本要求(go.mod 保持 `go 1.16`) - 不选 `log/slog`:需 Go 1.21+,升级成本太大 --- ## 三、配置参数重新设计 ### 删除 `mode` `mode` 原来控制的行为全部转移: - **DB 缓存**:`mode` 的拦截是多余的,cache 本身已是"配置即启用"设计(`cache.memory.db`/`cache.redis.db` 等控制)。删除 `mode` 后 `SetCache` 中始终挂载 `that.Db.HoTimeCache`,是否真正缓存由 cache 配置决定 - **Cache-Control**:跟 `logLevel` 联动 -- `logLevel > 0` 时设 `no-cache`,`logLevel == 0` 时 `public` - **SQL 日志打印**:完全由 `logLevel` 控制(`logLevel > 0` 时打印 SQL) - **代码生成**:本来就由 `codeConfig[].mode` 独立控制,不受影响 ### `logLevel` -- 保留,真正接管日志系统 - `0` = 仅 error(生产环境推荐) - `>=1` = 全部打印(info/warn/error/debug/SQL 全开) - **默认值 `1`** 简单二分:要么只看错误,要么全看。Cache-Control 同理:`logLevel==0` 时 `public`,`>0` 时 `no-cache`。 ### `logFile` -- 保留 路径模板不变(`a/b/c/20060102150405.txt`),应用日志写入此文件。 ### `logHistory` -- 新增 Logger 错误历史缓冲条数,默认 `100`。设为 `0` 关闭历史记录。可根据设备内存情况调小。 ### `webConnectLogShow` / `webConnectLogFile` -- 保留独立 保留访问日志独立的理由(业务角度): - 访问日志量远大于应用日志,混在一起文件膨胀、查找困难 - 生产环境需要不同的轮转策略 - Nginx/Apache 都是 access.log 和 error.log 分开的行业惯例 配置保持不变: - `webConnectLogShow`:默认 true - `webConnectLogFile`:独立文件路径 --- ## 四、新日志包设计 (`log/`) ### 核心结构 ```go package log type Logger struct { zl zerolog.Logger } func NewLogger(logLevel int, logFile string, showCaller bool) *Logger func (l *Logger) Debug() *zerolog.Event func (l *Logger) Info() *zerolog.Event func (l *Logger) Warn() *zerolog.Event func (l *Logger) Error() *zerolog.Event func (l *Logger) Debugf(format string, v ...interface{}) func (l *Logger) Infof(format string, v ...interface{}) func (l *Logger) Warnf(format string, v ...interface{}) func (l *Logger) Errorf(format string, v ...interface{}) ``` ### 控制台输出格式 ``` 2026-04-12 15:04:05 |INFO| 用户登录成功 [app/admin/user.go:45] 2026-04-12 15:04:05 |WARN| 数据库连接超时 [app/admin/user.go:78] 2026-04-12 15:04:05 |ERROR| 支付回调失败 order_id=456 [app/payment/callback.go:32] ``` 格式:`时间 | 类型标志 | 日志内容 | [代码位置]`,用 `[file:line]` 而非 logrus 的 `line="file:line"`。 ### 文件输出格式(JSON) ```json {"time":"2026-04-12T15:04:05+08:00","level":"info","caller":"app/admin/user.go:45","message":"用户登录成功"} ``` ### 调用者智能过滤 复用并优化现有 [log/logrus.go](log/logrus.go) 中 `findCaller` / `isHoTimeFrameworkFile` 逻辑,实现为 zerolog 的 `CallerMarshalFunc`。 ### 文件写入优化 `TemplateFileWriter`:带缓冲的 Writer,按时间模板自动切换文件路径,保持 `time.Now().Format(path)` 语义。 --- ## 五、删除 Error 类型 + DBState + Logger 错误历史 ### 删除 common.Error `common.Error` 完全删除,不再需要。原因: - Error 存在的意义是"存错误 + 自动打印",打印已移到 Log,只剩存储 - 审查全部用法后,每个 `SetError` 调用点都可以直接用 `Log.Error()` 替代 - `InitDb` 返回 `*Error` 从未被使用(调用方都是 `_ = that.InitDb(...)`) - `ConnectFunc(err ...*Error)` 可简化为返回 error - 需要"查最近的错误"由 Logger 错误历史替代 ### Logger 错误历史(环形缓冲) Logger 内置 ERROR 级别记录的环形缓冲(默认 100 条),打印即存储,零额外操作: ```go type ErrorRecord struct { Err error Msg string Time time.Time Caller string } // Logger 内部 type Logger struct { zl zerolog.Logger errors []ErrorRecord // 环形缓冲 errorsMu sync.RWMutex maxErrors int // 默认 100 } // 每次 Log.Error() 自动存入缓冲 // 获取最近 N 条错误(不传则返回全部已存储的,不超过 maxErrors) func (l *Logger) GetRecentErrors(n ...int) []ErrorRecord ``` 缓冲大小通过配置项 `logHistory` 控制,默认 100,可根据设备内存调整。 应用层使用: ```go // 正常打印,自动存入历史 that.Log.Error().Err(err).Msg("支付失败") // 获取最近 10 条错误 for _, e := range that.Log.GetRecentErrors(10) { fmt.Printf("[%s] %s: %v %s\n", e.Time.Format("15:04:05"), e.Msg, e.Err, e.Caller) } // 获取全部已存储的错误 all := that.Log.GetRecentErrors() ``` ### DBState -- DB 操作状态原子包装(新增) 新增 [common/dbstate.go](common/dbstate.go),将 `LastQuery` + `LastData` + `LastErr` 合为一体,一把锁保护原子读写。 **核心语义**:记录最后一次 DB 操作的完整状态。每次 SQL 执行后更新,用于判断"SQL 执行失败"还是"执行成功但结果为空"(如 UPDATE 影响 0 行)。 ```go // DBSnapshot 操作状态快照(只读,安全传递) type DBSnapshot struct { Query string Data []interface{} Err error } type DBState struct { query string data []interface{} err error mu sync.RWMutex // 零值即可用 } // Set 原子更新完整状态(每次 Query/Exec 执行后调用) func (s *DBState) Set(query string, data []interface{}, err error) { s.mu.Lock() s.query = query s.data = data s.err = err s.mu.Unlock() } // Get 返回一致性快照(query/data/err 保证是同一次操作的) func (s *DBState) Get() DBSnapshot { s.mu.RLock() defer s.mu.RUnlock() return DBSnapshot{Query: s.query, Data: s.data, Err: s.err} } // SetError 只更新错误(Ping 等无 SQL 的场景) func (s *DBState) SetError(err error) { s.mu.Lock() s.err = err s.mu.Unlock() } // GetError 快速判断最后操作是否出错 func (s *DBState) GetError() error { s.mu.RLock() defer s.mu.RUnlock() return s.err } ``` **使用场景**: ```go db.Exec("UPDATE stats SET count=count+1 WHERE id=?", id) rows, _ := result.RowsAffected() if rows == 0 { if state := db.GetLastState(); state != nil { // SQL 执行失败,state.Err 有具体错误 } else { // SQL 成功,只是没有匹配的行 } } ``` ### HoTimeDB 结构体变更 `HoTimeDB` 中原来的三个独立字段合并为私有 `lastState`,通过方法暴露,同时删除 `Mode`: ```go type HoTimeDB struct { *sql.DB // ... Log *log.Logger // 从 *logrus.Logger 改为新 Logger lastState DBState // 私有,替换 LastQuery + LastData + LastErr // 删除: Mode int // 删除: LastQuery string // 删除: LastData []interface{} // 删除: LastErr *Error } // 唯一对外方法:nil = 成功,非 nil = 失败(附带完整上下文) func (db *HoTimeDB) GetLastState() *DBSnapshot ``` 内部逻辑:`lastState` 始终记录 query/data/err(供日志使用),但 `GetLastState()` 只在有错误时返回非 nil: ```go func (db *HoTimeDB) GetLastState() *DBSnapshot { db.lastState.mu.RLock() defer db.lastState.mu.RUnlock() if db.lastState.err == nil { return nil // 没错误,不需要关心细节 } return &DBSnapshot{ Query: db.lastState.query, Data: db.lastState.data, Err: db.lastState.err, } } ``` 应用层使用: ```go // 一行判断 + 取详情 if state := db.GetLastState(); state != nil { // SQL 出错了,state.Err / state.Query / state.Data 全有 that.Log.Error().Err(state.Err).Str("sql", state.Query).Msg("SQL 执行失败") } // nil = SQL 执行成功,不用管 ``` --- ## 六、DB 日志格式优化 ### 修复重复打印 `SetError` 不再打印日志,`query.go` defer 统一负责 SQL 日志 -- 问题根除。 ### 输出格式 **无错误时(DEBUG 级别):** ``` 2026-04-12 15:04:05 |DEBUG| SQL: SELECT id,worker_id FROM `dingtalk` WHERE `state`=? DATA: [0] [dd/sync.go:63] ``` **有错误时(自动升为 ERROR 级别):** ``` 2026-04-12 15:04:05 |ERROR| SQL: SELECT id,worker_id FROM `dingtalk` WHERE `state`=? DATA: [0] ERROR: connection refused [dd/sync.go:63] ``` 关键改动: - 无错误时不显示 `ERROR:` - 有错误时日志级别自动升为 ERROR(而非一律 INFO) - SQL 日志在 `logLevel > 0` 时输出,`logLevel == 0` 时只有 SQL 出错才打印 ### 测试模式下 SQL 错误自动 fail 测试框架中 SQL 出错让测试用例直接失败报错。 --- ## 七、需要修改的文件清单 ### 核心改动 - [log/logrus.go](log/logrus.go) -- 全部重写为 zerolog 实现,重命名为 `log/logger.go` - [common/error.go](common/error.go) -- **删除整个文件** - 新增 [common/dbstate.go](common/dbstate.go) -- DBState + DBSnapshot 类型 - [application.go](application.go) -- 删除 `Error` 嵌入和所有 `Error{}` 初始化、删除 mode 逻辑、`Application.Log` 类型变更、`SetCache` 去掉 mode 判断、Cache-Control 改跟 logLevel、删除 `Db.Mode` 赋值(3 处)、`ConnectFunc` 签名简化、logLevel 接入 - [var.go](var.go) -- 删除 `"mode": 2` 默认值、ConfigNote 删除 mode 说明、logLevel 默认值改为 3 并更新说明 - [db/db.go](db/db.go) -- 删除 `Mode int` / `LastQuery` / `LastData` / `LastErr` 字段,改为 `LastState DBState` + `Log *log.Logger`,移除 logrus import - [db/query.go](db/query.go) -- 用 `LastState.Set()` 原子写入替换分散的赋值、SQL 日志格式优化(无错不显示 ERROR、有错升 ERROR 级别)、`Mode != 0` 改为 Logger 级别过滤 - [go.mod](go.mod) -- 添加 zerolog 依赖 ### 适配改动 - [db/crud.go](db/crud.go) -- `that.LastErr.SetError(e)` 改为 `that.lastState.SetError(e)` - [db/transaction.go](db/transaction.go) -- 事务拷贝中 `LastQuery`/`LastData`/`LastErr` 改为 `lastState` - [db/transaction_test.go](db/transaction_test.go) -- 删除 `Error{Logger: logger}`、logrus 替换 - [code/makecode.go](code/makecode.go) -- 删除嵌入的 `Error`,改用 `*Logger` 打印错误 - [cache/cache.go](cache/cache.go) -- `Init` 参数删除 `*Error`,改传 `*Logger` - [cache/cache_redis.go](cache/cache_redis.go) -- `that.Error.SetError(err)` 全部改为 `that.Log.Error().Err(err).Msg(...)` - [testing_helper.go](testing_helper.go) -- `app.Db.Mode` 删除、Logger 创建适配 - [context.go](context.go) -- `that.Error.SetError(...)` 改为 `that.Log.Error().Msg(...)` - [example/app/test.go](example/app/test.go) -- `that.Db.LastQuery` 改为 `that.Db.GetLastState()` 相关访问 - [example/app/mysql.go](example/app/mysql.go) -- 同上 - [example/batch_cache_tester.go](example/batch_cache_tester.go) -- `&Error{}` 删除 - [example/config/configNote.json](example/config/configNote.json) -- 删除 mode 说明、更新 logLevel 说明 - [example/config/config.json](example/config/config.json) -- 删除 mode --- ## 八、应用层使用示例 ```go // 基本使用 that.Log.Infof("用户 %s 登录成功", name) that.Log.Errorf("订单 %d 支付失败: %v", orderId, err) // 链式调用(附加结构化字段) that.Log.Info().Str("user", name).Int("age", 25).Msg("用户登录成功") that.Log.Error().Err(err).Str("order", orderId).Msg("支付回调失败") // 打印即存储(Log.Error 自动存入错误历史) that.Log.Error().Err(err).Msg("数据库连接失败") // DB 状态:nil = 成功,非 nil = 失败 if state := that.Db.GetLastState(); state != nil { that.Log.Error().Err(state.Err).Str("sql", state.Query).Msg("SQL 失败") } // 调阅最近 5 条错误(调试用,包括 DB 错误在内的所有 ERROR 级别日志) errors := that.Log.GetRecentErrors(5) // SQL 日志由框架自动处理,logLevel>0 时输出 // 无错误: |INFO| SQL: SELECT... DATA: [1,2] [app/user.go:45] // 有错误: |ERROR| SQL: SELECT... DATA: [1,2] ERROR: xxx [app/user.go:45] ``` --- ## 九、向后兼容与影响范围 ### 编译级 breaking changes - `common.Error` 类型完全删除:所有嵌入 `Error` 或传递 `*Error` 的地方改为 `*Logger` 或 `error` - `Application.Log` 从 `*logrus.Logger` 改为 `*log.Logger` - `Application.Error` 字段删除 - `ConnectFunc` 签名简化(不再传递 `*Error`) - `HoTimeDB.LastQuery`/`LastData`/`LastErr`/`Mode` 删除: - 应用层通过 `db.GetLastState()` 获取(nil=成功) - 框架内部通过 `db.lastState.SetError(e)` 更新 - 删除 `mode` 配置 ### 不受影响 - `codeConfig[].mode`(代码生成模式)完全不受影响 - `modeRouterStrict`(路由严格模式)完全不受影响 - `cache` 配置结构不受影响(`cache.db.mode` 等是缓存自己的 mode,与全局 mode 无关) - `dri/mongodb` 的 `LastErr error` 是独立的普通 error,不受影响