golang logrus库

golang 的笔记,内容多来源于互联网,放这里方便查找。

基本用法

package main

import (
	"os"

	log "github.com/sirupsen/logrus"
)

func init() {
	// 设置日志格式为json格式
	log.SetFormatter(&log.JSONFormatter{})

	// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
	// 日志消息输出可以是任意的io.writer类型
	log.SetOutput(os.Stdout)

	// 设置日志级别为warn以上
	log.SetLevel(log.InfoLevel)
}

func main() {
	log.Info("A group of walrus emerges from the ocean")

	log.Warn("The group's number increased tremendously!")
}

日志分割切片

package main

import (
	"time"

	rotatelogs "github.com/lestrrat-go/file-rotatelogs"
	log "github.com/sirupsen/logrus"
)

func init() {
	path := "./go.log"
	/* 日志轮转相关函数
	`WithLinkName` 为最新的日志建立软连接
	`WithRotationTime` 设置日志分割的时间,隔多久分割一次
	WithMaxAge 和 WithRotationCount二者只能设置一个
	 `WithMaxAge` 设置文件清理前的最长保存时间
	 `WithRotationCount` 设置文件清理前最多保存的个数
	*/
	// 下面配置日志每隔 1 分钟轮转一个新文件,保留最近 3 分钟的日志文件,多余的自动清理掉。
	writer, _ := rotatelogs.New(
		path+".%Y%m%d%H%M",
		rotatelogs.WithLinkName(path),
		rotatelogs.WithMaxAge(time.Duration(180)*time.Second),
		rotatelogs.WithRotationTime(time.Duration(60)*time.Second),
	)
	log.SetOutput(writer)
	//log.SetFormatter(&log.JSONFormatter{})
}

func main() {
	for {
		log.Info("hello, world!")
		time.Sleep(time.Duration(2) * time.Second)
	}
}

Hook

hook内部实现

Hook 接口定义如下:

type Hook interface {
  // 定义哪些等级的日志触发 hook 机制
	Levels() []Level
  // hook 触发器的具体执行操作
  // 如果 Fire 执行失败,错误日志会重定向到标准错误流
	Fire(*Entry) error
}

那logrus的内部是怎么实现触发的呢, logrus中有个内部结构LevelHooks用来存储所有定义的 hook 函数。

// 存储全局 hooks, 以日志等级为键聚合存储
type LevelHooks map[Level][]Hook
// 添加 hooks
func (hooks LevelHooks) Add(hook Hook) {
	for _, level := range hook.Levels() {
		hooks[level] = append(hooks[level], hook)
	}
}

// 根据日志等级触发 hooks
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
	for _, hook := range hooks[level] {
		if err := hook.Fire(entry); err != nil {
			return err
		}
	}

	return nil
}

在打印日志时, entry会调用 fireHooks()函数,该函数会触发所有对应的日志等级 的 hook 逻辑。

// 触发 hooks
func (entry *Entry) fireHooks() {
	entry.Logger.mu.Lock()
	defer entry.Logger.mu.Unlock()
	err := entry.Logger.Hooks.Fire(entry.Level, entry)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
	}
}

自带的syslog

package main

import (
	"log/syslog"

	log "github.com/sirupsen/logrus"
	logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
	airbrake "gopkg.in/gemnasium/logrus-airbrake-hook.v2"
)

func init() {

	// Use the Airbrake hook to report errors that have Error severity or above to
	// an exception tracker. You can create custom hooks, see the Hooks section.
	log.AddHook(airbrake.NewHook(123, "xyz", "production"))

	hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
	if err != nil {
		log.Error("Unable to connect to local syslog daemon")
	} else {
		log.AddHook(hook)
	}
}
func main() {
	log.Info("A group of walrus emerges from the ocean")

	log.Warn("The group's number increased tremendously!")
}

自定义hook,输出error/panic到文件中

在这个例子中我们希望当系统发生error或者panic的时候,将错误日志打印到单独的 err.log 文件中便于我们排查错误。

package main

import (
	"os"

	log "github.com/sirupsen/logrus"
)

// MyHook ...
type MyHook struct {
}

// Levels 只定义 error 和 panic 等级的日志,其他日志等级不会触发 hook
func (h *MyHook) Levels() []log.Level {
	return []log.Level{
		log.ErrorLevel,
		log.PanicLevel,
	}
}

// Fire 将异常日志写入到指定日志文件中
func (h *MyHook) Fire(entry *log.Entry) error {
	f, err := os.OpenFile("err.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
	if err != nil {
		return err
	}
	if _, err := f.Write([]byte(entry.Message)); err != nil {
		return err
	}
	return nil
}
func main() {
	log.AddHook(&MyHook{})
	log.Error("some errors\n")
	log.Panic("some panic\n")
	log.Print("hello world\n")
}

Hook日志分割

// log_hook.go
package main

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/pkg/errors"
	"github.com/rifflock/lfshook"
	"github.com/sirupsen/logrus"
)

