日志处理

itjunma · · 235 次点击 · 开始浏览    置顶
### 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") } ``` 然后可以在配置文件中定义这些布尔参数来启用或者停用日志器。

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:1006366459

235 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传