
本文详解如何使用logrus搭配自定义hook,实现在控制台输出彩色结构化日志、同时将原始json格式日志写入文件,并支持级别过滤与时间格式统一。
本文详解如何使用logrus搭配自定义hook,实现在控制台输出彩色结构化日志、同时将原始json格式日志写入文件,并支持级别过滤与时间格式统一。
Go标准库的log包轻量易用,但缺乏多输出目标、多格式、日志级别控制等现代应用所需的核心能力。当项目需要同时向终端(console)和文件(file)输出日志,且两者格式不同(例如:终端显示带颜色、含函数名的简洁文本;文件保存完整、可解析的JSON),推荐采用社区广泛验证的成熟方案——Logrus(注意:其作者已将导入路径更新为 github.com/sirupsen/logrus,非旧版 Sirupsen)。
Logrus 的核心优势在于其 Hook 机制:允许开发者在日志被写入前,将其分发至任意数量的后端(如 stdout、文件、网络服务),并为每个后端独立定制序列化逻辑与格式。
以下是一个生产就绪的示例,实现:
- 控制台输出:带 ANSI 颜色、含时间、级别、调用位置(文件:行号)的可读文本;
- 文件输出:标准 JSON 格式,含时间戳(RFC3339)、级别、消息、字段(fields),便于 ELK 或 Loki 摄取。
package main
import (
"io"
"os"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/term"
)
// ConsoleFormatter 实现彩色、可读的终端格式
type ConsoleFormatter struct{}
func (f *ConsoleFormatter) Format(entry *logrus.Entry) ([]byte, error) {
timestamp := entry.Time.Format("15:04:05")
levelText := strings.ToUpper(entry.Level.String()[0:1]) + entry.Level.String()[1:]
msg := entry.Message
if entry.HasCaller() {
msg = "[" + entry.Caller.ShortFile + ":" + strconv.Itoa(entry.Caller.Line) + "] " + msg
}
color := map[logrus.Level]string{
logrus.DebugLevel: "\033[36m", // cyan
logrus.InfoLevel: "\033[32m", // green
logrus.WarnLevel: "\033[33m", // yellow
logrus.ErrorLevel: "\033[31m", // red
logrus.FatalLevel: "\033[1;31m",
logrus.PanicLevel: "\033[1;41m",
}[entry.Level]
reset := "\033[0m"
return []byte(
color + timestamp + " " + levelText + " " + reset + msg + "\n",
), nil
}
func main() {
logger := logrus.New()
logger.SetLevel(logrus.DebugLevel)
logger.SetReportCaller(true) // 启用调用栈信息(用于文件:行号)
// 控制台输出(stdout)
consoleWriter := io.Writer(os.Stdout)
if term.IsTerminal(int(os.Stdout.Fd())) {
logger.SetFormatter(&ConsoleFormatter{})
} else {
logger.SetFormatter(&logrus.TextFormatter{
DisableColors: true,
FullTimestamp: true,
TimestampFormat: time.RFC3339,
})
}
logger.SetOutput(consoleWriter)
// 文件输出(log.json)
file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
logrus.WithError(err).Fatal("failed to open log file")
}
defer file.Close()
// 使用 JSON 格式写入文件,不干扰 console 输出
jsonWriter := io.MultiWriter(file)
logger.AddHook(&FileHook{writer: jsonWriter})
// 示例日志
logger.Debug("debug message with fields", logrus.Fields{"user_id": 1001, "action": "login"})
logger.Info("service started", logrus.Fields{"port": 8080})
logger.Warn("deprecated API used", logrus.Fields{"endpoint": "/v1/users"})
logger.Error("database timeout", logrus.Fields{"timeout_ms": 5000})
}
// FileHook:将日志以 JSON 格式写入指定 writer
type FileHook struct {
writer io.Writer
}
func (h *FileHook) Fire(entry *logrus.Entry) error {
data, err := entry.JSON()
if err != nil {
return err
}
_, err = h.writer.Write(append(data, '\n'))
return err
}
func (h *FileHook) Levels() []logrus.Level {
return logrus.AllLevels // 可按需限制,如 []logrus.Level{logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel}
}
✅ 关键要点说明:
- Logrus 默认仅支持单个 Output,因此需通过 AddHook 注册额外输出通道(如文件),避免覆盖控制台;
- SetFormatter() 仅影响主 Output(此处为 console),而 Hook 中的 Fire() 方法可自由选择序列化方式(如 entry.JSON());
- SetReportCaller(true) 启用调用位置追踪,配合 entry.Caller 可精准定位日志源头;
- Levels() 方法决定该 Hook 响应哪些日志级别,便于分离 debug 日志(仅存文件)与 error 日志(推送到告警系统);
- 生产环境建议搭配 rotatelogs(如 github.com/lestrrat-go/file-rotatelogs)实现自动日志轮转,防止单文件过大。
⚠️ 注意事项:
- 避免在 Hook 的 Fire() 中执行阻塞或耗时操作(如 HTTP 请求),否则会拖慢主业务流程;高并发场景建议使用异步 Hook(如 logrus/hooks/airbrake 的缓冲队列);
- 若需更高性能与零分配设计,可评估 zap(Uber)或 zerolog(无反射、极致快),二者均原生支持多 Writer 与结构化输出,但 API 风格与 Logrus 差异较大;
- 所有日志字段应为可序列化类型(string/int/bool/map/slice),避免传入 func、chan 等无法 JSON 编码的值。
Logrus + Hook 组合提供了极强的灵活性与可维护性,是 Go 生态中平衡易用性与扩展性的主流选择。掌握其 Hook 机制,即可轻松构建符合团队规范、适配监控体系的统一日志基础设施。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











