一、golang日志库

       1.1 golang日志库简介

         golang标准库的日志框架非常简单,仅仅提供了print,panic和fatal三个函数。对于更精细的日志级别、日志文件分割,以及日志分发等方面,并没有提供支持。所以,催生了很多第三方的日志库。但是,在golang的世界里,没有一个日志库像slf4j那样在Java中具有绝对统治地位。在golang的世界,流行的日志框架包括logrus、zap、zerolog、seelog等。 
        logrus是目前Github上star数量最多的日志库,目前(2018.08,下同)star数量为8119,fork数为1031。logrus功能强大,性能高效,而且具有高度灵活性,提供了自定义插件的功能。很多开源项目,如docker,prometheus等,都是用了logrus来记录其日志。 
        zap是Uber推出的一个快速、结构化的分级日志库。具有强大的ad-hoc分析功能,并且具有灵活的仪表盘。zap目前在GitHub上的star数量约为4.3k。 
        seelog提供了灵活的异步调度、格式化和过滤功能。目前在GitHub上的star数量也有约1.1k。 

        1.2 golang logrus的GitHub地址

           pkg/errors的GitHub地址    https://github.com/pkg/errors   

二、logrus特性

logrus具有以下特性:

  1. 完全兼容golang标准库日志模块。logrus拥有六种日志级别:debug、info、warn、error、fatal和panic,这是golang标准库日志模块的API的超集。如果你的项目使用标准库日志模块,完全可以用最低的代价迁移到logrus上。
  2. 可扩展的Hook机制。允许使用者通过hook方式,将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch或者mq等,或者通过hook定义日志内容和格式等。
  3. 可选的日志输出格式。logrus内置了两种日志格式,JSONFormatter和TextFormatter。如果这两个格式不满足需求,可以自己动手实现接口Formatter,来定义自己的日志格式。
  4. Field机制。logrus鼓励通过Field机制进行精细化、结构化的日志记录,而不是通过冗长的消息来记录日志。
  5. logrus是一个可插拔的、结构化的日志框架。
三、logrus的使用

3.1 第一个示例

最简单的使用logrus的示例如下:

package main

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

func main() {
  log.WithFields(log.Fields{
    "animal": "walrus",
  }).Info("A walrus appears")
}

上面代码执行后,标准输出上输出如下:

time="2018-08-11T15:42:22+08:00" level=info msg="A walrus appears" animal=walrus
log “github.com/sirupsen/logrus”

3.2 第二个示例

logrus可以通过简单的配置,来定义输出、格式或者日志级别等。示例如下:

package main

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

func initLog() {
	// 设置日志格式为json格式
	log.SetFormatter(&log.JSONFormatter{})

	// 设置将日志输出到标准输出(默认的输出为stderr,标准错误)
	// 日志消息输出可以是任意的io.writer类型
	log.SetOutput(os.Stdout)

	// 设置日志级别为warn以上
	log.SetLevel(log.WarnLevel)
}

func main() {
	initLog()

	log.WithFields(log.Fields{
		"animal": "walrus",
		"size":   10,
		"country": "china",
	}).Info("A group of walrus emerges from the ocean")

	log.WithFields(log.Fields{
		"omg":    true,
		"number": 122,
		"country": "china",
	}).Warn("The group's number increased tremendously!")

	log.WithFields(log.Fields{
		"omg":    true,
		"number": 100,
		"country": "america",
	}).Fatal("The ice breaks!")
}

上面代码执行后,标准输出上输出如下:

{"country":"china","level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2018-12-12T18:22:20+08:00"}
{"country":"america","level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2018-12-12T18:22:20+08:00"}
exit status 1

3.3 Logger

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")
}
执行结果如下所示:
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the ocean","size":10,"time":"2018-12-12T18:33:38+08:00"}

3.4 Fields

Fields
log.Fatalf("Failed to send event %s to topic %s with key %d", event, topic, key)
````

在logrus中不太提倡,logrus鼓励使用以下方式替代之:

<div class="se-preview-section-delimiter"></div>

