4、logrus库实战:logrus安装与配置、Hook机制、Formatter定制
说到Go的日志库,logrus绝对是个绕不开的名字。我个人习惯用它来做结构化日志,因为它用起来顺手,扩展性也强。今天我们就来聊聊logrus的安装配置、Hook机制,还有Formatter定制。这些内容,说白了就是让你手里的日志工具从「能打」变成「好用」。
4.1 logrus的安装与基础配置
安装logrus很简单,一行命令搞定:
go get github.com/sirupsen/logrus
装完之后,咱们先来个最基础的用法。你想想看,很多项目一开始就是log.Println随便打,等出了问题想查日志,那叫一个痛苦。logrus能帮你从一开始就规范起来。
package main
import (
"os"
"github.com/sirupsen/logrus"
)
func main() {
log := logrus.New()
log.SetOutput(os.Stdout)
log.SetLevel(logrus.InfoLevel)
log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
log.Info("服务启动成功")
log.Warn("磁盘空间不足")
log.Error("数据库连接失败")
}
这里有几个关键点:
- SetOutput:指定日志输出位置,默认是os.Stderr。我建议你直接输出到stdout,方便容器化部署时日志采集。
- SetLevel:设置日志级别,低于该级别的日志不会输出。级别从低到高是:Trace、Debug、Info、Warn、Error、Fatal、Panic。
- SetFormatter:选择日志格式,内置有TextFormatter和JSONFormatter。
4.2 配置进阶:日志轮转与文件输出
光输出到控制台肯定不够。生产环境里,日志文件得能自动轮转,不然磁盘撑爆了都不知道。我记得有一次线上事故,就是因为日志文件太大,把磁盘打满了,服务直接挂了。从那以后,我必配日志轮转。
logrus本身不提供轮转功能,需要配合第三方库,比如lumberjack:
import (
"github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
func main() {
log := logrus.New()
log.SetOutput(&lumberjack.Logger{
Filename: "./logs/app.log",
MaxSize: 100, // 每个文件最大100MB
MaxBackups: 3, // 保留3个备份
MaxAge: 28, // 保留28天
Compress: true, // 是否压缩
})
log.SetLevel(logrus.InfoLevel)
log.SetFormatter(&logrus.JSONFormatter{})
log.Info("日志轮转已配置")
}
4.3 Hook机制:让日志飞一会儿
Hook是logrus最强大的特性之一。说白了,Hook就是钩子,能在日志事件发生时,额外执行一些操作。比如把错误日志发到钉钉、写入数据库、或者统计日志数量。
实现一个Hook很简单,只需要实现Levels()和Fire()两个方法:
type ErrorHook struct {
// 可以在这里加一些配置字段
}
// Levels 返回该Hook要监听的日志级别
func (h *ErrorHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
}
// Fire 当匹配到对应级别的日志时,会调用这个方法
func (h *ErrorHook) Fire(entry *logrus.Entry) error {
// 这里可以发告警、写数据库等
// entry.Data 里可以拿到自定义字段
// entry.Message 是日志内容
// entry.Time 是时间戳
fmt.Printf("【告警】发生错误: %s\n", entry.Message)
return nil
}
func main() {
log := logrus.New()
log.AddHook(&ErrorHook{})
log.Error("数据库连接超时") // 触发Hook
log.Info("正常日志") // 不会触发
}
你想想看,这个机制多灵活。我曾在项目中用Hook把所有的Error级别日志自动推送到企业微信机器人,运维同学再也不用盯着日志文件看了。
4.4 实战:自定义Hook实现邮件告警
咱们来写一个稍微实用点的Hook——当出现Fatal级别日志时,自动发邮件通知:
type MailHook struct {
From string
To []string
Subject string
SMTPHost string
SMTPPort int
Password string
}
func (h *MailHook) Levels() []logrus.Level {
return []logrus.Level{logrus.FatalLevel}
}
func (h *MailHook) Fire(entry *logrus.Entry) error {
// 这里用net/smtp发邮件
// 实际项目中建议用goroutine异步发送,避免阻塞主流程
go func() {
body := fmt.Sprintf("时间: %s\n级别: %s\n消息: %s\n数据: %v",
entry.Time.Format("2006-01-02 15:04:05"),
entry.Level,
entry.Message,
entry.Data,
)
// 发送邮件逻辑...
fmt.Println("发送告警邮件:", body)
}()
return nil
}
4.5 Formatter定制:打造专属日志格式
内置的TextFormatter和JSONFormatter虽然够用,但有时候我们需要更个性化的格式。比如,我想在每条日志前面加上请求ID、用户ID这些上下文信息。
实现自定义Formatter,只需要实现Format()方法:
type CustomFormatter struct{}
func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
// 构建自定义格式
// 时间 | 级别 | 请求ID | 消息
requestID := entry.Data["request_id"]
if requestID == nil {
requestID = "-"
}
logLine := fmt.Sprintf("%s | %s | %v | %s\n",
entry.Time.Format("2006-01-02 15:04:05"),
strings.ToUpper(entry.Level.String()),
requestID,
entry.Message,
)
return []byte(logLine), nil
}
func main() {
log := logrus.New()
log.SetFormatter(&CustomFormatter{})
// 使用WithField传递上下文
log.WithField("request_id", "abc-123").Info("用户登录成功")
// 输出: 2025-01-15 10:30:00 | INFO | abc-123 | 用户登录成功
}
嗯,这里要注意一点:entry.Data里可以放任意键值对,但一定要在调用日志方法之前通过WithField或WithFields设置好。我见过有人直接在Formatter里从context取值,那样耦合度太高,不推荐。
4.6 实战:结合Context传递链路信息
在微服务架构中,日志必须带上链路追踪信息。我建议你这样做:
// 在中间件中注入traceID
func Middleware(log *logrus.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
traceID := c.GetHeader("X-Trace-ID")
if traceID == "" {
traceID = uuid.New().String()
}
// 把traceID存入context
ctx := context.WithValue(c.Request.Context(), "trace_id", traceID)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
// 在业务代码中使用
func GetUserHandler(log *logrus.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
traceID := c.Request.Context().Value("trace_id").(string)
log.WithField("trace_id", traceID).Info("查询用户信息")
// ...
}
}
log.WithContext(ctx).Info("xxx"),这样业务代码里就不用每次都手动取context了。代码会清爽很多。
4.7 性能优化与避坑指南
logrus虽然好用,但性能上确实不如zap这类零分配日志库。如果你的服务对性能要求极高,可以考虑在关键路径上使用zap,非关键路径用logrus。不过对于大部分业务系统来说,logrus完全够用。
我曾经遇到过一个坑:在循环里频繁调用WithField,导致内存分配暴增。后来改成在循环外用WithFields一次性传入所有字段,性能提升明显。
最后总结几个要点:
- 日志级别要合理:开发环境用Debug,生产环境用Info或Warn,避免打太多无用日志。
- Hook不要阻塞:异步处理,或者用消息队列缓冲。
- Formatter要高效:避免在Format里做复杂计算,比如正则匹配、数据库查询等。
- 日志文件要轮转:配合lumberjack,设置合理的MaxSize和MaxBackups。
好了,logrus的实战内容就这些。下一章我们会聊聊更轻量级的zap库,看看它在性能上到底有多猛。到时候咱们可以对比一下,不同场景该选哪个。