2023年11月29日发(作者:)

golang⽇志框架之logrus的使⽤

golang⽇志库

golang标准库的⽇志框架⾮常简单,仅仅提供了printpanicfatal三个函数对于更精细的⽇志级别、⽇志⽂件分割以及⽇志分发等⽅⾯并没

有提供⽀持。所以催⽣了很多第三⽅的⽇志库,但是在golang的世界⾥,没有⼀个⽇志库像slf4j那样在Java中具有绝对统治地位。golang中,

流⾏的⽇志框架包括logruszapzerologseelog等。

logrus是⽬前Githubstar数量最多的⽇志库,⽬前(2018.08,下同)star数量为8119fork数为1031logrus功能强⼤,性能⾼效,⽽且具有⾼

度灵活性,提供了⾃定义插件的功能。很多开源项⽬,如dockerprometheus等,都是⽤了logrus来记录其⽇志。

zapUber推出的⼀个快速、结构化的分级⽇志库。具有强⼤的ad-hoc分析功能,并且具有灵活的仪表盘。zap⽬前在GitHub上的star数量约

4.3k

seelog提供了灵活的异步调度、格式化和过滤功能。⽬前在GitHub上也有约1.1k

logrus特性

logrus具有以下特性:

完全兼容golang标准库⽇志模块:logrus拥有六种⽇志级别:debuginfowarnerrorfatalpanic,这是golang标准库⽇志模块的

API的超集。如果您的项⽬使⽤标准库⽇志模块,完全可以以最低的代价迁移到logrus上。

可扩展的Hook机制:允许使⽤者通过hook的⽅式将⽇志分发到任意地⽅,如本地⽂件系统、标准输出、logstashelasticsearch或者mq

等,或者通过hook定义⽇志内容和格式等。

可选的⽇志输出格式:logrus内置了两种⽇志格式,JSONFormatterTextFormatter,如果这两个格式不满⾜需求,可以⾃⼰动⼿实现

接⼝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

logrusgolang标准库⽇志模块完全兼容,因此您可以使⽤替换所有⽇志导⼊。

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添加hooklogrus可以实现各种扩展功能。

Hook接⼝

logrushook接⼝定义如下,其原理是每此写⼊⽇志时拦截,修改

// 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上的logrusissue#63:创

建于2014年,四年过去了仍是open状态~~~

⽹上给出的解决⽅案分位两类,⼀就是⾃⼰实现⼀个hook;⼆就是通过装饰器包装。两种⽅案⽹上都有很多代码,但是⼤多⽆法正

常⼯作。但总体来说,解决问题的思路都是对的:通过标准库的模块获取运⾏时信息,并从中提取⽂件名,⾏号和调⽤函数名。

runtime

标准库模块的函数可以返回当前goroutine调⽤栈中的⽂件名,⾏号,函数信息等,参数skip表⽰表⽰返回的栈帧的层次,0

runtimeCaller(skip int)

表⽰的调⽤着。返回值包括响应栈帧层次的pc(程序计数器),⽂件名和⾏号信息。为了提⾼效率,我们先通过跟踪调⽤栈发现,

的调⽤者开始,到记录⽇志的⽣成代码之间,⼤概有811层左右,所有我们在hook中循环第811层调⽤栈应该可以找到⽇志

()

记录的⽣产代码。

此外,可以返回指定的函数信息。

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(),

// WithMaxAgeWithRotationCount⼆者只能设置⼀个,

// 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

让应⽤正确的关闭。

线程安全

默认情况下,logrusapi都是线程安全的,其内部通过互斥锁来保护并发写。互斥锁⼯作于调⽤hooks或者写⽇志的时候,如果不需要锁,可

以调⽤来关闭之。可以关闭logrus互斥锁的情形包括:

ock()

没有设置hook,或者所有的hook都是线程安全的实现。

写⽇志到已经是线程安全的了,如已经被锁保护,或者写⽂件时,⽂件是以⽅式打开的,并且每次写操作都

O_APPEND

⼩于4k

以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。