在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
golang日志库golang标准库的日志框架非常简单,仅仅提供了print,panic和fatal三个函数对于更精细的日志级别、日志文件分割以及日志分发等方面并没有提供支持。 所以催生了很多第三方的日志库,但是在golang的世界里,没有一个日志库像slf4j那样在Java中具有绝对统治地位。golang中,流行的日志框架包括logrus、zap、zerolog、seelog等。 logrus特性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 logrus与golang标准库日志模块完全兼容,因此您可以使用 1 package main 2 3 import ( 4 "os" 5 log "github.com/sirupsen/logrus" 6 ) 7 8 func init() { 9 // 设置日志格式为json格式 10 log.SetFormatter(&log.JSONFormatter{}) 11 12 // 设置将日志输出到标准输出(默认的输出为stderr,标准错误) 13 // 日志消息输出可以是任意的io.writer类型 14 log.SetOutput(os.Stdout) 15 16 // 设置日志级别为warn以上 17 log.SetLevel(log.WarnLevel) 18 } 19 20 func main() { 21 log.WithFields(log.Fields{ 22 "animal": "walrus", 23 "size": 10, 24 }).Info("A group of walrus emerges from the ocean") 25 26 log.WithFields(log.Fields{ 27 "omg": true, 28 "number": 122, 29 }).Warn("The group's number increased tremendously!") 30 31 log.WithFields(log.Fields{ 32 "omg": true, 33 "number": 100, 34 }).Fatal("The ice breaks!") 35 } 输出: {"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2020-01-14T10:28:02+08:00"} {"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2020-01-14T10:28:02+08:00"} exit status 1
Loggerlogger是一种相对高级的用法, 对于一个大型项目, 往往需要一个全局的logrus实例,即 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") } Fields前一章提到过,logrus不推荐使用冗长的消息来记录运行信息,它推荐使用 log.Fatalf("Failed to send event %s to topic %s with key %d", event, topic, key) 在logrus中不太提倡,logrus鼓励使用以下方式替代之: log.WithFields(log.Fields{ "event": event, "topic": topic, "key": key, }).Fatal("Failed to send event") 前面的WithFields API可以规范使用者按照其提倡的方式记录日志。但是WithFields依然是可选的,因为某些场景下,使用者确实只需要记录仪一条简单的消息。 通常,在一个应用中、或者应用的一部分中,都有一些固定的Field。比如在处理用户http请求时,上下文中,所有的日志都会有request_id和user_ip。为了避免每次记录日志都要使用log.WithFields(log.Fields{"request_id": request_id, "user_ip": user_ip}),我们可以创建一个logrus.Entry实例,为这个实例设置默认Fields,在上下文中使用这个logrus.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")
Hooklogrus最令人心动的功能就是其可扩展的HOOK机制了,通过在初始化时为logrus添加hook,logrus可以实现各种扩展功能。 Hook接口logrus的hook接口定义如下,其原理是每此写入日志时拦截,修改logrus.Entry。 // logrus在记录Levels()返回的日志级别的消息时会触发HOOK, // 按照Fire方法定义的内容修改logrus.Entry。 type Hook interface { Levels() []Level Fire(*Entry) error } 一个简单自定义hook如下, 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 } hook的使用也很简单,在初始化前调用 logrus官方仅仅内置了syslog的hook。
记录文件名和行号logrus的一个很致命的问题就是没有提供文件名和行号,这在大型项目中通过日志定位问题时有诸多不便。Github上的logrus的issue#63:Log filename and line number创建于2014年,四年过去了仍是open状态~~~ 标准库runtime模块的Caller(skip int)函数可以返回当前goroutine调用栈中的文件名,行号,函数信息等,参数skip表示表示返回的栈帧的层次,0表示runtime.Caller的调用着。返回值包括响应栈帧层次的pc(程序计数器),文件名和行号信息。为了提高效率,我们先通过跟踪调用栈发现,从runtime.Caller()的调用者开始,到记录日志的生成代码之间,大概有8到11层左右,所有我们在hook中循环第8到11层调用栈应该可以找到日志记录的生产代码。 此外,runtime.FuncForPC(pc uintptr) *Func可以返回指定pc的函数信息。 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()"
日志本地文件分割 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 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 } 其中 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_amqp:Logrus hook for Activemq。 其他注意事项 线程安全 没有设置hook,或者所有的hook都是线程安全的实现。 |
请发表评论