- 将日志记录库从 Logrus 替换为 Zerolog,提升性能和灵活性 - 更新各个模块的日志记录方式,确保一致性 - 优化错误处理逻辑,确保在发生错误时能够正确记录并传递错误信息 - 移除不再使用的错误处理字段,简化代码结构 - 更新相关文档以反映新的日志记录和错误处理机制
16 KiB
name, overview, todos, isProject
| name | overview | todos | isProject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Logging System Redesign | 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. |
|
false |
HoTime 日志系统全面重写方案
一、现状问题分析
1. logrus 已不适合继续使用
- logrus 已进入 维护模式,不再开发新功能,仅修复安全问题
- 性能差:依赖 mutex 锁和反射,在高并发下显著慢于现代替代品
- 当前使用版本
v1.8.1(go.mod line 13)
2. 现有日志配置形同虚设
logLevel在 var.go line 59 有定义和注释,但 从未在代码中接入 logrusmode承担了过多职责(0/1/2/3 四种),但代码中实际只区分== 0和!= 0,且 mode 2/3 无独立分支
3. Error 类型与日志耦合导致重复打印
SetError内部隐式Warn打印 +query.godefer 再打一次 = 同一错误打印两次LastQuery/LastData/LastErr用不同的锁保护,存在读到不一致状态的风险
二、日志库选型:zerolog
推荐 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:默认 truewebConnectLogFile:独立文件路径
四、新日志包设计 (log/)
核心结构
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)
{"time":"2026-04-12T15:04:05+08:00","level":"info","caller":"app/admin/user.go:45","message":"用户登录成功"}
调用者智能过滤
复用并优化现有 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 条),打印即存储,零额外操作:
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,可根据设备内存调整。
应用层使用:
// 正常打印,自动存入历史
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,将 LastQuery + LastData + LastErr 合为一体,一把锁保护原子读写。
核心语义:记录最后一次 DB 操作的完整状态。每次 SQL 执行后更新,用于判断"SQL 执行失败"还是"执行成功但结果为空"(如 UPDATE 影响 0 行)。
// 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
}
使用场景:
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:
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:
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,
}
}
应用层使用:
// 一行判断 + 取详情
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 -- 全部重写为 zerolog 实现,重命名为
log/logger.go - common/error.go -- 删除整个文件
- 新增 common/dbstate.go -- DBState + DBSnapshot 类型
- application.go -- 删除
Error嵌入和所有Error{}初始化、删除 mode 逻辑、Application.Log类型变更、SetCache去掉 mode 判断、Cache-Control 改跟 logLevel、删除Db.Mode赋值(3 处)、ConnectFunc签名简化、logLevel 接入 - var.go -- 删除
"mode": 2默认值、ConfigNote 删除 mode 说明、logLevel 默认值改为 3 并更新说明 - db/db.go -- 删除
Mode int/LastQuery/LastData/LastErr字段,改为LastState DBState+Log *log.Logger,移除 logrus import - db/query.go -- 用
LastState.Set()原子写入替换分散的赋值、SQL 日志格式优化(无错不显示 ERROR、有错升 ERROR 级别)、Mode != 0改为 Logger 级别过滤 - go.mod -- 添加 zerolog 依赖
适配改动
- db/crud.go --
that.LastErr.SetError(e)改为that.lastState.SetError(e) - db/transaction.go -- 事务拷贝中
LastQuery/LastData/LastErr改为lastState - db/transaction_test.go -- 删除
Error{Logger: logger}、logrus 替换 - code/makecode.go -- 删除嵌入的
Error,改用*Logger打印错误 - cache/cache.go --
Init参数删除*Error,改传*Logger - cache/cache_redis.go --
that.Error.SetError(err)全部改为that.Log.Error().Err(err).Msg(...) - testing_helper.go --
app.Db.Mode删除、Logger 创建适配 - context.go --
that.Error.SetError(...)改为that.Log.Error().Msg(...) - example/app/test.go --
that.Db.LastQuery改为that.Db.GetLastState()相关访问 - example/app/mysql.go -- 同上
- example/batch_cache_tester.go --
&Error{}删除 - example/config/configNote.json -- 删除 mode 说明、更新 logLevel 说明
- example/config/config.json -- 删除 mode
八、应用层使用示例
// 基本使用
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或errorApplication.Log从*logrus.Logger改为*log.LoggerApplication.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,不受影响