15 日志库

15.1 标准日志库 log

原文链接 《Go语言标准库log介绍

Go 语言内置的 log 包实现了简单的日志服务。

15.1.1 使用 Logger

log 包定义了 Logger 类型,该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”logger,可以通过调用函数 Print系列(Print|Printf|Println)Fatal系列(Fatal|Fatalf|Fatalln)、和 Panic系列(Panic|Panicf|Panicln)来使用,比自行创建一个 logger 对象更容易使用。

例如,我们可以像下面的代码一样直接通过 log 包来调用上面提到的方法,默认会将日志信息打印到终端界面

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"log"
)

func main() {
log.Println("普通的日志打印")
str := "日志打印"
log.Printf("格式化的 %s", str)
log.Fatalln("Fatal 日志打印")
log.Panicln("Panic 日志打印")
}

编译并执行上面的代码会得到如下输出:

1
2
3
4
2020/09/17 08:21:23 普通的日志打印
2020/09/17 08:21:23 格式化的 日志打印
2020/09/17 08:21:23 Fatal 日志打印
exit status 1
  • logger 会打印每条日志信息的日期、时间,默认输出到系统的终端。
  • Fatal 系列函数会在写入日志信息后调用 os.Exit(1)
  • Panic系列函数会在写入日志信息后 panic。

15.1.2 配置 logger

15.1.2.1 标准 logger 的配置

默认情况下的 logger 只会提供日志的时间信息,但是很多情况下我们希望得到更多信息,比如记录该日志的文件名和行号等。log 标准库中为我们提供了定制这些设置的方法。

log 标准库中的 Flags 函数 会返回标准 logger 的输出配置,而 SetFlags函数 用来设置标准 logger 的输出配置。

1
2
func Flags() int
func SetFlags(flag int)

15.1.2.2 flag 选项

log 标准库提供了如下的 flag 选项,它们是一系列定义好的常量。

1
2
3
4
5
6
7
8
9
10
11
const (
// 控制输出日志信息的细节,不能控制输出的顺序和格式。
// 输出的日志在每一项后会有一个冒号分隔:例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
Ldate = 1 << iota // 日期:2009/01/23
Ltime // 时间:01:23:23
Lmicroseconds // 微秒级别的时间:01:23:23.123123(用于增强Ltime位)
Llongfile // 文件全路径名+行号: /a/b/c/d.go:23
Lshortfile // 文件名+行号:d.go:23(会覆盖掉Llongfile)
LUTC // 使用UTC时间
LstdFlags = Ldate | Ltime // 标准logger的初始值
)

下面我们在记录日志之前先设置一下标准logger的输出选项如下:

1
2
3
4
5
6
7
8
9
10
package main

import (
"log"
)

func main() {
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("定制 flag 之后的日志打印")
}

编译执行后得到的输出结果如下:

1
2020/09/17 08:29:05.719377 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:9: 定制 flag 之后的日志打印

15.1.2.3 配置日志前缀

log标准库中还提供了关于日志信息前缀的两个方法:

1
2
func Prefix() string
func SetPrefix(prefix string)

其中 Prefix函数 用来查看标准 logger 的输出前缀,SetPrefix函数 用来设置输出前缀。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"log"
)

func main() {
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("定制 flag 之后的日志打印")
log.SetPrefix("[CnPeng]")
log.Println("定制 flag 和 prefix 之后的日志打印")
}

上面的代码输出如下:

1
2
2020/09/17 08:35:08.293025 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:9: 定制 flag 之后的日志打印
[CnPeng]2020/09/17 08:35:08.293197 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:11: 定制 flag 和 prefix 之后的日志打印

这样我们就能够在代码中为我们的日志信息添加指定的前缀,方便之后对日志信息进行检索和处理。

15.1.2.4 配置日志输出位置

1
func SetOutput(w io.Writer)

SetOutput函数 用来设置标准 logger 的输出目的地,默认是标准错误输出。

例如,下面的代码会把日志输出到同目录下的 xx.log 文件中。

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
package main

import (
"fmt"
"log"
"os"
)