//自实现 logrus hook
func getLogger(module string) *logrus.Logger {
	//实例化
	logger := logrus.New()
	//设置输出
	logger.Out = os.Stdout
	//设置日志级别
	logger.SetLevel(logrus.DebugLevel)
	//设置日志格式
	//自定writer就行, hook 交给 lfshook
	logger.AddHook(newLogrusHook("./log", module))

	logger.SetFormatter(&logrus.JSONFormatter{
		TimestampFormat: "2006-01-02 15:04:05",
	})
	return logger
}

//确保每次调用使用的文件都是唯一的。
func GetNewFieldLoggerContext(module, appField string) *logrus.Entry {
	logger := getLogger(module)
	return logger.WithFields(logrus.Fields{
		"app": appField,
	})
}

type LogWriter struct {
	logDir           string //日志根目录地址。
	module           string //模块 名
	curFileName      string //当前被指定的filename
	curBaseFileName  string //在使用中的file
	turnCateDuration time.Duration
	mutex            sync.RWMutex
	outFh            *os.File
}

func (w *LogWriter) Write(p []byte) (n int, err error) {
	w.mutex.Lock()
	defer w.mutex.Unlock()
	if out, err := w.getWriter(); err != nil {
		return 0, errors.New("failed to fetch target io.Writer")
	} else {
		return out.Write(p)
	}
}

func (w *LogWriter) getFileName() string {
	base := time.Now().Truncate(w.turnCateDuration)
	return fmt.Sprintf("%s/%s/%s_%s", w.logDir, base.Format("2006-01-02"), w.module, base.Format("15"))
}

func (w *LogWriter) getWriter() (io.Writer, error) {
	fileName := w.curBaseFileName
	//判断是否有新的文件名
	//会出现新的文件名
	baseFileName := w.getFileName()
	if baseFileName != fileName {
		fileName = baseFileName
	}

	dirname := filepath.Dir(fileName)
	if err := os.MkdirAll(dirname, 0755); err != nil {
		return nil, errors.Wrapf(err, "failed to create directory %s", dirname)
	}

	fileHandler, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		return nil, errors.Errorf("failed to open file %s", err)
	}
	w.outFh.Close()
	w.outFh = fileHandler
	w.curBaseFileName = fileName
	w.curFileName = fileName

	return fileHandler, nil
}

func New(logPath, module string, duration time.Duration) *LogWriter {
	return &LogWriter{
		logDir:           logPath,
		module:           module,
		turnCateDuration: duration,
		curFileName:      "",
		curBaseFileName:  "",
	}
}

func newLogrusHook(logPath, moduel string) logrus.Hook {
	logrus.SetLevel(logrus.WarnLevel)

	writer := New(logPath, moduel, time.Hour*2)

	lfsHook := lfshook.NewHook(lfshook.WriterMap{
		logrus.DebugLevel: writer,
		logrus.InfoLevel:  writer,
		logrus.WarnLevel:  writer,
		logrus.ErrorLevel: writer,
		logrus.FatalLevel: writer,
		logrus.PanicLevel: writer,
	}, &logrus.TextFormatter{DisableColors: true})

	// writer 生成新的log文件类型 writer  在通过new hook函数 消费 fire 函数
	// writer 是实现了writer 接口的库,在日志调用write是做预处理
	return lfsHook
}

func main() {
	lg := GetNewFieldLoggerContext("test", "d")
	lg.Logger.Info("????")
}

logger实例持有了 自定义的 io.writer 结构体,在消费Fire函数时,会调用Write方法,此时通过Truncate时间切片函数逻辑判断需要写入的文件。或创建新的文件。 注: 文章提供的代码是按天切分文件夹的,文件夹内模块日志再按2小时切分。可自行替换成按模块切分。

dingding hook

package main

import (
	"github.com/leafney/dingtalkrus"
	"github.com/sirupsen/logrus"
	"os"
)

func main() {
	logrus.SetFormatter(&logrus.JSONFormatter{})

	logrus.SetOutput(os.Stderr)

	logrus.SetLevel(logrus.DebugLevel)

	logrus.AddHook(dingtalkrus.NewHook(
		"", // dingtalk token
		"", // dingtalk secret
		dingtalkrus.LevelThreshold(logrus.ErrorLevel)),
	)

	logrus.Info("This is the info test message.")
	logrus.WithFields(dingtalkrus.SendTextMsg("This is the warn test message.",[]string{},false)).Warn()
	logrus.WithFields(dingtalkrus.SendMarkdownMsg("杭州天气","#### 杭州天气 \n 9度,西北风1级,空气良89,相对温度73%\n",[]string{},false)).Error()
}