refactor(logging): 迁移日志记录到 Zerolog 并优化错误处理

- 将日志记录库从 Logrus 替换为 Zerolog,提升性能和灵活性
- 更新各个模块的日志记录方式,确保一致性
- 优化错误处理逻辑,确保在发生错误时能够正确记录并传递错误信息
- 移除不再使用的错误处理字段,简化代码结构
- 更新相关文档以反映新的日志记录和错误处理机制
This commit is contained in:
2026-04-13 00:38:50 +08:00
parent 86daa3153e
commit 9a9b9c83ff
445 changed files with 59349 additions and 13297 deletions
@@ -0,0 +1,424 @@
---
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,移除 logrusvendor 中 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.goError 类型不再需要)、新增 common/dbstate.goDBState 包装 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:<nil>`
- 有错误时日志级别自动升为 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,不受影响