func main() {

// 创建或者打开 log 文件
logFile, err := os.OpenFile("./日志信息.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)

if nil != err {
fmt.Println("打开日志文件出错", err)
return
}
// 指定日志输出文件
log.SetOutput(logFile)

log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
log.Println("定制 flag 之后的日志打印")
log.SetPrefix("[CnPeng]")
log.Println("定制 flag 和 prefix 之后的日志打印")
}

![](/note_image/Go/1 Go标准库/log/pics/15-1-指定日志输出文件.png)

日志信息.log 文件中的内容如下:

1
2
2020/09/17 08:40:57.335316 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:22: 定制 flag 之后的日志打印
[CnPeng]2020/09/17 08:40:57.335490 /Users/cnpeng/CnPeng/04_Demos/12_Go/oldBoy/varAndConst/const.go:24: 定制 flag 和 prefix 之后的日志打印

如果要使用标准的 logger,我们通常会把上面的配置操作写到 init 函数中。

1
2
3
4
5
6
7
8
9
func init() {
logFile, err := os.OpenFile("./xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Println("open log file failed, err:", err)
return
}
log.SetOutput(logFile)
log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
}

15.1.3 创建logger

log 标准库中还提供了一个创建新 logger 对象的构造函数–New,支持我们创建自己的 logger 示例。New函数的签名如下:

1
func New(out io.Writer, prefix string, flag int) *Logger

New 创建一个 Logger 对象。其中,

  • 参数 out 设置日志信息写入的目的地。
  • 参数 prefix 会添加到生成的每一条日志前面。
  • 参数 flag 定义日志的属性(时间、文件等等)。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"log"
"os"
)

func main() {
// Stdout 表示终端
cusLog := log.New(os.Stdout, "[CnPeng]", log.Lshortfile|log.Ldate|log.Ltime)
cusLog.Println("自定义的log对象输出日志")
}

将上面的代码编译执行之后,得到结果如下:

1
[CnPeng]2020/09/17 08:48:34 const.go:10: 自定义的log对象输出日志

15.1.4 总结

Go 内置的 log 库功能有限,例如无法满足记录不同级别日志的情况,我们在实际的项目中根据自己的需要选择使用第三方的日志库,如 logruszap 等。

15.2 第三方日志库 logrus 使用

日志是程序中必不可少的一个环节,由于 Go 语言内置的日志库功能比较简洁,我们在实际开发中通常会选择使用第三方的日志库来进行开发。本文介绍了 logrus 这个日志库的基本使用。

15.2.1 logrus介绍

Logrus 是 Go(golang)的结构化 logger,与标准库 logger 的 API 完全兼容。

它有以下特点:

  • 完全兼容标准日志库,拥有七种日志级别:Trace, Debug, Info, Warning, Error, Fataland, Panic
  • 可扩展的 Hook 机制,允许使用者通过 Hook 的方式将日志分发到任意地方,如本地文件系统,logstashelasticsearch或者mq等,或者通过 Hook 定义日志内容和格式等
  • 可选的日志输出格式,内置了两种日志格式 JSONFormaterTextFormatter,还可以自定义日志格式
  • Field 机制,通过 Filed 机制进行结构化的日志记录
  • 线程安全

15.2.2 安装

CnPeng 不执行该安装也可以。不安装时直接在使用的地方导入,运行之后就会主动的安装。

1
go get github.com/sirupsen/logrus

15.2.3 基本示例

使用 Logrus 最简单的方法是简单的包级导出日志程序:

1
2
3
4
5
6
7
8
9
package main

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

func main() {
log.WithFields(log.Fields{"animal": "dog"}).Info("引入三方日志库")
}

运行结果如下:

1
2
3
4
CnPeng:varAndConst cnpeng$ go run const.go 
go: finding module for package github.com/sirupsen/logrus
go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.6.0
INFO[0000] 引入三方日志库 animal=dog

15.2.4 进阶示例

对于更高级的用法,例如在同一应用程序记录到多个位置,你还可以创建 logrus Logger 的实例:

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 (
"os"
"github.com/sirupsen/logrus"
)

