如何用Golang Logrus实现带时间戳的缓冲日志并隔离全局日志
问题与解决方案:Logrus带时间戳的缓冲日志实现
需求
使用github.com/sirupsen/logrus实现缓冲日志:
- 操作启动时开始计时,所有步骤日志写入缓冲区
- 操作总耗时超过阈值时,输出缓冲区所有日志;否则丢弃缓冲区
- 日志需保留原始时间戳,且缓冲日志为独立实例,不影响全局Logrus配置
现有实现的问题
你当前的代码存在两个核心问题:
- 输出带多余INFO前缀:
PrintBufferedLogs中使用logrus.Print输出,而全局Logrus配置了TextFormatter,会自动添加INFO级别前缀 - 封装性不足:缓冲区直接暴露,依赖全局Logrus输出,且未完全隔离独立日志实例的行为
优化实现方案
下面是更优雅的封装实现,完全隔离独立日志实例,解决前缀问题,同时保证并发安全:
完整代码
package main import ( "io" "os" "sync" "time" "github.com/sirupsen/logrus" ) // BufferedLog 存储缓冲的日志条目 type BufferedLog struct { Message string `json:"message"` Level logrus.Level `json:"level"` Time time.Time `json:"time"` } // BufferedLogger 封装缓冲日志逻辑 type BufferedLogger struct { logger *logrus.Logger buffer []*BufferedLog startTime time.Time threshold time.Duration bufferLock sync.Mutex } // 自定义Formatter,用于捕获日志到缓冲区 type bufferCaptureFormatter struct { buffer *[]*BufferedLog lock *sync.Mutex } func (f *bufferCaptureFormatter) Format(entry *logrus.Entry) ([]byte, error) { f.lock.Lock() defer f.lock.Unlock() *f.buffer = append(*f.buffer, &BufferedLog{ Message: entry.Message, Level: entry.Level, Time: entry.Time, }) // 返回空字节,避免底层Logger输出 return []byte{}, nil } // NewBufferedLogger 创建缓冲日志实例 func NewBufferedLogger(threshold time.Duration) *BufferedLogger { buf := make([]*BufferedLog, 0) lock := &sync.Mutex{} logger := logrus.New() logger.SetFormatter(&bufferCaptureFormatter{buffer: &buf, lock: lock}) logger.SetOutput(io.Discard) // 禁用底层直接输出 logger.SetLevel(logrus.DebugLevel) return &BufferedLogger{ logger: logger, buffer: buf, startTime: time.Now(), threshold: threshold, bufferLock: sync.Mutex{}, } } // 日志方法代理,直接调用底层Logger的对应方法 func (bl *BufferedLogger) Debug(args ...interface{}) { bl.logger.Debug(args...) } func (bl *BufferedLogger) Info(args ...interface{}) { bl.logger.Info(args...) } func (bl *BufferedLogger) Warn(args ...interface{}) { bl.logger.Warn(args...) } func (bl *BufferedLogger) Error(args ...interface{}) { bl.logger.Error(args...) } func (bl *BufferedLogger) Debugf(format string, args ...interface{}) { bl.logger.Debugf(format, args...) } func (bl *BufferedLogger) Infof(format string, args ...interface{}) { bl.logger.Infof(format, args...) } func (bl *BufferedLogger) Warnf(format string, args ...interface{}) { bl.logger.Warnf(format, args...) } func (bl *BufferedLogger) Errorf(format string, args ...interface{}) { bl.logger.Errorf(format, args...) } // Finish 结束操作,判断是否输出缓冲日志 func (bl *BufferedLogger) Finish() { bl.bufferLock.Lock() defer bl.bufferLock.Unlock() duration := time.Since(bl.startTime) if duration < bl.threshold { // 耗时未超阈值,清空缓冲区 bl.buffer = make([]*BufferedLog, 0) return } // 使用独立的JSONFormatter输出,避免依赖全局配置 formatter := &logrus.JSONFormatter{ TimestampFormat: time.RFC3339, } for _, entry := range bl.buffer { // 构造logrus.Entry用于格式化 logEntry := &logrus.Entry{ Message: entry.Message, Level: entry.Level, Time: entry.Time, Logger: logrus.New(), } data, err := formatter.Format(logEntry) if err != nil { // 这里可以用全局日志或者直接输出错误,根据需求调整 os.Stderr.WriteString("failed to format log entry: " + err.Error() + "\n") continue } os.Stdout.Write(data) } // 输出后清空缓冲区 bl.buffer = make([]*BufferedLog, 0) } func main() { // 全局Logrus配置不受影响 logrus.SetLevel(logrus.DebugLevel) logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true}) logrus.Info("global log message") // 创建缓冲日志实例,阈值5秒 bufferedLogger := NewBufferedLogger(5 * time.Second) // 记录操作日志 bufferedLogger.Info("This is an informational message.") bufferedLogger.Warn("This is a warning message.") bufferedLogger.Error("This is an error message.") bufferedLogger.Debug("This is a debug message.") // 模拟耗时超过阈值 time.Sleep(5 * time.Second) bufferedLogger.Finish() // 再测试一次耗时不足的情况 bufferedLogger.Info("Another info message that will be discarded") time.Sleep(2 * time.Second) bufferedLogger.Finish() }
关键优化点
- 完全独立的日志实例:
- 底层使用
logrus.New()创建独立Logger,设置io.Discard作为输出,避免直接打印日志 - 不依赖全局Logrus的配置和输出,彻底隔离影响
- 底层使用
- 解决前缀问题:
- 输出时使用独立的
JSONFormatter直接格式化日志条目并写入os.Stdout,不会添加额外的级别前缀
- 输出时使用独立的
- 更好的封装性:
- 把缓冲区、计时器、阈值全部封装在
BufferedLogger结构体中,对外只暴露日志方法和Finish方法 - 用互斥锁保证并发安全,避免多goroutine下的缓冲区竞争
- 把缓冲区、计时器、阈值全部封装在
- 自动判断逻辑:
Finish方法自动计算操作耗时,超过阈值才输出日志,否则直接清空缓冲区,无需手动判断
输出示例
INFO[2024-05-20T15:30:00+08:00] global log message {"level":"info","message":"This is an informational message.","time":"2024-05-20T15:30:00+08:00"} {"level":"warning","message":"This is a warning message.","time":"2024-05-20T15:30:00+08:00"} {"level":"error","message":"This is an error message.","time":"2024-05-20T15:30:00+08:00"} {"level":"debug","message":"This is a debug message.","time":"2024-05-20T15:30:00+08:00"}
可以看到:
- 全局日志保留原有格式(带INFO前缀)
- 缓冲日志输出为纯净的JSON格式,无多余前缀,且保留了原始时间戳
- 耗时不足的日志被自动丢弃,不会输出
内容的提问来源于stack exchange,提问作者viral_mutant
相关产品推荐
相关产品推荐

