You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Golang Logrus实现带时间戳的缓冲日志并隔离全局日志

问题与解决方案:Logrus带时间戳的缓冲日志实现

需求

使用github.com/sirupsen/logrus实现缓冲日志:

  • 操作启动时开始计时,所有步骤日志写入缓冲区
  • 操作总耗时超过阈值时,输出缓冲区所有日志;否则丢弃缓冲区
  • 日志需保留原始时间戳,且缓冲日志为独立实例,不影响全局Logrus配置

现有实现的问题

你当前的代码存在两个核心问题:

  1. 输出带多余INFO前缀:PrintBufferedLogs中使用logrus.Print输出,而全局Logrus配置了TextFormatter,会自动添加INFO级别前缀
  2. 封装性不足:缓冲区直接暴露,依赖全局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()
}

关键优化点

  1. 完全独立的日志实例:
    • 底层使用logrus.New()创建独立Logger,设置io.Discard作为输出,避免直接打印日志
    • 不依赖全局Logrus的配置和输出,彻底隔离影响
  2. 解决前缀问题:
    • 输出时使用独立的JSONFormatter直接格式化日志条目并写入os.Stdout,不会添加额外的级别前缀
  3. 更好的封装性:
    • 把缓冲区、计时器、阈值全部封装在BufferedLogger结构体中,对外只暴露日志方法和Finish方法
    • 用互斥锁保证并发安全,避免多goroutine下的缓冲区竞争
  4. 自动判断逻辑:
    • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 07:27:03