Go项目开发中的第三方日志库 logrus

Posted by OuterCyrex on July 4, 2024

logrus库的使用

一.基础语句

可以通过代码go get github.com/sirupsen/logrus来获取logrus库(go中更常称作而不是

通过logrus库可以获得更高效的日志输出。

1.输出等级

下列为logrus库中常见的几种输出格式

1
2
3
4
5
logrus.Error("ERROR")
logrus.Warnln("Warn")
logrus.Infof("Info")
logrus.Debugf("Debug")
logrus.Println("Println")

输出结果:

1
2
3
4
time="2024-06-12T20:07:43+08:00" level=error msg=ERROR
time="2024-06-12T20:07:43+08:00" level=warning msg=Warn
time="2024-06-12T20:07:43+08:00" level=info msg=Info
time="2024-06-12T20:07:43+08:00" level=info msg=Println

其中Debugf并未成功输出,是因为logrus内存在输出的优先级,当低于该优先级时,该日志便不会输出。可以通过logrus.GetLevel()来获取当前的优先级信息

1
2
3
fmt.Println(logrus.GetLevel())
//输出结果
time="2024-06-12T20:08:56+08:00" level=info msg=Println

即此时的输出等级info级别,低于info级别的日志将不会被输出。

其中logrus库中的等级排序为:

1
2
3
4
5
6
7
8
9
var AllLevels = []Level{
	PanicLevel,
	FatalLevel,
	ErrorLevel,
	WarnLevel,
	InfoLevel,
	DebugLevel,
	TraceLevel,
}

如果我们想更改最低输出等级,可以通过logrus.SetLevel()来继续修改。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
	logrus.SetLevel(logrus.DebugLevel)
	logrus.Error("ERROR")
	logrus.Warnln("Warn")
	logrus.Infof("Info")
	logrus.Debugf("Debug")
}
//输出结果
time="2024-06-12T20:16:24+08:00" level=error msg=ERROR
time="2024-06-12T20:16:24+08:00" level=warning msg=Warn
time="2024-06-12T20:16:24+08:00" level=info msg=Info
time="2024-06-12T20:16:24+08:00" level=debug msg=Debug

此时,Debugf便可正常输出。

对于所有的7个等级中,常用的等级为Error、Warn、Info、Debug,其余的等级存在特殊性,如Panic会引起程序恐慌(Panic),而Fatal则会在输出日志后退出。

2.设置特定字段

通过logrus.WithField指令,可以增加日志输出时的字段

1
2
3
4
5
6
func main() {
	log := logrus.WithField("first_name", "Outer").WithField("last_name", "Cyrex")
	log.Errorf("Error")
}
//输出结果
time="2024-06-12T20:24:31+08:00" level=error msg=Error first_name=Outer last_name=Cyrex

更常用的是logrus.WithFields,能够批量增加输出字段

1
2
3
4
5
6
7
8
9
10
func main() {
	log := logrus.WithFields(logrus.Fields{
		"name": "Outer",
		"ip":   "127.0.0.1",
		"port": 8080,
	})
	log.Errorf("Error")
}
//输出结果
time="2024-06-12T20:28:25+08:00" level=error msg=Error ip=127.0.0.1 name=Outer port=8080

由于logrus为我们默认提供的输出格式是Text格式,我们也可以选择将其更改为JSON格式

1
2
logrus.SetFormatter(&logrus.JSONFormatter{})//JSON格式
logrus.SetFormatter(&logrus.TextFormatter{})//Text格式

若以JSON格式输出,则会输出下列字段:

1
2
3
4
5
6
7
8
9
//JSON格式已美化
{
    "ip":"127.0.0.1",
    "level":"error",
    "msg":"Error",
    "name":"Outer",
    "port":8080,
    "time":"2024-06-12T20:35:25+08:00",
}

通过ForceColors可以调节颜色输出,但只能应用于Text格式

1
2
3
4
5
6
7
8
logrus.SetFormatter(&logrus.TextFormatter{
		ForceColors: true,
	})
//输出效果
ERRO[0000] ERROR  //红色                                      
WARN[0000] Warn   //黄色                                      
INFO[0000] Info   //蓝色                                      
DEBU[0000] Debug  //灰色                                      

其中,SetFormatter的常用指令还有

