日志库
Gin自带日志系统
gin自带的日志系统使用起来不是特别方便,现在流行使用logrus和zap,这里对gin自带日志系统作简要介绍
写日志文件
func main() {
// 禁用控制台颜色
gin.DisableConsoleColor()
// 创建记录日志的文件
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// 如果需要将日志同时写入文件和控制台,请使用以下代码
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
自定义日志格式
func main() {
router := gin.New()
// LoggerWithFormatter 中间件会将日志写入 gin.DefaultWriter
// By default gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// 你的自定义格式
return fmt.Sprintf("[wan] %s - [%s] " %s %s %s | %s | %s | %s %d %s | %s " || %s || " %s"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.MethodColor(), param.Method, param.ResetColor(),
param.Path,
param.Request.Proto,
param.StatusCodeColor(), param.StatusCode, param.ResetColor(), // 设置颜色
param.Latency, // 响应时间
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {c.String(200, "pong")})
router.Run(":8080")
}
输出示例:
[wan] ::1 - [Fri, 07 Apr 2023 19:37:01 CST] " GET | /ping | HTTP/1.1 | 200 | 0s " || PostmanRuntime/7.31.3 || " "
logrus
下载
go get github.com/sirupsen/logrus
logrus常用方法
logrus.Debug("Debugln") logrus.Debugf("Debugln:%s", "any..") logrus.Debugln("Debugln")
logrus.Info("Infoln") logrus.Infof("Infoln:%s", "any..") logrus.Infoln("Infoln")
logrus.Warn("Warnln") logrus.Warnf("Warnln:%s", "any..") logrus.Warnln("Warnln")
logrus.Error("Errorln") logrus.Errorf("Errorln:%s", "any..") logrus.Errorln("Errorln")
logrus.Print("Println") logrus.Printf("Println:%s", "any..") logrus.Println("Println")
// 输出如下
time="2022-12-17T14:02:01+08:00" level=info msg=Infoln
time="2022-12-17T14:02:01+08:00" level=warning msg=Warnln
time="2022-12-17T14:02:01+08:00" level=error msg=Errorln
time="2022-12-17T14:02:01+08:00" level=info msg=Println
debug的没有输出,是因为logrus默认的日志输出等级是 info
fmt.Println(logrus.GetLevel()) // info
日志等级
PanicLevel // 会抛一个异常
FatalLevel // 打印日志之后就会退出
ErrorLevel
WarnLevel
InfoLevel
DebugLevel
TraceLevel // 低级别
如果你想显示Debug的日志,那么你可以更改日志显示等级
logrus.SetLevel(logrus.DebugLevel)
日志级别一般是和系统环境挂钩,例如开发环境,肯定就要显示debug信息,测试环境也是需要的
线上环境就不需要这些日志,可能只显示warnning的日志
设置特定字段
log1 := logrus.WithField("project", "study")
log1.Errorln("hello")
// 输出:time="2022-12-17T15:02:28+08:00" level=error msg=hello project=study
log1 := logrus.WithField("project", "study").WithField("project2", "study2") // 链式引用
log1.Errorln("hello")
// 输出:time="2022-12-17T15:02:28+08:00" level=error msg=hello project=study project2=study2
log2 := logrus.WithFields(logrus.Fields{
"func": "main",
"auth": "枫枫",
})
log2.Warningf("你好")
// 输出:time="2022-12-17T15:02:28+08:00" level=warning msg="你好" func=main auth="枫枫"
log3 := log2.WithFields(logrus.Fields{ // 链式引用
"auth2": "枫枫2",
})
log3.Warnln("你好")
// 输出:time="2022-12-17T15:02:28+08:00" level=warning msg="你好" func=main auth="枫枫" auth2="枫枫2"
通常,在一个应用中、或者应用的一部分中,都有一些固定的Field。比如在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip。
为了避免每次记录日志都要使用log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip}),我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可。
requestLogger := logrus.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
requestLogger.Info("something happened on that request") // will log request_id and user_ip
requestLogger.Warn("something not great happened")
日志条目
除了使用WithField或WithFields添加的字段外,一些字段会自动添加到所有日志记录事中:
- time:记录日志时的时间戳
- msg:记录的日志信息
- level:记录的日志级别
显示样式 Text和Json
默认的是以text的形式展示,也可以设置为json形式展示,如下:
logrus.SetFormatter(&logrus.JSONFormatter{})
log1 := logrus.WithField("project", "study")
log1.Errorln("hello")
// {"level":"error","msg":"hello","project":"study","time":"2022-12-17T15:08:24+08:00"}
在设置显示样式的时候还可以进行其他的一些配置:
ForceColors:是否强制使用颜色输出。
DisableColors:是否禁用颜色输出。
ForceQuote:是否强制引用所有值。
DisableQuote:是否禁用引用所有值。
DisableTimestamp:是否禁用时间戳记录。
FullTimestamp:是否在连接到 TTY 时输出完整的时间戳。
TimestampFormat:用于输出完整时间戳的时间戳格式。
func main() {
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
TimestampFormat: "2006-01-02 15:04:05",
FullTimestamp: true,
})
logrus.Debugln("Debugln")
logrus.Infoln("Infoln")
logrus.Warnln("Warnln")
logrus.Errorln("Errorln")
logrus.Println("Println")
}
输出结果:
如果以上这两个格式不满足需求,可以自己动手实现接口 Formatter 接口来定义自己的日志格式。
Tip:关于颜色设置的一些知识
// 前景色
fmt.Println("\033[30m 前景颜色展示:黑色 \033[0m")
fmt.Println("\033[31m 前景颜色展示:红色 \033[0m")
fmt.Println("\033[32m 前景颜色展示:绿色 \033[0m")
fmt.Println("\033[33m 前景颜色展示:黄色 \033[0m")
fmt.Println("\033[34m 前景颜色展示:蓝色 \033[0m")
fmt.Println("\033[35m 前景颜色展示:紫色 \033[0m")
fmt.Println("\033[36m 前景颜色展示:青色 \033[0m")
fmt.Println("\033[37m 前景颜色展示:灰色 \033[0m")
// 背景色
fmt.Println("\033[40m 背景颜色展示:黑色 \033[0m")
fmt.Println("\033[41m 背景颜色展示:红色 \033[0m")
fmt.Println("\033[42m 背景颜色展示:绿色 \033[0m")
fmt.Println("\033[43m 背景颜色展示:黄色 \033[0m")
fmt.Println("\033[44m 背景颜色展示:蓝色 \033[0m")
fmt.Println("\033[45m 背景颜色展示:紫色 \033[0m")
fmt.Println("\033[46m 背景颜色展示:青色 \033[0m")
fmt.Println("\033[47m 背景颜色展示:灰色 \033[0m")
输出样式:
自定义颜色输出函数:
const (
CBlack = iota
CRed
CGreen
CYellow
CBlue
CPurple
CCyan
CGray
)
func PrintColor(colorCode int, text string, isBackGround bool) {
if isBackGround {
fmt.Printf("\033[4%dm %s \033[0m", colorCode, text)
return
}
fmt.Printf("\033[3%dm %s \033[0m", colorCode, text)
}
func main() {
PrintColor(CCyan, "随便写的", true)
PrintColor(CRed, "随便写的", false)
}
输出到日志文件
默认的输出是在控制台上
使用SetOutput(out io.Writer)函数即可将日志输出到文件上:
file, _ := os.OpenFile("info.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
logrus.SetOutput(file)
同时输出屏幕和文件
利用io.MultiWriter()实现写入复制
func MultiWriter(writers ...Writer) Writer
// 将多个writers对象合成一个Writer输出,其可以实现将写入复制到所有提供的writers中
file, err := os.OpenFile("checkemstools.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
// 同时写文件和屏幕
fileAndStdoutWriter := io.MultiWriter(file, os.Stdout)
logrus.SetOutput(fileAndStdoutWriter)
显示行号
logrus.SetReportCaller(true)
// 设置为true后可以显示行号和函数名,但开启这个模式会增加性能开销。
Hook接口用法
logrus最令人心动的功能就是其可扩展的HOOK机制了,通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能。比如可以通过 Hooks 实现:Error 以上级别日志发送邮件通知、重要日志告警、日志切割、程序优雅退出等,非常实用。
hook的使用也很简单,在初始化前调用log.AddHook(hook)添加相应的hook即可。logrus官方仅仅内置了syslog的hook。此外,但Github也有很多第三方的hook可供使用,文末将提供一些第三方HOOK的链接.
logrus的hook接口定义如下,其原理是每此写入日志时拦截,修改logrus.Entry.
// logrus在记录Levels()返回的日志级别的消息时会触发HOOK
// 按照Fire方法定义的内容修改logrus.Entry.
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
示例1:一个简单自定义hook如下,DefaultFieldHook定义会在所有级别的日志消息中加入默认字段appName=”myAppName”.
package main
import "github.com/sirupsen/logrus"
type DefaultFieldHook struct {
}
func (hook *DefaultFieldHook) Fire(entry *logrus.Entry) error {
entry.Data["appName"] = "MyAppName"
return nil
}
func (hook *DefaultFieldHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func main() {
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
TimestampFormat: "2006-01-02 15:04:05",
FullTimestamp: true,
})
logrus.AddHook(&DefaultFieldHook{})
logrus.Errorf("hello world")
logrus.Infoln("hello world")
logrus.Warnln("hello world")
}
输出结果:
示例2:将error级别的日志独立输出到error.log文件里,其他都放在一起。
package main
import (
"fmt"
"github.com/sirupsen/logrus"
"os"
)
type MyHook struct {
Writer *os.File
}
func (hook *MyHook) Fire(entry *logrus.Entry) error {
line, err := entry.String()
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
return err
}
hook.Writer.Write([]byte(line))
return nil
}
func (hook *MyHook) Levels() []logrus.Level {
return []logrus.Level{logrus.ErrorLevel}
}
func main() {
logrus.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
TimestampFormat: "2006-01-02 15:04:05",
FullTimestamp: true,
})
logrus.SetReportCaller(true) // 显示行号
// 日志的打开格式是追加,所以不能用os.Create,需要自定义文件打开模式
file, _ := os.OpenFile("err.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
hook := &MyHook{Writer: file}
logrus.AddHook(hook)
logrus.Errorf("hello world")
logrus.Infoln("hello world")
logrus.Warnln("hello world")
}
控制台输出:
文件输出:
日志分割
按照时间分割
利用file-rotatelogs 包实现日志切割
logrus 本身不支持日志轮转切割功能,需要配合 file-rotatelogs 包来实现,防止日志打满磁盘。file-rotatelogs 实现了 io.Writer 接口,并且提供了文件的切割功能,其实例可以作为 logrus 的目标输出,两者能无缝集成。
日志轮转相关函数:
WithLinkName为最新的日志建立软连接WithRotationTime设置日志分割的时间,隔多久分割一次 WithMaxAge 和 WithRotationCount二者只能设置一个
WithMaxAge设置文件清理前的最长保存时间WithRotationCount设置文件清理前最多保存的个数
示例代码:
package main
import (
"github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
log "github.com/sirupsen/logrus"
"time"
)
var logLevels = map[string]log.Level{
"panic": log.PanicLevel,
"fatal": log.FatalLevel,
"error": log.ErrorLevel,
"warn": log.WarnLevel,
"info": log.InfoLevel,
"debug": log.DebugLevel,
"trace": log.TraceLevel,
}
func newLfsHook(logName string, logLevel string, maxRemainCnt uint) log.Hook {
writer, err := rotatelogs.New(
logName+"-%Y%m%d%H.log",
// WithLinkName为最新的日志建立软连接,以方便随着找到当前日志文件
rotatelogs.WithLinkName(logName),
// WithRotationTime设置日志分割的时间,这里设置为一小时分割一次
rotatelogs.WithRotationTime(time.Hour),
// WithMaxAge和WithRotationCount二者只能设置一个,
// WithMaxAge设置文件清理前的最长保存时间,
// WithRotationCount设置文件清理前最多保存的个数.
// rotatelogs.WithMaxAge(time.Hour*24),
rotatelogs.WithRotationCount(maxRemainCnt),
)
if err != nil {
log.Errorf("config local file system for logger error: %v", err)
}
// 判断输入的日志等级是否存在,不存在则给一个默认值
if level, ok := logLevels[logLevel]; ok {
log.SetLevel(level)
} else {
log.SetLevel(log.WarnLevel)
}
// 使用了lfshook软件包创建了一个新的日志钩子,该钩子将日志记录到指定的日志文件中。
// lfshook.WriterMap指定了每个日志级别所使用的写入器(writer)。
// 在这个函数中,所有的日志级别都使用同一个写入器writer。
lfsHook := lfshook.NewHook(lfshook.WriterMap{
log.DebugLevel: writer,
log.InfoLevel: writer,
log.WarnLevel: writer,
log.ErrorLevel: writer,
log.FatalLevel: writer,
log.PanicLevel: writer,
}, &log.TextFormatter{
ForceColors: true,
TimestampFormat: "2006-01-02 15:04:05",
FullTimestamp: true,
})
return lfsHook
}
func main() {
log.SetReportCaller(true) // 显示行号
rotateHook := newLfsHook("./rotate", "debug", 3)
log.AddHook(rotateHook)
log.Errorf("hello world")
log.Debugln("hello world")
log.Infoln("hello world")
log.Warnln("hello world")
}
按日志等级分割
与前面hook接口用法的示例2类似。
package main
import (
"fmt"
"github.com/sirupsen/logrus"
"os"
)
const (
allLog = "all"
errLog = "err"
warnLog = "warn"
infoLog = "info"
)
type FileLevelHook struct {
file *os.File
errFile *os.File
warnFile *os.File
infoFile *os.File
logPath string
}
func (hook FileLevelHook) Levels() []logrus.Level {
return logrus.AllLevels
}
func (hook FileLevelHook) Fire(entry *logrus.Entry) error {
line, _ := entry.String()
switch entry.Level {
case logrus.ErrorLevel:
hook.errFile.Write([]byte(line))
case logrus.WarnLevel:
hook.warnFile.Write([]byte(line))
case logrus.InfoLevel:
hook.infoFile.Write([]byte(line))
}
hook.file.Write([]byte(line))
return nil
}
func InitLevel(logPath string) {
err := os.MkdirAll(fmt.Sprintf("%s", logPath), os.ModePerm)
if err != nil {
logrus.Error(err)
return
}
allFile, err := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, allLog), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
errFile, err := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, errLog), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
warnFile, err := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, warnLog), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
infoFile, err := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, infoLog), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
fileHook := FileLevelHook{allFile, errFile, warnFile, infoFile, logPath}
logrus.AddHook(&fileHook)
}
func main() {
InitLevel("logrus_study/log_level")
logrus.Errorln("你好")
logrus.Errorln("err")
logrus.Warnln("warn")
logrus.Infof("info")
logrus.Println("print")
}
线程安全
默认的logger在并发写的时候是被mutex保护的,比如当同时调用hook和写log时mutex就会被请求,有另外一种情况,文件是以appending mode打开的, 此时的并发操作就是安全的,可以用logger.SetNoLock()来关闭它。
gin框架使用logrus
// a gin with logrus demo
var log = logrus.New()
func init() {
// Log as JSON instead of the default ASCII formatter.
log.Formatter = &logrus.JSONFormatter{}
// Output to stdout instead of the default stderr
// Can be any io.Writer, see below for File example
f, _ := os.Create("./gin.log")
log.Out = f
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = log.Out
// Only log the warning severity or above.
log.Level = logrus.InfoLevel
}
func main() {
// 创建一个默认的路由引擎
r := gin.Default()
// GET:请求方式;/hello:请求的路径
// 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数
r.GET("/hello", func(c *gin.Context) {
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Warn("A group of walrus emerges from the ocean")
// c.JSON:返回JSON格式的数据
c.JSON(200, gin.H{
"message": "Hello world!",
})
})
// 启动HTTP服务,默认在0.0.0.0:8080启动服务
r.Run()
}