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。
小提示: 我个人习惯在开发环境用TextFormatter,方便肉眼查看;生产环境用JSONFormatter,方便日志采集系统(比如ELK)解析。

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("日志轮转已配置")
}
注意: 日志文件路径一定要提前创建好目录,否则logrus不会自动创建,会直接报错。我曾经因为这个踩过坑,排查了半天才发现是目录不存在。

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
}
核心要点: Hook里的Fire方法尽量不要阻塞主流程。如果发邮件、写数据库这些操作耗时较长,一定要用goroutine异步处理,否则会影响业务代码的执行效率。

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里可以放任意键值对,但一定要在调用日志方法之前通过WithFieldWithFields设置好。我见过有人直接在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("查询用户信息")
        // ...
    }
}
我的经验: 建议把traceID、userID这类通用字段封装成一个工具函数,比如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库,看看它在性能上到底有多猛。到时候咱们可以对比一下,不同场景该选哪个。