应初始化线程安全的独立 logrus 实例:var log = logrus.New(),避免使用全局 logrus 函数以防配置冲突;设置输出用 log.SetOutput();高频日志复用 WithFields 封装的 entry;需 caller 信息时用专用 hook 并验证 CPU 开销;生产环境强制 JSONFormatter。

别用 logrus.Info() 这类全局函数初始化日志,它会导致多模块配置互相覆盖,线上日志格式错乱、级别失效——这是 Go 项目集成 Logrus 最常见的崩溃点。
为什么不能直接用 logrus.xxx 全局函数
全局 logger 是单例,所有包调用 logrus.SetLevel() 或 logrus.SetFormatter() 都会修改同一实例。A 包设成 JSONFormatter,B 包又切回 TextFormatter,日志输出就变成混合体;C 包调 logrus.SetLevel(log.WarnLevel),整个服务的 info 日志全消失。
- 微服务或中大型项目里,依赖库可能自带 logrus 初始化逻辑,你无法控制其执行顺序
-
logrus.WithFields()返回的新*logrus.Entry仍绑定全局 logger,字段注入后依然受全局 level 和 formatter 控制 - 测试时并发启动多个服务实例,
logrus.SetOutput()可能被反复覆盖,导致日志写入错位甚至 panic
正确创建线程安全的独立 logger 实例
每个模块、每个 HTTP 服务、甚至每个关键子系统,都该持有自己的 *logrus.Logger 实例,通过变量名明确归属(如 authLog、paymentLog)。
- 用
logrus.New()创建,不要复用logrus.StandardLogger() - 输出目标必须走
log.SetOutput()设置,而非直接赋值log.Out = xxx(后者跳过内部 hook 初始化) - 文件输出务必加
os.O_APPEND和错误检查:os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644),否则重启后日志从头覆盖 - 生产环境强制设
log.SetFormatter(&logrus.JSONFormatter{}),文本格式无法被 Loki / Fluentd 等采集器结构化解析
高频日志场景下避免 WithField 性能陷阱
HTTP 中间件、gRPC 拦截器这类每请求必打日志的位置,log.WithField("req_id", id).Info("start") 看似简洁,实则每次调用都 new 一个 *logrus.Entry,GC 压力陡增。
立即学习“go语言免费学习笔记(深入)”;
- 改用
log.WithFields(log.Fields{"req_id": id, "path": r.URL.Path})一次性构造 entry,后续复用该变量调用.Info()、.Error() - 字段值只传基础类型(
string/int/bool),别传 struct 或指针——logrus 会深拷贝,且遇到未导出字段可能 panic - 不要在每条 info 日志里无脑塞
user_id、ip:真正需要上下文关联时才加;通用字段可提前提取到中间件 entry 中统一注入
怎么让日志带文件名和行号
logrus 默认不记录 file:line,不是 bug,是为避免 runtime.Callers 带来的性能抖动。但排障时缺这个信息非常痛苦,得靠第三方 hook 补齐。
- 用
github.com/onrik/logrus/filename:注册 hook 后自动注入file和line字段,不改业务代码 - hook 必须在
log.SetFormatter()之后添加,否则字段不会出现在 JSON 输出里 - 别自己写
runtime.Caller逻辑塞进Formatter:Hook 执行时机在格式化之后,此时再改entry.Data不影响最终输出内容 - 注意
filename.NewHook()默认字段名是file,可调Field属性改成source之类更符合团队规范的名称
最易被忽略的是 Hook 的并发安全性——如果你在 Hook 的 Fire() 方法里做计数或写共享 map,没加 sync.Mutex 就会丢数据;而 Hook 内发起 HTTP 上报又不加 goroutine 异步,主线程就会卡住。这两处不处理,日志系统本身就成了故障源。


