1
2
3
4
5
6
7
ForceColors:     true, 		//是否强制颜色输出
FullTimestamp:   true, 		//是否输出完整的时间戳
TimestampFormat: "2006-01-02 15:04:05",		//自定义时间戳格式
DisableColors:   true,		//是否关闭颜色显示
DisableQuote:    true,		//是否禁止引用所有制
ForceQuote:      true,		//是否强制引用所有值
DisableTimestamp:  true,	//是否关闭时间戳

补充:自定义颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
	//字体色
	fmt.Println("\033[30mBlack\033[0m")
	fmt.Println("\033[31mRed\033[0m")
	fmt.Println("\033[32mGreen\033[0m")
	fmt.Println("\033[33mYellow\033[0m")
	fmt.Println("\033[34mBlue\033[0m")
	fmt.Println("\033[35mPurple\033[0m")
	fmt.Println("\033[36mCyan\033[0m")
	fmt.Println("\033[37mGrey\033[0m")
	//背景色
	fmt.Println("\033[40mBlack\033[0m")
	fmt.Println("\033[41mRed\033[0m")
	fmt.Println("\033[42mGreen\033[0m")
	fmt.Println("\033[43mYellow\033[0m")
	fmt.Println("\033[44mBlue\033[0m")
	fmt.Println("\033[45mPurple\033[0m")
	fmt.Println("\033[46mCyan\033[0m")
	fmt.Println("\033[47mGrey\033[0m")
}

二.自定义输出

1.文件输出

在进行文件输出之前,需要熟悉os库的OpenFile函数是如何使用的。

如下所示,OpenFile的输入值有三个,分别为相对路径、附加指令、权限掩码

附加指令的定义如下方const内的内容。

1
2
3
4
5
6
7
8
9
10
11
12
func OpenFile(name string, flag int, perm FileMode) (*File, error)
const (
    //O_RDONLY、O_WRONLY或O_RDWR中必存在且仅能存在一个
    O_RDONLY int = syscall.O_RDONLY // 打开的文件只可读
    O_WRONLY int = syscall.O_WRONLY // 打开的文件只可写
    O_RDWR   int = syscall.O_RDWR   // 打开的文件可读可写
    O_APPEND int = syscall.O_APPEND // 写入时将数据附加到文件中
    O_CREATE int = syscall.O_CREAT  // 若文件不存在则创建新文件
    O_EXCL   int = syscall.O_EXCL   // 与O_CREATE一起使用时,文件不能存在。
    O_SYNC   int = syscall.O_SYNC   // 打开以进行同步I/O。
    O_TRUNC  int = syscall.O_TRUNC  // 打开时截断常规可写文件
)

而最后的perm字段用于指定新文件的权限和/或现有文件的权限掩码(umask),若没有权限掩码时,文件的默认权限为0666,文件夹的默认权限为0777

Unix-like系统中,权限位通常由三组数字表示,每组三位,分别对应文件的所有者(user)、所属组(group)和其他人(others)的权限。每组数字的每一位代表一种权限:读(r,数字4)、写(w,数字2)和执行(x,数字1)。

一些常见的事例为:

  • 0644: 所有者有读写权限,组和其他人有读权限。(rw-r–r–)
  • 0755: 所有者有读写执行权限,组有读和执行权限,其他人有读和执行权限。(rwxr-xr-x)
  • 0600: 所有者有读写权限,组和其他人没有任何权限。(rw——-)
  • 0777: 所有者、组和其他人都有读写执行权限。(rwxrwxrwx)

由上方的介绍便可以理解os.Create()的内容了

1
2
3
func Create(name string) (*File, error) {
	return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}

由此,我们可以将日志内容写入log/gin.log文件中

1
2
3
4
5
6
7
8
9
10
11
func main() {
	file, _ := os.OpenFile("log/gin.log", os.O_CREATE|os.O_WRONLY, 0666)
	logrus.SetOutput(file)

	logrus.SetFormatter(&logrus.TextFormatter{
		DisableTimestamp: true,
	})
	logrus.Infof("Hello,World!")
}
//log文件内
level=info msg="Hello,World!"

同样,为了让日志即在文件中写入,又可在控制台输出

可以采用io.MultiWriter()来实现

1
2
3
4
5
6
7
writers := []io.Writer{
		file,
		os.Stdout,
	}
