文件名与行号
logrus的一个很致命的问题就是没有提供文件名和行号,这在大型项目中通过日志定位问题时有诸多不便。Github上的logrus的issue#63:Log filename and line number创建于2014年,四年过去了仍是open状态~~~
网上给出的解决方案分位两类,一就是自己实现一个hook;二就是通过装饰器包装logrus.Entry。两种方案网上都有很多代码,但是大多无法正常工作。但总体来说,解决问题的思路都是对的:通过标准库的runtime模块获取运行时信息,并从中提取文件名,行号和调用函数名。

标准库runtime模块的Caller(skip int)函数可以返回当前goroutine调用栈中的文件名,行号,函数信息等,参数skip表示表示返回的栈帧的层次,0表示runtime.Caller的调用着。返回值包括响应栈帧层次的pc(程序计数器),文件名和行号信息。为了提高效率,我们先通过跟踪调用栈发现,从runtime.Caller()的调用者开始,到记录日志的生成代码之间,大概有8到11层左右,所有我们在hook中循环第8到11层调用栈应该可以找到日志记录的生产代码。

此外,runtime.FuncForPC(pc uintptr) *Func可以返回指定pc的函数信息。
所有我们要实现的hook也是基于以上原理,使用runtime.Caller()依次循环调用栈的第7~11层,过滤掉sirupsen包内容,那么第一个非siupsenr包就认为是我们的生产代码了,并返回pc以便通过runtime.FuncForPC()获取函数名称。然后将文件名、行号和函数名组装为source字段塞到logrus.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()"

日志本地文件分割
logrus本身不带日志本地文件分割功能,但是我们可以通过file-rotatelogs进行日志本地文件分割。 每次当我们写入日志的时候,logrus都会调用file-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

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

将日志发送到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

其中Host记录产生日志主机信息,在创建hook是指定。其他数据需要从logrus.Entry中取得。测试过程我们选择按照此原理实现的第三方HOOK:elogrus。其使用如下:

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

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

  • logrus_amqp:Logrus hook for Activemq。
  • logrus-logstash-hook:Logstash hook for logrus。
  • mgorus:Mongodb Hooks for Logrus。
  • logrus_influxdb:InfluxDB Hook for Logrus。
  • logrus-redis-hook:Hook for Logrus which enables logging to RELK stack (Redis, Elasticsearch, Logstash and Kibana)。
    等等,上述第三方hook我这里没有具体验证,大家可以根据需要自行尝试。

其他注意事项

  • Fatal处理
    和很多日志框架一样,logrus的Fatal系列函数会执行os.Exit(1)。但是logrus提供可以注册一个或多个fatal handler函数的接口logrus.RegisterExitHandler(handler func() {} ),让logrus在执行os.Exit(1)之前进行相应的处理。fatal handler可以在系统异常时调用一些资源释放api等,让应用正确的关闭。
  • 线程安全
    默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁工作于调用hooks或者写日志的时候,如果不需要锁,可以调用logger.SetNoLock()来关闭之。可以关闭logrus互斥锁的情形包括:
  1. 没有设置hook,或者所有的hook都是线程安全的实现。
  2. 写日志到logger.Out已经是线程安全的了,如logger.Out已经被锁保护,或者写文件时,文件是以O_APPEND方式打开的,并且每次写操作都小于4k。
文件名与行号logrus的一个很致命的问题就是没有提供文件名和行号,这在大型项目中通过日志定位问题时有诸多不便。Github上的logrus的issue#63:Log filename and line number创建于2014年,四年过去了仍是open状态~~~网上给出的解决方案分位两类,一就是自己实现一个hook;二就是通过装饰器包装logrus.Entry。两种方案网上都有很多代码,但...
日志是程序中必不可少的一个环节,由于Go语言内置的日志库功能比较简洁,我们在实际开发中通常会选择使用第三方的日志库来进行开发。本文介绍了logrus这个日志库的基本使用logrus介绍 Logrus是Go(golang)的结构化logger,与标准库logger完全API兼容。 它有以下特点: 完全兼容标准日志库,拥有七种日志级别:Trace,Debug,Info,Warning,Error,FatalandPanic。 可扩展的Hook机制,允许使用者通过Hook的方式将日志分发...
下面示例代码中的一些import我没做校验,我只是从我的一个测试代码里抠出来的,出现一些错误自己简单处理下就可以用。 logrus简介 logrus是一个可插拔的、结构化的日志框架。 logrus拥有六种日志级别:debug、info、warn、error、fatal和panic 可扩展的Hook机制: 允许使用者通过hook的方式将日志分发到任意地方,如本地文件系统、标准输出、logs...
logrus的初始化启动,详细使用 今天也是初次接触这个组件,来进行日志存储,自己也是俺就半天,想要所有的错误日志放到一个错误日志中,所首先创建一个日志文件。 自己也是没有找到相关的官方文档,就在gitee上直接查看的文档, package log import ( "fmt" "github.com/sirupsen/logrus" "time" var log = logrus.New() func Init() { file,err := os.OpenFile
logrus是go的一个日志框架,它最让人激动的应该是hook机制,可以在初始化时为logrus添加hook,logrus可以实现各种扩展功能,可以将日志输出到elasticsearch和activemq等中间件去,甚至可以输出到你的email和叮叮中去,不要问为为什么可以发现可以输入到叮叮中去,都是泪,手动笑哭! 言归正传,这里就简单的通过hook机制将文件输出到本地磁盘。 go get github.com/sirupsen/logrus logrus和go lib里面一样有6个等级,可以直接调用 logrus.Debug("Useful debugging inform
```go logrus.SetLevel(logrus.DebugLevel) logrus.SetFormatter(&logrus.TextFormatter{}) logrus.SetOutput(os.Stdout) 最后,在代码中使用logrus记录日志: ```go logrus.Info("This is an info message") logrus.Warn("This is a warning message") logrus.Error("This is an error message") 以上就是一个基本的logrus使用示例。当然,logrus还提供了其他丰富的功能,如日志文件的输出、钩子函数等,可以根据自己的需求进行选择和使用