一、综述
Gin自带的日志功能较弱,
- 使用
logrus(sirupsen/logrus: Structured, pluggable logging for Go. )定制日志内容,核心函数:
func (logger *Logger) WithFields(fields Fields) *Entry
//Fields 的实际类型是 map[string]interface{} ,包含了自定义日志的各个字段名和字段值
- 使用
rotatelogs完成日志分割、日志定期清理、生成软链文件指向最新日志。核心函数:
func New(p string, options ...Option) (*RotateLogs, error)
- 使用
lfshook决定哪些级别的日志可以使用rotatelogs的切割设置,并决定输出格式(TEXT / JSON)。
核心数据结构是一个WriteMap,记录可用于输出的日志级别;核心函数是NewHook(),加载WriteMap和输出格式(TEXT /JSON),并返回一个logrus可挂载的hook。
type WriterMap map[logrus.Level]io.Writer
func NewHook(output interface{}, formatter logrus.Formatter) *LfsHook
二、rotatelogs和lfshook的配合
log 是logrus的一个实例
// 设置日志切割 rotatelogs
writer, _ := rotatelogs.New(
filePath+"%Y%m%d.log",
//在项目根目录下生成软链文件 latest_log.log 指向最新的日志文件。注意!!!必须在管理员权限下开终端启动。
rotatelogs.WithLinkName(linkName),
//日志最大保存时间
rotatelogs.WithMaxAge(7*24*time.Hour),
////设置日志切割时间间隔(1天)(隔多久分割一次)
rotatelogs.WithRotationTime(24*time.Hour),
)
// lfshook 决定哪些日志级别可用日志分割
writeMap := lfshook.WriterMap{
logrus.PanicLevel: writer,
logrus.FatalLevel: writer,
logrus.ErrorLevel: writer,
logrus.WarnLevel: writer,
logrus.InfoLevel: writer,
logrus.DebugLevel: writer,
}
// 配置 lfshook
hook := lfshook.NewHook(writeMap, &logrus.TextFormatter{
// 设置日期格式
TimestampFormat: "2006.01.02 - 15:04:05",
})
//为 logrus 实例添加自定义 hook
log.AddHook(hook)
三、logrus的使用
1.基本用法
logrus 可以取代自带的log,局部地使用
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
}).Info("A walrus appears")
}
上面代码执行后,标准输出上输出如下:
time="2022-08-22T15:42:22+08:00" level=info msg="A walrus appears" animal=walrus
logrus可以通过简单的配置,来定义输出格式或者日志级别。
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.WarnLevel)
}
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(log.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(log.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
}
2.自定义Logger
如果想在一个应用里面向多个地方log,可以创建Logger实例。 logger是一种相对高级的用法,,对于一个大型项目, 往往需要一个全局的logrus实例,即logger对象来记录项目所有的日志。如:
package main
import (
"github.com/sirupsen/logrus"
"os"
)
// logrus提供了New()函数来创建一个logrus的实例.
// 项目中,可以创建任意数量的logrus实例.
var log = logrus.New()
func main() {
// 为当前logrus实例设置消息的输出,同样地,
// 可以设置logrus实例的输出到任意io.writer
log.Out = os.Stdout
// 为当前logrus实例设置消息输出格式为json格式.
// 同样地,也可以单独为某个logrus实例设置日志级别和hook,这里不详细叙述.
log.Formatter = &logrus.JSONFormatter{}
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
}
3.Fields用法
logrus推荐使用Fields来进行精细化的,结构化的信息记录,如下:
log.WithFields(log.Fields{
"event": event,
"topic": topic,
"key": key,
}).Fatal("Failed to send event")
WithFields()可以规范使用者按照其提倡的方式记录日志。但是WithFields()依然是可选的,因为某些场景下只需记录一条简单的消息。
通常在一个应用中,都有一些固定的Field。比如在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip。为了避免每次记录日志都要使用
log.WithFields(log.Fields{“request_id”: request_id, “user_ip”: user_ip})
我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.Entry实例记录日志即可。
entry := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
//↓ will log request_id and user_ip
entryr.Info("something happened on that request")
entry.Warn("something not great happened")
4.Hook接口用法
logrus最令人心动的功能就是其可扩展的HOOK机制了,通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能.
logrus的hook接口定义如下,其原理是每此写入日志时拦截,修改logrus.Entry.
// logrus在记录Levels()返回的日志级别的消息时会触发HOOK,
// 按照Fire方法定义的内容修改logrus.Entry.
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
一个简单自定义hook如下,DefaultFieldHook定义会在所有级别的日志消息中加入默认字段appName=”myAppName”.
type DefaultFieldHook struct {
}
func (hook *DefaultFieldHook) Fire(entry *log.Entry) error {
entry.Data["appName"] = "MyAppName"
return nil
}
func (hook *DefaultFieldHook) Levels() []log.Level {
return log.AllLevels
}
hook的使用也很简单,在初始化前调用log.AddHook(hook)添加相应的hook即可.
logrus官方仅仅内置了syslog的hook. 此外,但Github也有很多第三方的hook可供使用,文末将提供一些第三方HOOK的连接.
4.1 Logrus-Hook-Email
email这里只需用NewMailAuthHook方法得到hook,再添加即可
func Email(){
logger:= logrus.New()
//parameter"APPLICATION_NAME", "HOST", PORT, "FROM", "TO"
//首先开启smtp服务,最后两个参数是smtp的用户名和密码
hook, err := logrus_mail.NewMailAuthHook("testapp", "smtp.163.com",25,"username@163.com","username@163.com","smtp_name","smtp_password")
if err == nil {
logger.Hooks.Add(hook)
}
//生成*Entry
var filename="123.txt"
contextLogger :=logger.WithFields(logrus.Fields{
"file":filename,
"content": "GG",
})
//设置时间戳和message
contextLogger.Time=time.Now()
contextLogger.Message="这是一个hook发来的邮件"
//只能发送Error,Fatal,Panic级别的log
contextLogger.Level=logrus.FatalLevel
//使用Fire发送,包含时间戳,message
hook.Fire(contextLogger)
}
4.2 Logrus-Hook-Slack
安装slackrus github.com/johntdyer/slackrus
package main
import (
logrus "github.com/sirupsen/logrus"
"github.com/johntdyer/slackrus"
"os"
)
func main() {
logrus.SetFormatter(&logrus.JSONFormatter{})
logrus.SetOutput(os.Stderr)
logrus.SetLevel(logrus.DebugLevel)
logrus.AddHook(&slackrus.SlackrusHook{
HookURL: "https://hooks.slack.com/services/abc123/defghijklmnopqrstuvwxyz",
AcceptedLevels: slackrus.LevelThreshold(logrus.DebugLevel),
Channel: "#slack-testing",
IconEmoji: ":ghost:",
Username: "foobot",
})
logrus.Warn("warn")
logrus.Info("info")
logrus.Debug("debug")
}
- HookURL: 填写slack web-hook地址
- AcceptedLevels: 设置日志输出级别
- Channel: 设置日志频道
- Username: 设置需要@的用户名
四、Go Blog 项目中的一个完整的log中间件
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
"os"
"time"
)
func Logger() gin.HandlerFunc {
log := logrus.New()
// 设置输出文件
filePath := "log/"
linkName := "latest_log.log"
// 打开指定处的文件,并指定权限为:可读可写,可创建
src, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755) //0755-> rwx r-x r-x linux知识
if err != nil {
fmt.Println("err:", err)
}
log.Out = src
// 设置日志级别。低于 Debug 级别的 Trace 将不会被打印
log.SetLevel(logrus.DebugLevel)
// 设置日志切割 rotatelogs
writer, _ := rotatelogs.New(
filePath+"%Y%m%d.log",
//在项目根目录下生成软链文件 latest_log.log 指向最新的日志文件。注意!!!必须在管理员权限下开终端启动。
rotatelogs.WithLinkName(linkName),
//日志最大保存时间
rotatelogs.WithMaxAge(7*24*time.Hour),
////设置日志切割时间间隔(1天)(隔多久分割一次)
rotatelogs.WithRotationTime(24*time.Hour),
)
// lfshook 决定哪些日志级别可用日志分割
writeMap := lfshook.WriterMap{
logrus.PanicLevel: writer,
logrus.FatalLevel: writer,
logrus.ErrorLevel: writer,
logrus.WarnLevel: writer,
logrus.InfoLevel: writer,
logrus.DebugLevel: writer,
}
// 配置 lfshook
hook := lfshook.NewHook(writeMap, &logrus.TextFormatter{
// 设置日期格式
TimestampFormat: "2006.01.02 - 15:04:05",
})
//为 logrus 实例添加自定义 hook
log.AddHook(hook)
return func(c *gin.Context) {
// 一.配置所需的 Fields
startTime := time.Now()
c.Next()
spendTime := time.Since(startTime).Milliseconds()
ST := fmt.Sprintf("%d ms", spendTime) // 1.API 调用耗时
hostName, err := os.Hostname() // 2.主机名
if err != nil {
hostName = "unknown"
}
statusCode := c.Writer.Status() // 3.状态码
clientIP := c.ClientIP() // 4.请求客户端的 IP
userAgent := c.Request.UserAgent() // 5.用户代理,通常是某个浏览器。dev环境下是apipost
dataSize := c.Writer.Size() // 6.响应报文 body 的字节长度
if dataSize < 0 {
dataSize = 0
}
method := c.Request.Method // 7.请求方法
path := c.Request.RequestURI // 8.请求 URL
// 二.从标准记录器创建一个条目,并向其中添加多个字段(隐式添加 log 本身的时间戳,信息等 fields )
entry := log.WithFields(logrus.Fields{
"HostName": hostName,
"Status": statusCode,
"SpendTime": ST,
"IP": clientIP,
"UserAgent": userAgent,
"Method": method,
"DataSize": dataSize,
"Path": path,
})
// Errors 保存了使用当前context的所有中间件/handler 所产生的全部错误信息。
// 源码注释: Errors is a list of errors attached to all the handlers/middlewares who used this context.
// 三.将系统内部的错误 log 出去
if len(c.Errors) > 0 {
log.Error(c.Errors.ByType(gin.ErrorTypePrivate).String())
}
// 四.根据状态码决定打印 log 的等级
if statusCode >= 500 {
entry.Error()
} else if statusCode >= 400 {
entry.Warn()
} else {
entry.Info()
}
}
}