logrus.SetOutput(io.MultiWriter(writers...))
//或者也可直接写入
logrus.SetOutput(io.MultiWriter(file, os.Stdout))

2.自定义输出格式

为了自定义输出格式,我们需要定义一个结构体来存储方法

1
2
3
4
5
6
7
8
9
10
11
12
type myFormatter struct{}
func (myFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	var b *bytes.Buffer
	if entry.Buffer == nil {
		b = &bytes.Buffer{}
	} else {
		b = entry.Buffer
	}
	TimeForm := entry.Time.Format("2006-01-02")
	_, _ = fmt.Fprintf(b, "Level:%s msg:%s time:[%s]", entry.Level, entry.Message, TimeForm)
	return b.Bytes(), nil
}

随后在主函数中调用myFormatter来实现自定义输出格式

1
2
3
4
logrus.SetFormatter(Format)
logrus.Infof("Hello,World!")
//输出格式
Level:info msg:Hello,World! time:[2024-06-12]

通过在Format内加入下列代码可以输出详细的文件信息行号以及对应的函数名

1
2
fileVal := fmt.Sprintf("file:%s line:%d", path.Base(entry.Caller.File), entry.Caller.Line)
funcname := entry.Caller.Func.Name()

其中,path.Base可以截取文件路径中相对于当前项目的相对路径

加入上述代码后的输出结果:

1
2
Level:info msg:Hello,World! time:[2024-06-12] file:lesson 15.go line:36 Funcname:main.main
//main.main是因为函数为匿名函数

上述代码能够成功实现的前提是要注意开启ReportCaller

1
2
logrus.SetReportCaller(true)
//SetReportCaller用来设置是否标准日志将包含calling方法

此外,可以在方法对应的结构体中传参

1
2
3
4
5
6
type myFormatter struct {
	prefix string
}
_, _ = fmt.Fprintf(b, "[%s] Level:%s msg:%s time:[%s] %s Funcname:%s", f.prefix, entry.Level, entry.Message, TimeForm, fileVal, funcname)
//在Format方法中加入前缀的输出
logrus.SetFormatter(&myFormatter{prefix: "Gin"})//在调用时传入参数"Gin"

此时便可灵活的输出结果:

1
[Gin] Level:info msg:Hello,World! time:[2024-06-12] file:lesson 15.go line:38 Funcname:main.main

三.Hook

Hook作为logrus的重要功能,主要用于扩展日志系统的功能,允许用户自定义日志的处理逻辑。其作用类似于选择特定等级的日志进行特定操作钩子

Hook作为一个接口类型,需要接受一个Levels()方法和一个Fire()方法

1
2
3
4
type Hook interface {
    Levels() []Level
    Fire(*Entry) error
}

此处的Levels()通过返回的等级值来判断哪些等级会触发Hook机制。而Fire(*Entry)则是Hook机制的本体,通过设定Fire()的内容来决定Hook将进行什么工作。

如何使用Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
type myHook struct{}

func (*myHook) Levels() []logrus.Level {
	return logrus.AllLevels
}
func (h *myHook) Fire(entry *logrus.Entry) error {
	fmt.Println(entry.Message)
	return nil
}
func main() {
	logrus.AddHook(&myHook{})
	logrus.Errorf("hello world!")
}

上述代码首先创建了一个用于存储方法的结构体,存储了两个方法Levels的返回值是logrus.AllLevels,即所有等级均可触发Hook。后边定义Fire的内容,即输出entry.Message

1
entry.Data["name"] = "OuterCyrex"

通过在Fire中加入上述语句,可以让所有被Hook处理的entry语句加上特定信息,如name=OuterCyrex

1
2
3
func (*myHook) Levels() []logrus.Level {
	return []logrus.Level{logrus.ErrorLevel}
}

通过设置Levels()方法的返回值,我们可以指定哪些类型的日志将会被处理。但注意Levels()的返回值一定为[]logrus.Level类型。