```go
log.WithFields(log.Fields{
  "event": event,
  "topic": topic,
  "key": key,
}).Fatal("Failed to send event")
WithFieldsWithFields
Fieldrequest_iduser_iplog.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})logrus.EntryFieldslogrus.Entry
requestLogger := log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip})
requestLogger.Info("something happened on that request") # will log request_id and user_ip
requestLogger.Warn("something not great happened")
四、Hook

logrus最令人心动的功能,就是其可扩展的HOOK机制了。通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能。

4.1 Hook接口

logrus的hook原理是:在每次写入日志时拦截,修改logrus.Entry。logrus的hook接口定义如下。

// logrus在记录Levels()返回的日志级别的消息时,会触发HOOK。
// 然后,按照Fire方法定义的内容,修改logrus.Entry。
type Hook interface {
    Levels() []Level
    Fire(*Entry) error
}
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
}
log.AddHook(hook)hook

logrus官方仅仅内置了syslog的hook。 但Github上有很多第三方的hook可供使用,文末将提供一些第三方HOOK的链接。

五、问题与解决方案

尽管logrus有诸多优点,但是为了灵活性和可扩展性,官方也削减了很多实用的功能,例如:

  • 没有提供行号和文件名的支持
  • 输出到本地文件系统时,没有提供日志分割功能
  • 官方没有提供输出到ELK等日志处理中心的功能

但是,这些功能都可以通过自定义hook来实现。

5.1 记录文件名和行号

logrus.Entryruntime
runtimeCaller(skip int)runtime.Callerruntime.Caller()runtime.FuncForPC(pc uintptr) *Funcpcruntime.Caller()sirupsensiupsenrpcruntime.FuncForPC()sourcelogrus.Entry
time="2018-08-11T19:10:15+08:00" level=warning msg="postgres_exporter is ready for scraping on 0.0.0.0:9295..." source="postgres_exporter/main.go:60:main()"
time="2018-08-11T19:10:17+08:00" level=error msg="!!!msb info not found" source="postgres/postgres_query.go:63:QueryPostgresInfo()"
time="2018-08-11T19:10:17+08:00" level=error msg="get postgres instances info failed, scrape metrics failed, error:msb env not found" source="collector/exporter.go:71:Scrape()"

5.2 日志本地文件分割

file-rotatelogsfile-rotatelogs
import (
    "github.com/lestrrat-go/file-rotatelogs"
    "github.com/rifflock/lfshook"
    log "github.com/sirupsen/logrus"
    "time"
)

func newLfsHook(logLevel *string, maxRemainCnt uint) log.Hook {
    writer, err := rotatelogs.New(
        logName+".%Y%m%d%H",
        // WithLinkName 为最新的日志建立软连接,以方便随时找到当前日志文件
        rotatelogs.WithLinkName(logName),

        // WithRotationTime 设置日志分割的时间,这里设置为一小时分割一次
        rotatelogs.WithRotationTime(time.Hour),

        // WithMaxAge和WithRotationCount 二者只能设置一个,
        // WithMaxAge 设置文件清理前的最长保存时间,
        // WithRotationCount 设置文件清理前最多保存的个数。
        //rotatelogs.WithMaxAge(time.Hour*24),
        rotatelogs.WithRotationCount(maxRemainCnt),
    )

    if err != nil {
        log.Errorf("config local file system for logger error: %v", err)
    }

    level, ok := logLevels[*logLevel]

    if ok {
        log.SetLevel(level)
    } else {
        log.SetLevel(log.WarnLevel)
    }

    lfsHook := lfshook.NewHook(lfshook.WriterMap{
        log.DebugLevel: writer,
        log.InfoLevel:  writer,
        log.WarnLevel:  writer,
        log.ErrorLevel: writer,
        log.FatalLevel: writer,
        log.PanicLevel: writer,
    }, &log.TextFormatter{DisableColors: true})

    return lfsHook
}

使用上述本地日志文件切割的效果如下: 
这里写图片描述

5.3 将日志发送到elasticsearch

将日志发送到elasticsearch,是很多日志监控系统的选择。将logrus日志发送到elasticsearch的原理是:在hook的每次fire调用时,使用golang的es客户端将日志信息写到elasticsearch。elasticsearch官方没有提供golang客户端,但是有很多第三方的go语言客户端可供使用,我们选择elastic。elastic提供了丰富的文档,以及Java中的流式接口,使用起来非常方便。

client, err := elastic.NewClient(elastic.SetURL("http://localhost:9200"))
    if err != nil {
        log.Panic(err)
    }

// Index a tweet (using JSON serialization)
tweet1 := Tweet{User: "olivere", Message: "Take Five", Retweets: 0}
put1, err := client.Index().
    Index("twitter").
    Type("tweet").
    Id("1").
    BodyJson(tweet1).
    Do(context.Background())

考虑到logrus的Fields机制,可以实现如下数据格式:

msg := struct {
    Host      string
    Timestamp string `json:"@timestamp"`
    Message   string
    Data      logrus.Fields
    Level     string
}
Hostlogrus.Entry
import (
    "github.com/olivere/elastic"
    "gopkg.in/sohlich/elogrus"
)

func initLog() {
    client, err := elastic.NewClient(elastic.SetURL("http://localhost:9200"))
    if err != nil {
        log.Panic(err)
    }
    hook, err := elogrus.NewElasticHook(client, "localhost", log.DebugLevel, "mylog")
    if err != nil {
        log.Panic(err)
    }
    log.AddHook(hook)
}

从Elasticsearch查询得到日志存储,效果如下:

GET http://localhost:9200/mylog/_search

HTTP/1.1 200 OK
content-type: application/json; charset=UTF-8
transfer-encoding: chunked

{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 2474,
    "max_score": 1.0,
    "hits": [
      {
        "_index": "mylog",
        "_type": "log",
        "_id": "AWUw13jWnMZReb-jHQup",
        "_score": 1.0,
        "_source": {
          "Host": "localhost",
          "@timestamp": "2018-08-13T01:12:32.212818666Z",
          "Message": "!!!msb info not found",
          "Data": {},
          "Level": "ERROR"
        }
      },
      {
        "_index": "mylog",
        "_type": "log",
        "_id": "AWUw13jgnMZReb-jHQuq",
        "_score": 1.0,
        "_source": {
          "Host": "localhost",
          "@timestamp": "2018-08-13T01:12:32.223103348Z",
          "Message": "get postgres instances info failed, scrape metrics failed, error:msb env not found",
          "Data": {
            "source": "collector/exporter.go:71:Scrape()"
          },
          "Level": "ERROR"
        }
      },
      //...
      {
        "_index": "mylog",
        "_type": "log",
        "_id": "AWUw2f1enMZReb-jHQu_",
        "_score": 1.0,
        "_source": {
          "Host": "localhost",
          "@timestamp": "2018-08-13T01:15:17.212546892Z",
          "Message": "!!!msb info not found",
          "Data": {
            "source": "collector/exporter.go:71:Scrape()"
          },
          "Level": "ERROR"
        }
      },
      {
        "_index": "mylog",
        "_type": "log",
        "_id": "AWUw2NhmnMZReb-jHQu1",
        "_score": 1.0,
        "_source": {
          "Host": "localhost",
          "@timestamp": "2018-08-13T01:14:02.21276903Z",
          "Message": "!!!msb info not found",
          "Data": {},
          "Level": "ERROR"
        }
      }
    ]
  }
}

Response code: 200 (OK); Time: 16ms; Content length: 3039 bytes

5.4 将日志发送到其他位置

将日志发送到日志中心,也是logrus所提倡的。虽然没有提供官方支持,但是目前Github上有很多第三方hook可供使用:

等等。对于上述第三方hook,我这里没有具体验证,大家可以根据需要自行尝试。

5.5 其他注意事项

5.5.1 Fatal处理

Fatalos.Exit(1)fatal handler”logrus.RegisterExitHandler(handler func(){})os.Exit(1)fatal handler

5.5.2 线程安全

logger.SetNoLock()
  • 没有设置hook,或者所有的hook都是线程安全的实现。
  • 写日志到logger.Out已经是线程安全的了。例如,logger.Out已经被锁保护,或者写文件时,文件是以O_APPEND方式打开的,并且每次写操作都小于4k。

尊重别人的劳动成果,原文网址: