golang日志框架之logrus

}


hook的使用也很简单,在初始化前调用`log.AddHook(hook)`添加相应的`hook`即可。


logrus官方仅仅内置了syslog的[hook]( )。  
 此外,但Github也有很多第三方的hook可供使用,文末将提供一些第三方HOOK的连接。


### 记录文件名和行号


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层调用栈应该可以找到日志记录的生产代码。  
 ![这里写图片描述](https://img-blog.csdn.net/20180814170801498?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dzbHlrNjA2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)  
 此外,`runtime.FuncForPC(pc uintptr) *Func`可以返回指定`pc`的函数信息。  
 所有我们要实现的hook也是基于以上原理,使用`runtime.Caller()`依次循环调用栈的第7~11层,过滤掉`sirupsen`包内容,那么第一个非`siupsenr`包就认为是我们的生产代码了,并返回`pc`以便通过`runtime.FuncForPC()`获取函数名称。然后将文件名、行号和函数名组装为`source`字段塞到`logrus.Entry`中即可。



import (
“fmt”
log “github.com/sirupsen/logrus”
“runtime”
“strings”
)

// line number hook for log the call context,
type lineHook struct {
Field string
// skip为遍历调用栈开始的索引位置
Skip int
levels []log.Level
}

// Levels implement levels
func (hook lineHook) Levels() []log.Level {
return log.AllLevels
}

// Fire implement fire
func (hook lineHook) Fire(entry *log.Entry) error {
entry.Data[hook.Field] = findCaller(hook.Skip)
return nil
}

func findCaller(skip int) string {
file := “”
line := 0
var pc uintptr
// 遍历调用栈的最大索引为第11层.
for i := 0; i < 11; i++ {
file, line, pc = getCaller(skip + i)
// 过滤掉所有logrus包,即可得到生成代码信息
if !strings.HasPrefix(file, “logrus”) {
break
}
}

fullFnName := runtime.FuncForPC(pc)

fnName := ""
if fullFnName != nil {
	fnNameStr := fullFnName.Name()
    // 取得函数名
	parts := strings.Split(fnNameStr, ".")
	fnName = parts[len(parts)-1]
}

return fmt.Sprintf("%s:%d:%s()", file, line, fnName)

}

func getCaller(skip int) (string, int, uintptr) {
pc, file, line, ok := runtime.Caller(skip)
if !ok {
return “”, 0, pc
}
n := 0

// 获取包名
for i := len(file) - 1; i > 0; i-- {
	if file[i] == '/' {
		n++
		if n >= 2 {
			file = file[i+1:]
			break
		}
	}
}
return file, line, pc

}


效果如下:



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

}


使用上述本地日志文件切割的效果如下:  
 ![这里写图片描述](https://img-blog.csdn.net/20180814170847468?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dzbHlrNjA2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)


### 将日志发送到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互斥锁的情形包括:


![img](https://img-blog.csdnimg.cn/img_convert/e916866f2bf40f6ef359fc7e2892ddc5.png)
![img](https://img-blog.csdnimg.cn/img_convert/59a0c01bf5d1bc3eb4eb8d319bb5a395.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
。


#### 线程安全


默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁工作于调用hooks或者写日志的时候,如果不需要锁,可以调用`logger.SetNoLock()`来关闭之。可以关闭logrus互斥锁的情形包括:


[外链图片转存中...(img-ZeaO4OnL-1725670405773)]
[外链图片转存中...(img-6Ym5pY2q-1725670405774)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

加入社区》https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值