func main() {
log := logrus.New()
log.Out = os.Stdout
log.WithFields(logrus.Fields{"animal": "dog"}).Info("引入三方日志库")
log.Info("logrus.Fields 是可选的")

fileObj, err := os.OpenFile("logrus.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
if nil != err {
log.Info("创建日志文件出错了")
return
}

log.Out = fileObj
log.Info("这一句日志信息将写入到 logrus.log 文件中")
}

上述代码运行之后,控制台的输出信息为:

1
2
3
CnPeng:varAndConst cnpeng$ go run const.go
INFO[0000] 引入三方日志库 animal=dog
INFO[0000] logrus.Fields 是可选的

另外,还会在当前 go 文件的同级目录下创建一个 logrus.log 文件,如下:

![](/note_image/Go/1 Go标准库/log/pics/15-2-logrus进阶用法.png)

15.2.5 日志级别

15.2.5.1 日志级别

Logrus 有七个日志级别:Trace, Debug, Info, Warning, Error, Fataland ,Panic

1
2
3
4
5
6
7
8
9
log.Trace("Something very low level.")
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
// 记完日志后会调用os.Exit(1)
log.Fatal("Bye.")
// 记完日志后会调用 panic()
log.Panic("I'm bailing.")

15.2.5.2 设置日志级别

你可以在 Logger 上设置日志记录级别,然后它只会记录具有该级别或以上级别任何内容的条目:

1
2
// 会记录info及以上级别 (warn, error, fatal, panic)
log.SetLevel(log.InfoLevel)

如果你的程序支持 debug 或环境变量模式,设置 log.Level = logrus.DebugLevel 会很有帮助。

15.2.6 字段

Logrus 鼓励通过日志字段进行谨慎的结构化日志记录,而不是冗长的、不可解析的错误消息。

例如,区别于使用 log.Fatalf("Failed to send event %s to topic %s with key %d"),你应该使用如下方式记录更容易发现的内容:

1
2
3
4
5
log.WithFields(log.Fields{
"event": event,
"topic": topic,
"key": key,
}).Fatal("Failed to send event")

WithFields 的调用是可选的。

15.2.7 默认字段

通常,将一些字段始终附加到应用程序的全部或部分的日志语句中会很有帮助。例如,你可能希望始终在请求的上下文中记录 request_iduser_ip

区别于在每一行日志中写上 log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip}),你可以向下面的示例代码一样创建一个 logrus.Entry 去传递这些字段。

1
2
3
4
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
# will log request_id and user_ip
requestLogger.Info("something happened on that request")
requestLogger.Warn("something not great happened")

15.2.8 日志条目

除了使用 WithFieldWithFields 添加的字段外,一些字段会自动添加到所有日志记录事中:

  • time:记录日志时的时间戳
  • msg:记录的日志信息
  • level:记录的日志级别

15.2.9 Hooks

你可以添加日志级别的钩子(Hook)。例如,向异常跟踪服务发送 ErrorFatal Panic、信息到StatsD 或同时将日志发送到多个位置,例如 syslog。

Logrus 配有内置钩子。在 init 中添加这些内置钩子或你自定义的钩子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import (
log "github.com/sirupsen/logrus"
"gopkg.in/gemnasium/logrus-airbrake-hook.v2" // the package is named "airbrake"
logrus_syslog "github.com/sirupsen/logrus/hooks/syslog"
"log/syslog"
)

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)
}
}

注意:Syslog 钩子还支持连接到本地 syslog(例如. /dev/log or /var/run/syslog or /var/run/log)。有关详细信息,请查看 syslog hook README。

15.2.10 格式化

logrus 内置以下两种日志格式化程序:

logrus.TextFormatterlogrus.JSONFormatter

还支持一些第三方的格式化程序,详见项目首页。

15.2.11 记录函数名

CnPeng 实际操作时,没看明白怎么得到的示例中的结果

如果你希望将调用的函数名添加为字段,请通过以下方式设置:

1
log.SetReportCaller(true)

这会将调用者添加为 ”method”,如下所示:

1
2
{"animal":"penguin","level":"fatal","method":"github.com/sirupsen/arcticcreatures.migrate","msg":"a penguin swims by",
"time":"2014-03-10 19:57:38.562543129 -0400 EDT"}