1
2
3
4
5
func (h *myHook) Fire(entry *logrus.Entry) error {
	file, _ := os.OpenFile("log/Error.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	_, _ = file.WriteString(entry.Message)
	return nil
}

通过对Fire()方法进行上述的更改,可以实现对特定等级的日志存入特定的文件。

四.日志分割

1.按时间分割

通过对日志写入的时间进行划分,将不同时间的日志纳入对应时间的文件夹,可以便于日志的存储查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
type FileDateHook struct {
	file     *os.File
	logPath  string
	fileDate string
	appName  string
}

func (hook *FileDateHook) Levels() []logrus.Level {
	return logrus.AllLevels
}

func (hook *FileDateHook) Fire(entry *logrus.Entry) error {
	Timer := time.Now().Format("2006-01-02")
	if Timer == hook.fileDate {
		_, _ = hook.file.WriteString(entry.Time.Format("2006-01-02 15:04:05") + " " + entry.Message + "\n")
	}
	hook.fileDate = Timer
	err := hook.file.Close()
	if err != nil {
		logrus.Fatal(err)
	}
	err = os.MkdirAll(fmt.Sprintf("%s/%s", hook.logPath, hook.fileDate), os.ModePerm)
	filename := fmt.Sprintf("%s/%s/%s.log", hook.logPath, hook.fileDate, hook.appName)
	hook.file, _ = os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
	_, _ = hook.file.WriteString(entry.Time.Format("2006-01-02 15:04:05") + " " + entry.Message + "\n")
	return nil
}

func initHook(logPath string, appName string) {
	fileDate := time.Now().Format("2006-01-02")
	err := os.MkdirAll(fmt.Sprintf("%s/%s", logPath, fileDate), os.ModePerm)
	if err != nil {
		logrus.Fatal(err)
	}
	filename := fmt.Sprintf("%s/%s/%s.log", logPath, fileDate, appName)
	file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		logrus.Fatal(err)
	}
	hook := FileDateHook{file, logPath, fileDate, appName}
	logrus.AddHook(&hook)
}

上述代码通过日期为单位来划分对应的日志文件。

initHook函数用于创建首个文件夹并将file、logPath、fileDate、appName初始信息录入Hook结构体中,便于后续比较。

HookFire方法主要用于判断日期是否相同,以此来实现划分,实现了若日期相同则直接写入现有的文件,若日期不同则创建新的文件夹并写入。

2.按日志等级分割

通过日志等级分割主要是采用了Hook的选择功能

下边的代码展示了一种日志等级分割的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package main

import (
	"fmt"
	log "github.com/sirupsen/logrus"
	"os"
)

const (
	allLog   = "all"
	errLog   = "error"
	warnLog  = "warn"
	infoLog  = "info"
	otherLog = "other"
)

type FileLevelHook struct {
	Allfile   *os.File
	Errfile   *os.File
	Warnfile  *os.File
	Infofile  *os.File
	Otherfile *os.File
	logPath   string
}

func (hook *FileLevelHook) Levels() []log.Level {
	return log.AllLevels
}

func (hook *FileLevelHook) Fire(entry *log.Entry) error {
	line, _ := entry.String()
	switch entry.Level {
	case log.ErrorLevel:
		hook.Errfile.WriteString(line)
	case log.WarnLevel:
		hook.Warnfile.WriteString(line)
	case log.InfoLevel:
		hook.Infofile.WriteString(line)
	default:
		hook.Otherfile.WriteString(line)
	}
	hook.Allfile.WriteString(line)
	return nil
}
func InitLevel(logPath string) {
	err := os.MkdirAll(logPath, os.ModePerm)
	if err != nil {
		log.Fatal(err)
		return
	}
	All, _ := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, allLog), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	Err, _ := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, errLog), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	Warn, _ := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, warnLog), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	Info, _ := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, infoLog), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	Other, _ := os.OpenFile(fmt.Sprintf("%s/%s.log", logPath, otherLog), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	fileHook := FileLevelHook{All, Err, Warn, Info, Other, logPath}
	log.AddHook(&fileHook)
}

首先,const定义了一些字符串常量,便于之后再创建文件和打开文件时的调用,也可以不定义常量,直接在打开文件时输入对应的字符串即可。

之后在定义Hook结构体时,定义了多个*os.file字段,便于后续对应文件的处理。

Fire(*entry)中以等级对日志内容进行了分辨,并存入写入对应的文件中。

InitLevel中的MkdirAll函数用于创建一个文件夹。剩余操作便是创建/打开文件,并启用Hook

