2023年11月29日发(作者:)
golang⽇志框架之logrus的使⽤
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上也有约1.1k。
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是⼀个可插拔的、结构化的⽇志框架。
logrus的使⽤
第⼀个⽰例
最简单的使⽤logrus的⽰例如下:
package main
import (
log "/sirupsen/logrus"
)
func main() {
elds({
"animal": "walrus",
}).Info("A walrus appears")
}
上⾯代码执⾏后,标准输出上输出如下:
time="2018-08-11T15:42:22+08:00" level=info msg="A walrus appears" animal=walrus
logrus与golang标准库⽇志模块完全兼容,因此您可以使⽤替换所有⽇志导⼊。
log“/sirupsen/logrus”
logrus可以通过简单的配置,来定义输出、格式或者⽇志级别等。
package main
import (
"os"
log "/sirupsen/logrus"
)
func init() {
// 设置⽇志格式为json格式
matter(&rmatter{})
// 设置将⽇志输出到标准输出(默认的输出为stderr,标准错误)
// ⽇志消息输出可以是任意的类型
put()
// 设置⽇志级别为warn以上
el(vel)
}
func main() {
elds({
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
elds({
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
elds({
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
}
Logger
logger是⼀种相对⾼级的⽤法, 对于⼀个⼤型项⽬, 往往需要⼀个全局的logrus实例,即对象来记录项⽬所有的⽇志。如:
logger
package main
import (
"/sirupsen/logrus"
"os"
)
// logrus提供了New()函数来创建⼀个logrus的实例。
// 项⽬中,可以创建任意数量的logrus实例。
var log = ()
func main() {
// 为当前logrus实例设置消息的输出,同样地,
// 可以设置logrus实例的输出到任意
=
// 为当前logrus实例设置消息输出格式为json格式。
// 同样地,也可以单独为某个logrus实例设置⽇志级别和hook,这⾥不详细叙述。
ter = &rmatter{}
elds({
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
}
Fields
前⼀章提到过,logrus不推荐使⽤冗长的消息来记录运⾏信息,它推荐使⽤来进⾏精细化的、结构化的信息记录。
Fields
例如下⾯的记录⽇志的⽅式:
("Failed to send event %s to topic %s with key %d", event, topic, key)
````
在logrus中不太提倡,logrus⿎励使⽤以下⽅式替代之:
```go
elds({
"event": event,
"topic": topic,
"key": key,
}).Fatal("Failed to send event")
前⾯的 API可以规范使⽤者按照其提倡的⽅式记录⽇志。但是依然是可选的,因为某些场景下,使⽤者确实只需要记录仪⼀
WithFieldsWithFields
条简单的消息。
通常,在⼀个应⽤中、或者应⽤的⼀部分中,都有⼀些固定的。⽐如在处理⽤户http请求时,上下⽂中,所有的⽇志都会
Field
有和。为了避免每次记录⽇志都要使⽤,我们可以创建⼀
request_iduser_ipelds({"request_id": request_id, "user_ip": user_ip})
个实例,为这个实例设置默认,在上下⽂中使⽤这个实例记录⽇志即可。
Fields
requestLogger := elds({"request_id": request_id, "user_ip": user_ip})
("something happened on that request") # will log request_id and user_ip
("something not great happened")
Hook
logrus最令⼈⼼动的功能就是其可扩展的HOOK机制了,通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能。
Hook接⼝
logrus的hook接⼝定义如下,其原理是每此写⼊⽇志时拦截,修改。
// logrus在记录Levels()返回的⽇志级别的消息时会触发HOOK,
// 按照Fire⽅法定义的内容修改。
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
⼀个简单⾃定义hook如下,定义会在所有级别的⽇志消息中加⼊默认字段。
DefaultFieldHookappName="myAppName"
type DefaultFieldHook struct {
}
func (hook *DefaultFieldHook) Fire(entry *) error {
["appName"] = "MyAppName"
return nil
}
func (hook *DefaultFieldHook) Levels() [] {
return els
}
hook的使⽤也很简单,在初始化前调⽤添加相应的即可。
k(hook)hook
logrus官⽅仅仅内置了syslog的。
此外,但Github也有很多第三⽅的hook可供使⽤,⽂末将提供⼀些第三⽅HOOK的连接。
问题与解决⽅案
尽管logrus有诸多优点,但是为了灵活性和可扩展性,官⽅也削减了很多实⽤的功能,例如:
没有提供⾏号和⽂件名的⽀持
输出到本地⽂件系统没有提供⽇志分割功能
官⽅没有提供输出到ELK等⽇志处理中⼼的功能
但是这些功能都可以通过⾃定义hook来实现。
记录⽂件名和⾏号
logrus的⼀个很致命的问题就是没有提供⽂件名和⾏号,这在⼤型项⽬中通过⽇志定位问题时有诸多不便。Github上的logrus的issue#63:创
建于2014年,四年过去了仍是open状态~~~
⽹上给出的解决⽅案分位两类,⼀就是⾃⼰实现⼀个hook;⼆就是通过装饰器包装。两种⽅案⽹上都有很多代码,但是⼤多⽆法正
常⼯作。但总体来说,解决问题的思路都是对的:通过标准库的模块获取运⾏时信息,并从中提取⽂件名,⾏号和调⽤函数名。
runtime
标准库模块的函数可以返回当前goroutine调⽤栈中的⽂件名,⾏号,函数信息等,参数skip表⽰表⽰返回的栈帧的层次,0
runtimeCaller(skip int)
表⽰的调⽤着。返回值包括响应栈帧层次的pc(程序计数器),⽂件名和⾏号信息。为了提⾼效率,我们先通过跟踪调⽤栈发现,
从的调⽤者开始,到记录⽇志的⽣成代码之间,⼤概有8到11层左右,所有我们在hook中循环第8到11层调⽤栈应该可以找到⽇志
()
记录的⽣产代码。
此外,可以返回指定的函数信息。
rPC(pc uintptr) *Funcpc
所有我们要实现的hook也是基于以上原理,使⽤依次循环调⽤栈的第7~11层,过滤掉包内容,那么第⼀个⾮包就认
()sirupsensiupsenr
为是我们的⽣产代码了,并返回以便通过获取函数名称。然后将⽂件名、⾏号和函数名组装为字段塞到中
pcrPC()source
即可。
time="2018-08-11T19:10:15+08:00" level=warning msg="postgres_exporter is ready for scraping on 0.0.0."
source="postgres_exporter/:60:main()"
time="2018-08-11T19:10:17+08:00" level=error msg="msb info not found"
source="postgres/postgres_: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/:71:Scrape()"
⽇志本地⽂件分割
logrus本⾝不带⽇志本地⽂件分割功能,但是我们可以通过进⾏⽇志本地⽂件分割。 每次当我们写⼊⽇志的时候,logrus都会调
file-rotatelogs
⽤来判断⽇志是否要进⾏切分。关于本地⽇志⽂件分割的例⼦⽹上很多,这⾥不再详细介绍,奉上代码:
file-rotatelogs
import (
"/lestrrat-go/file-rotatelogs"
"/rifflock/lfshook"
log "/sirupsen/logrus"
"time"
)
func newLfsHook(logLevel *string, maxRemainCnt uint) {
writer, err := (
logName+".%Y%m%d%H",
// WithLinkName为最新的⽇志建⽴软连接,以⽅便随着找到当前⽇志⽂件
nkName(logName),
// WithRotationTime设置⽇志分割的时间,这⾥设置为⼀⼩时分割⼀次
tationTime(),
// WithMaxAge和WithRotationCount⼆者只能设置⼀个,
// WithMaxAge设置⽂件清理前的最长保存时间,
// WithRotationCount设置⽂件清理前最多保存的个数。
//xAge(*24),
tationCount(maxRemainCnt),
)
if err != nil {
("config local file system for logger error: %v", err)
}
level, ok := logLevels[*logLevel]
if ok {
el(level)
} else {
el(vel)
}
lfsHook := k(Map{
evel: writer,
vel: writer,
vel: writer,
evel: writer,
evel: writer,
evel: writer,
}, &rmatter{DisableColors: true})
return lfsHook
}
使⽤上述本地⽇志⽂件切割的效果如下:
将⽇志发送到elasticsearch
Level string
}
其中记录产⽣⽇志主机信息,在创建hook是指定。其他数据需要从中取得。测试过程我们选择按照此原理实现的第三⽅
Host
HOOK:。其使⽤如下:
import (
"/olivere/elastic"
"/sohlich/elogrus"
)
func initLog() {
},
{
"_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"
}
}
]
}
}
将⽇志发送到其他位置
将⽇志发送到⽇志中⼼也是logrus所提倡的,虽然没有提供官⽅⽀持,但是⽬前Github上有很多第三⽅hook可供使⽤:
:Logrus hook for Activemq。
:Logstash hook for logrus。
:Mongodb Hooks for Logrus。
:InfluxDB Hook for Logrus。
:Hook for Logrus which enables logging to RELK stack (Redis, Elasticsearch, Logstash and Kibana)。
等等,上述第三⽅hook我这⾥没有具体验证,⼤家可以根据需要⾃⾏尝试。
其他注意事项
Fatal处理
和很多⽇志框架⼀样,logrus的系列函数会执⾏。但是logrus提供可以注册⼀个或多个函数的接
Fatal(1)fatal handler
⼝,让logrus在执⾏之前进⾏相应的处理。可以在系统异常时调⽤⼀些资源释放api等,
erExitHandler(handler func() {} )(1)fatal handler
让应⽤正确的关闭。
线程安全
默认情况下,logrus的api都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁⼯作于调⽤hooks或者写⽇志的时候,如果不需要锁,可
以调⽤来关闭之。可以关闭logrus互斥锁的情形包括:
ock()
没有设置hook,或者所有的hook都是线程安全的实现。
写⽇志到已经是线程安全的了,如已经被锁保护,或者写⽂件时,⽂件是以⽅式打开的,并且每次写操作都
O_APPEND
⼩于4k。
以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。


发布评论