注意:开启这个模式会增加性能开销。

15.2.12 线程安全

默认的 logger 在并发写的时候是被 mutex 保护的,比如当同时调用 hook 和写 log 时 mutex 就会被请求,有另外一种情况,文件是以 appending mode 打开的, 此时的并发操作就是安全的,可以用logger.SetNoLock() 来关闭它。

15.2.13 gin 框架使用 logrus

Gin 是一个 go 写的 web 框架,具有高性能的优点。官方地址:https://github.com/gin-gonic/gin

CnPeng 下面的代码运行之后并没有得到想要的结果。。。 gin.log 文件中没有内容,不知道为啥

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
// a gin with logrus demo
package main

import (
"os"

"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)

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()
}

15.3 自定义实现日志库

视频 81-86, 重点是视频 85。

15.3.1 需求分析

一个日志库需要支持如下内容:

    1. 支持向不同的地方输出
    • fmt.Fprintf 指定内容写到哪里去
    • path.Join( filePath, fileName) 将路径和名字进行拼接,得到完整的路径
    • os.OpenFile(fullFileName, ,) 打开文件,后面两个参数为写入模式,
    1. 日志需要分级
    • Debug
    • Trace
    • Info
    • Waring
    • Error
    • Fatal
    1. 支持开关控制(开发接口可以打印任意级别的日志,线上环境则不允许打印日志)
    • 定义 int 常量用于比较级别
    1. 完整的日志记录要包含有时间、行号、文件名、日志级别、日志信息
    • time.Now() 获取时间
    • runtime.Caller(int) 获取文件信息(fileInfo)、行号、函数信息(funcInfo)。其中的 int 表示从该处上溯几层,获取的是上溯到层数的文件、行数、函数信息。
    • 然后通过 path.Base(fileInfo) 可以得到文件名
    • 通过 runtime.FuncForPc(funcInfo).NAme() 可以获取 包名.函数名, 然后再通过 strings.Split(,) 得到具体的函数名。
    • fmt.Sprintf(a,b...) 可以将 a 和 可变长参数 b 拼接成一个字符串。
    1. 日志文件需要切割
    • 可以按日期切割
    • 可以按文件大小切割,
      • 预定义一个单文件最大值,每次写日志时判断日志文件是否超出该值
      • 切割时,
        • 先关闭当前的日志文件——fileOj.close()
        • 然后通过 os.Rename() 对文件进行备份
        • 打开一个新的日志文件
        • 将新打开的日志文件对象作为删除目标

获取文件大小的操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获取文件大小
fileObj , err := os.Open("./main.go")
if err != nil {
fmt.Printf("文件打开失败,错误信息为:%v",err)
return
}

// 检查得到的文件对象类型,输出为:`*os.File`
fmt.Printf("%T\n",fileObj)

// 获取文件对象的详细信息
fileInfo,err := fileObj.Stat()
if err != nil {
fmt.Printf("获取文件信息时出错,错误为:%v\n",err)
return
}

// 得到的是字节
fmt.Printf("文件大小是:%dB\n",fileInfo.Size())

切割日志文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1 将原名字 xx.log 备份为 xx.log.bak20200927084105 
timeStr := time.Now().Format("20200927084105")
// 使用这种方式拼接不需要区分是 Win 还是 Linux
preLogName := path.Join(filePath,fileName)
newLogName := fmt.Sprintf("%s.bak%s",preLogName,timeStr)
os.Rename(preLogName,newLogName)

//2 关闭当前日志文件——假设当前日志文件对象为 fileObj
fileObj.Close()

//3 打开一个新的日志文件
newFileObj,err := os.OpenFile(newLogName, os.O_CREATE|os.O_APPEND|os.O_WRONLY,0644)
if err != nil {
fmt.Prinftf("文件打开出错了,%v\n",err)
return
}

//4 将新打开的文件对象赋值给日志对象中的文件对象字段。然后继续执行写操作即可。

15.3.2 自定义日志库

首字母大写的标识符是对外暴露的标识符,必须要添加注释信息。