通过下方的主函数进行测试:

1
2
3
4
5
6
7
8
func main() {
	InitLevel("newlog")
	log.SetLevel(log.DebugLevel)
	log.Error("ERROR")
	log.Warnln("Warn")
	log.Infof("Info")
	log.Debugf("Debug")
}

测试结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//创建了一个文件夹名为newlog,其内部包含5个文件,分别为all.log、error.log、info.log、other.log、warn.log
//all.log文件内容:
time="2024-06-13T11:59:42+08:00" level=error msg=ERROR
time="2024-06-13T11:59:42+08:00" level=warning msg=Warn
time="2024-06-13T11:59:42+08:00" level=info msg=Info
time="2024-06-13T11:59:42+08:00" level=debug msg=Debug
//error.log文件内容:
time="2024-06-13T11:59:42+08:00" level=error msg=ERROR
//info.log文件内容:
time="2024-06-13T11:59:42+08:00" level=info msg=Info
//other.log文件内容:
time="2024-06-13T11:59:42+08:00" level=debug msg=Debug
//warn.log文件内容:
time="2024-06-13T11:59:42+08:00" level=warning msg=Warn

3.基于lumberjack的滚动日志

在实际开发过程中,为了节省磁盘方便查看,日志需要按照时间或者大小维度进行切割分成多分归档过期的日志,删除久远的日志。这个就是在日常开发中经常遇见的日志滚动(log rotation)logrus 本身并没有实现滚动日志功能,但是我们可以使用第三方滚动插件lumberjack来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
	log "github.com/Sirupsen/logrus"
	"gopkg.in/natefinch/lumberjack.v2"
)

func main() {
	logger := &lumberjack.Logger{
    	// 日志输出文件路径。
		Filename:   "log/gin.log", 
    	// 日志文件最大 size, 单位是 MB。
		MaxSize:    100,
    	// 最大过期日志保留的个数。
		MaxBackups: 10,
    	// 保留过期文件的最大时间间隔,单位天。
		MaxAge:     30,
    	// 是否需要压缩滚动日志, 使用的 gzip 压缩,缺省为 false。
		Compress:   true,
	}
	log.SetOutput(logger) // 设置 logrus 的 io.Writer
}

五.gin集成logrus

gin框架提供的全局中间件可以很方便的传输回每次访问的信息和内容。

我们可以通过设置一个日志中间件来实现将每次访问信息都写入日志内。

实际是,gin框架本身自带的Logger()方法也是通过全局中间件来实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package Middleware

import (
	"bytes"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	"path"
)

type myFormatter struct {
	prefix string
}

func (f myFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	var b *bytes.Buffer
	if entry.Buffer == nil {
		b = &bytes.Buffer{}
	} else {
		b = entry.Buffer
	}
	funcname := entry.Caller.Func.Name()
	fileVal := fmt.Sprintf("file:%s line:%d", path.Base(entry.Caller.File), entry.Caller.Line)
	TimeForm := entry.Time.Format("2006-01-02")
	_, _ = fmt.Fprintf(b, "[%s] Level:%s Infomation:%s time:[%s] %s Funcname:%s\n", f.prefix, entry.Level, entry.Message, TimeForm, fileVal, funcname)
	return b.Bytes(), nil
}

func LogMiddleware() gin.HandlerFunc {
	logrus.SetReportCaller(true)
	logrus.SetFormatter(&myFormatter{prefix: "Gin"})
	return func(c *gin.Context) {
		logrus.Infof("%s | %s | %s ", c.ClientIP(), c.Request.Method, c.Request.RequestURI)
	}
}

其中的内容可以自定义的进行更改。

通过在程序中使用全局中间件来写入日志。

1
2
3
4
5
6
7
8
9
func main() {
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()
	router.Use(Middleware.LogMiddleware())
	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "Hello World"})
	})
	router.Run(":8080")
}

通过上述代码,每次访问网页时都会向后端传入访问数据并写入日志中。

1
2
3
//访问localhost:8080/
[Gin] Level:info Infomation:::1 | GET | /  time:[2024-06-16] file:middleware.go line:33 
Funcname:bin/gin_logrus/Middleware.LogMiddleware.func1

上述代码只能实现将日志内容在控制台中输出,要想写入日志文件还需要一定的操作。

