### 1 日志基础
使用 Golang log 标准库的日志框架非常简单,仅仅提供了print,panic和fatal三个函数。
日志适用于追踪简单的活动,例如通过使用可用的选项在错误信息之前添加一个时间戳。
下面是一个 Golang 中如何记录错误日志的简单例子:
```go
package main
import (
"fmt"
"errors"
"log"
)
func div(a, b int) (ret int, err error) {
if b == 0 {
err = errors.New("division by zero")
return
}
ret = a / b
return
}
func main() {
/* 定义局部变量 */
a := 10
b := 0
/* 除法函数,除以 0 的时候会返回错误 */
ret, err := div(a, b)
if err != nil {
log.Fatal(err)
}
fmt.Println(ret)
}
```
如果尝试除以 0,就会得到类似下面的结果:
```shell
2017/02/24 16:13:30 division by zero
```
为了快速测试一个 Golang 函数,可以使用go playground。确保日志总是能轻易访问,可以把log写在一个文件:
```go
package main
import (
"log"
"os"
)
func main() {
// 按照所需读写权限创建文件
f, err := os.OpenFile("filename", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Fatal(err)
}
//延迟关闭
defer f.Close()
//设置日志输出到 f
log.SetOutput(f)
//测试用例
log.Println("check to make sure it works")
}
```
日志也可以帮将活动流拼接在一起,查找需要修复的错误上下文,或者调查在系统中单个请求如何影响其它应用层和 API。
为了获得更好的日志效果,首先需要在项目中使用尽可能多的上下文丰富 Golang 日志,并标准化使用的格式。这就是 Golang 原生库能达到的极限。
于更精细的日志级别、日志文件分割以及日志分发等方面并没有提供支持。所以催生了很多第三方的日志库,在 Golang 中,流行的日志框架包括logrus、zap、zerolog、seelog等。
**logrus日志库**
logrus是目前Github上使用数量最多的日志库。logrus功能强大,性能高效,而且具有高度灵活性,提供了自定义插件的功能。很多开源项目,如docker,prometheus等,都是用了logrus来记录其日志。
logrus具有以下特性:
- 完全兼容golang标准库日志模块:logrus拥有六种日志级别:debug、info、warn、error、fatal和panic,这是golang标准库日志模块的API的超集。如果在项目使用标准库日志模块,完全可以以最低的代价迁移到logrus上。
- 可扩展的Hook机制:允许使用者通过hook的方式将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch或者mq等,或者通过hook定义日志内容和格式等。
- 可选的日志输出格式:logrus内置了两种日志格式,`JSONFormatter`和`TextFormatter`,如果两个格式不满足需求,可以实现接口`Formatter`,来定义指定的日志格式。
- Field机制:logrus鼓励通过Field机制进行精细化的、结构化的日志记录,而不是通过冗长的消息来记录日志。
- logrus是一个可插拔的、结构化的日志框架。
### 2 日志统一格式
#### 2.1 JSON 格式
在一个项目或者多个微服务中结构化 Golang 日志可能是最困难的事情,结构化日志能使机器可读,一旦完成就很轻松了。
灵活性和层级是 JSON 格式的核心,因此信息能够轻易被人和机器解析以及处理。
logrus与golang标准库日志模块完全兼容,因此使用 log“github.com/sirupsen/logrus” 替换所有日志导入。
logrus可以通过简单的配置,来定义输出、格式或者日志级别等。
下面是一个使用 Logrus 中的 JSON 格式记录日志的例子:
```go
package main
import (
log "github.com/sirupsen/logrus"
)
func main() {
// 使用 JSONFormatter
log.SetFormatter(&log.JSONFormatter{})
// 使用 logrus 记录事件
log.WithFields(log.Fields{"string": "foo", "int": 1, "float": 1.1 }).Info("My first ssl event from golang")
}
```
会输出结果:
```
{
"date":"2016-05-09T10:56:00+02:00",
"float":1.1,
"int":1,
"level":"info",
"message":"My first ssl event from golang",
"String":"foo"
}
```
#### 2.2 标准化日志
同一个错误出现在代码的不同部分,却以不同形式被记录下来,在操作时绝对是一件麻烦的事。
下面是一个由于一个变量错误导致无法确定 web 页面加载状态的例子。
开发者1日志格式是:
```
message: 'unknown error: cannot determine loading status from unknown error: missing or invalid arg value client'
```
开发者2日志格式是:
```
unknown error: cannot determine loading status - invalid client
```
强制日志标准化的解决办法是在代码和日志库之间创建一个接口。标准化接口会包括所有添加到日志中的可能行为的预定义日志消息。这么做可以防止出现不符合想要的标准格式的自定义日志信息。这么做也便于日志调查。
由于日志格式都被统一处理,使其保持更新也变得更加简单。如果出现了一种新的错误类型,它只需要被添加到一个接口,这样每个组员都会使用完全相同的信息。
最常使用的简单例子就是在 Golang 日志信息前面添加日志器名称和 id。发送 “事件” 到标准化接口,会继续将其转化为 Golang 日志消息。
```go
// 主要部分,定义所有消息。
// Event 结构体很简单。为了当所有信息都被记录时能检索,
// 维护了一个 Id
var (
invalidArgMessage = Event{1, "Invalid arg: %s"}
invalidArgValueMessage = Event{2, "Invalid arg value: %s = %v"}
missingArgMessage = Event{3, "Missing arg: %s"}
)
// 应用程序中可以使用的所有日志事件
func (l *Logger)InvalidArg(name string) {
l.entry.Errorf(invalidArgMessage.toString(), name)
}
func (l *Logger)InvalidArgValue(name string, value interface{}) {
l.entry.WithField("arg." + name, value).Errorf(invalidArgValueMessage.toString(), name, value)
}
func (l *Logger)MissingArg(name string) {
l.entry.Errorf(missingArgMessage.toString(), name)
}
```
因此如果使用前面例子中无效的参数值,就会得到相似的日志信息:
```json
time="2017-02-24T23:12:31+01:00" level=error msg="LoadPageLogger00003 - Missing arg: client - cannot determine loading status" arg.client=<nil logger.name=LoadPageLogger
```
JSON 格式如下:
```json
{"arg.client":null,"level":"error","logger.name":"LoadPageLogger","msg":"LoadPageLogger00003 - Missing arg: client - cannot determine loading status", "time":"2017-02-24T23:14:28+01:00"}
```
#### 2.3 Hook
logrus提供了一个功能就是其可扩展的HOOK机制,通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能。
**Hook接口**
logrus的hook接口定义如下,其原理是每此写入日志时拦截,修改logrus.Entry。
```go
// logrus在记录Levels()返回的日志级别的消息时会触发HOOK,
// 按照Fire方法定义的内容修改logrus.Entry。
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
```
自定义hook如下,`DefaultFieldHook`定义会在所有级别的日志消息中加入默认字段`appName="myAppName"`。
```go
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](https://github.com/sirupsen/logrus/tree/master/hooks/syslog)。此外Github也有很多第三方的hook可供使用。
**将日志发送到其他位置**
将日志发送到日志中心也是logrus所提倡的,虽然没有提供官方支持,但是目前Github上有很多第三方hook可供使用:
- [logrus_amqp](https://github.com/vladoatanasov/logrus_amqp):Logrus hook for Activemq。
- [logrus-logstash-hook](https://github.com/bshuster-repo/logrus-logstash-hook):Logstash hook for logrus。
- [mgorus](https://github.com/weekface/mgorus):Mongodb Hooks for Logrus。
- [logrus_influxdb](https://github.com/abramovic/logrus_influxdb):InfluxDB Hook for Logrus。
- [logrus-redis-hook](https://github.com/rogierlommers/logrus-redis-hook):Hook for Logrus which enables logging to RELK stack (Redis, Elasticsearch, Logstash and Kibana)。
### 3 Go日志上下文
Golang 日志已经按照特定结构和标准格式记录,时间会决定需要添加哪些上下文以及相关信息。
为了能从日志中抽取信息,例如追踪一个用户活动或者工作流,上下文和元数据的顺序非常重要。
例如在 logrus 库中可以按照下面这样使用 JSON 格式添加hostname、appname和session 参数:
```go
// 对于元数据,通常做法是通过复用来重用日志语句中的字段。
contextualizedLog := log.WithFields(log.Fields{
"hostname": "staging-1",
"appname": "foo-app",
"session": "1ce3f6v"
})
contextualizedLog.Info("Simple event with global metadata")
```
元数据可以视为 javascript 片段。为了更好地说明其重要性,可以看看 Golang 微服务中元数据的使用案例,就可以清楚地看到是怎么在应用程序中跟踪的。一旦一个错误发生了,还要知道是哪个实例以及什么模式导致了错误。
假设有两个按顺序调用的微服务。上下文信息保存在头部(header)中传输:
```go
func helloMicroService1(w http.ResponseWriter, r *http.Request) {
client := &http.Client{}
// 该服务负责接收所有到来的用户请求
// 我们会检查是否是一个新的会话还是已有会话的另一次调用
session := r.Header.Get("x-session")
if ( session == "") {
session = generateSessionId()
// 为新会话记录日志
}
// 每个请求的 Track Id 都是唯一的,因此会为每个会话生成一个
track := generateTrackId()
// 调用你的第二个微服务,添加 session/track
reqService2, _ := http.NewRequest("GET", "http://localhost:8082/", nil)
reqService2.Header.Add("x-session", session)
reqService2.Header.Add("x-track", track)
resService2, _ := client.Do(reqService2)
```
当调用第二个服务时:
```go
func helloMicroService2(w http.ResponseWriter, r *http.Request) {
// 类似之前的微服务,我们检查会话并生成新的 track
session := r.Header.Get("x-session")
track := generateTrackId()
// 这一次,我们检查请求中是否已经设置了一个 track id,
// 如果是,它变为父 track
parent := r.Header.Get("x-track")
if (session == "") {
w.Header().Set("x-parent", parent)
}
// 为响应添加 meta 信息
w.Header().Set("x-session", session)
w.Header().Set("x-track", track)
if (parent == "") {
w.Header().Set("x-parent", track)
}
// 填充响应
w.WriteHeader(http.StatusOK)
io.WriteString(w, fmt.Sprintf(aResponseMessage, 2, session, track, parent))
}
```
现在第二个微服务中已经有和初始查询相关的上下文和信息,一个 JSON 格式的日志消息看起来类似如下。
在第一个微服务:
```json
{"appname":"go-logging","level":"debug","msg":"hello from ms 1","session":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","track":"UzWHRihF"}
```
在第二个微服务:
```json
{"appname":"go-logging","level":"debug","msg":"hello from ms 2","parent":"UzWHRihF","session":"eUBrVfdw","time":"2017-03-02T15:29:26+01:00","track":"DPRHBMuE"}
```
如果在第二个微服务中出现了错误,通过查看 Golang 日志中保存的上下文信息,就可以确定是怎样被调用的以及什么模式导致了这个错误。
### 4 Go 日志对性能的影响
#### 4.1 协程安全
在每个 goroutine 中创建一个新的日志器看起来都是可行的,但最好别这么做。
Goroutine 是一个轻量级线程管理器,用于完成一个 “简单的” 任务。因此不应该负责日志,而且可能导致并发问题,因为在每个 goroutine 中使用 log.New() 会重复接口,所有日志器会并发尝试访问同一个 io.Writer。
为了限制对性能的影响以及避免并发调用 io.Writer,日志库通常使用一个特定的 goroutine 用于日志输出。
logrus的api都是协程安全的,其内部通过互斥锁来保护并发写操作。
#### 4.2 异步库
尽管有很多可用的 Golang 日志库,要注意大部分都是同步的(事实上是伪异步)。原因很可能是到现在为止都没有一个会由于日志严重影响性能。
实验中展示使用多个协程创建成千上万日志,即便是在最坏情况下,异步 Golang 日志也会有 40% 的性能提升。因此日志是有开销的,也会对应用程序性能产生影响。如果不需要处理大量的日志,使用伪异步 Golang 日志库可能就足够了。
#### 4.3 使用严重等级管理日志
日志库允许启用或停用特定的日志器。例如在生产环境中可能不需要一些特定等级的日志。
下面是一个如何停用日志器的例子,其中日志器被定义为布尔值:
```go
type Log bool
func (l Log) Println(args ...interface{}) {
fmt.Println(args...)
}
var debug Log = false
if debug {
debug.Println("DEBUGGING")
}
```
然后可以在配置文件中定义这些布尔参数来启用或者停用日志器。
有疑问加站长微信联系(非本文作者)