0991555c2d
- 将日志记录库从 Logrus 替换为 Zerolog,提升性能和灵活性 - 更新各个模块的日志记录方式,确保一致性 - 优化错误处理逻辑,确保在发生错误时能够正确记录并传递错误信息 - 移除不再使用的错误处理字段,简化代码结构 - 更新相关文档以反映新的日志记录和错误处理机制
425 lines
16 KiB
Markdown
425 lines
16 KiB
Markdown
---
|
||
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:<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,不受影响
|