通过在LogMiddleware函数中加入下述语句,将日志写入gin.log

1
2
3
4
5
6
file, _ := os.OpenFile("gin_logrus/log/gin.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	writers := []io.Writer{
		file,
		os.Stdout,
	}
logrus.SetOutput(io.MultiWriter(writers...))

此时在同目录的log文件内将出现gin.log文件,内容为:

1
2
3
4
5
6
7
//连续访问5次localhost:8080
//gin.log的内容为:
[Gin] Level:info msg:::1 | GET | /  time:[2024-06-16] file:middleware.go line:41 Funcname:bin/gin_logrus/Middleware.LogMiddleware.func1
[Gin] Level:info msg:::1 | GET | /  time:[2024-06-16] file:middleware.go line:41 Funcname:bin/gin_logrus/Middleware.LogMiddleware.func1
[Gin] Level:info msg:::1 | GET | /  time:[2024-06-16] file:middleware.go line:41 Funcname:bin/gin_logrus/Middleware.LogMiddleware.func1
[Gin] Level:info msg:::1 | GET | /  time:[2024-06-16] file:middleware.go line:41 Funcname:bin/gin_logrus/Middleware.LogMiddleware.func1
[Gin] Level:info msg:::1 | GET | /  time:[2024-06-16] file:middleware.go line:41 Funcname:bin/gin_logrus/Middleware.LogMiddleware.func1

由此,我们可以十分方便的采用logrus库来记录gin框架的日志。

也可以结合之前的日期分割来搭建日志库,便于gin框架日志的储存查看

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package LogCode

import (
	"fmt"
	"github.com/sirupsen/logrus"
	"os"
	"time"
)

type FileDateHook struct {
	file     *os.File
	logPath  string
	fileDate string
	appName  string
}

func (hook *FileDateHook) Levels() []logrus.Level {
	return logrus.AllLevels
}

func (hook *FileDateHook) Fire(entry *logrus.Entry) error {
	Timer := time.Now().Format("2006-01-02")
	if Timer == hook.fileDate {
		_, _ = hook.file.WriteString("Time:[" + entry.Time.Format("2006-01-02 15:04:05") + "] " + entry.Message + "\n")
		return nil
	}
	hook.fileDate = Timer
	err := hook.file.Close()
	if err != nil {
		logrus.Fatal(err)
	}
	err = os.MkdirAll(fmt.Sprintf("%s/%s", hook.logPath, hook.fileDate), os.ModePerm)
	filename := fmt.Sprintf("%s/%s/%s.log", hook.logPath, hook.fileDate, hook.appName)
	hook.file, _ = os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
	_, _ = hook.file.WriteString("Time:[" + entry.Time.Format("2006-01-02 15:04:05") + "] " + entry.Message + "\n")
	return nil
}

func InitHook(logPath string, appName string) {
	fileDate := time.Now().Format("2006-01-02")
	err := os.MkdirAll(fmt.Sprintf("%s/%s", logPath, fileDate), os.ModePerm)
	if err != nil {
		logrus.Fatal(err)
	}
	filename := fmt.Sprintf("%s/%s/%s.log", logPath, fileDate, appName)
	file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		logrus.Fatal(err)
	}
	hook := FileDateHook{file, logPath, fileDate, appName}
	logrus.AddHook(&hook)
}

基本思路同之前的日期分割一致。

此时便只需要在主程序中写入InitHook便可以创建Hook,实现Hook的初始化。

接下来写入中间件函数

1
2
3
4
5
func LogMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		logrus.Infof("[GIN] %s | %s | %s", c.ClientIP(), c.Request.Method, c.Request.RequestURI)
	}
}

注意,此处Infof的内容决定了输出时Entry.Message的内容

将上述两个函数导入主程序中,引用其对应的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
	"bin/gin_logrus/LogCode"
	"bin/gin_logrus/Middleware"
	"github.com/gin-gonic/gin"
)

func main() {
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()
	LogCode.InitHook("log", "GIN")
	router.Use(Middleware.LogMiddleware())
	router.GET("/", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "Hello World"})
	})
	router.Run(":8080")
}

这样便可以获得一个根据日期来分割的记录gin框架日志的文件了。

通过logrus库,可以很方便的实现日志的记录分割,便于网站管理查询访问记录。