Go中的日志及第三方日志包logrus

有别的语言使用基础的同窗工做中都会接触到日志的使用,Go中天然也有log相关的实现。Go log模块主要提供了3类接口,分别是 “Print 、Panic 、Fatal ”,对每一类接口其提供了三种调用方式,分别是 “Xxxx 、Xxxxln 、Xxxxf”,基本和fmt中的相关函数相似。git

1. Go中的log包

1. 基本使用

  1. log.Print:打印日志,和fmt包没什么区别,只是加上了上面的日志格式
  2. log.Fatal :会先将日志内容打印到标准输出,接着调用系统的os.exit(1) 接口,退出程序并返回状态 1 。可是有一点须要注意,因为是直接调用系统接口退出,defer函数不会被调用
  3. log.Panic:该函数把日志内容刷到标准错误后调用 panic 函数

log 结构的定义以下:github

type Logger struct {
	mu     sync.Mutex // ensures atomic writes; protects the following fields
	prefix string     // prefix to write at beginning of each line
	flag   int        // properties
	out    io.Writer  // destination for output
	buf    []byte     // for accumulating text to write
}

可见在结构体中有sync.Mutex类型字段,因此log中全部的操做都是支持并发的。web

下面看一下这三种log打印的用法:json

package main

import (
	"log"
)


func main() {
	log.Print("我就是一条日志")
	log.Printf("%s,","谁说我是日志了,我是错误")
	log.Panic("哈哈,我好痛")
}

输出:安全

2019/05/23 22:14:36 我就是一条日志
2019/05/23 22:14:36 谁说我是日志了,我是错误,
2019/05/23 22:14:36 哈哈,我好痛
panic: 哈哈,我好痛

goroutine 1 [running]:
log.Panic(0xc00007bf78, 0x1, 0x1)
	D:/soft/go/src/log/log.go:333 +0xb3
main.main()
	E:/go_path/src/webDemo/demo.go:12 +0xfd

使用很是简单,能够看到log的默认输出带了时间,很是的方便。Panic方法在输出后调用了Panic方法,因此抛出了异常信息。上面的示例中没有演示Fatal方法,你能够试着把log.Fatal()放在程序的第一行,你会发现下面的代码都不会执行。由于上面说过,它在打印完日志以后会调用os.exit(1)方法,因此系统就退出了。并发

2. 定制打印参数

上面说到log打印的时候默认是自带时间的,那若是除了时间之外,咱们还想要别的信息呢,固然log也是支持的。app

SetFlags(flag int)方法提供了设置打印默认信息的能力,下面的字段是log中自带的支持的打印类型:框架

Ldate         = 1 << iota     // the date in the local time zone: 2009/01/23
Ltime                         // the time in the local time zone: 01:23:23
Lmicroseconds                 // microsecond resolution: 01:23:23.123123.  assumes Ltime.
Llongfile                     // full file name and line number: /a/b/c/d.go:23
Lshortfile                    // final file name element and line number: d.go:23. overrides Llongfile
LUTC                          // if Ldate or Ltime is set, use UTC rather than the local time zone
LstdFlags     = Ldate | Ltime // initial values for the standard logger

这是log包定义的一些抬头信息,有日期、时间、毫秒时间、绝对路径和行号、文件名和行号等,在上面都有注释说明,这里须要注意的是:若是设置了Lmicroseconds,那么Ltime就不生效了;设置了Lshortfile, Llongfile也不会生效,你们本身能够测试一下。elasticsearch

LUTC比较特殊,若是咱们配置了时间标签,那么若是设置了LUTC的话,就会把输出的日期时间转为0时区的日期时间显示。ide

最后一个LstdFlags表示标准的日志抬头信息,也就是默认的,包含日期和具体时间。

使用方法:

func init(){
    log.SetFlags(log.Ldate|log.Lshortfile)
}

使用init方法,能够在main函数执行以前初始化代码。另外,虽然参数是int类型,可是上例中使用位运算符传递了多个常量为何会被识别到底传了啥进去了呢。这是由于源码中去作解析的时候,也是根据不一样的常量组合的位运算去判断你传了啥的。因此先看源码,你就能够大胆的传了。

package main

import (
"log"
)


func main() {
	log.SetFlags(log.Ldate|log.Lshortfile)
	log.Print("我就是一条日志")
	log.Printf("%s,","谁说我是日志了,我是错误")

}

输出:
2019/05/23 demo.go:11: 我就是一条日志
2019/05/23 demo.go:12: 谁说我是日志了,我是错误,

3. 如何传自定义参数进日志

在Java开发中咱们会有这样的日志需求:为了查日志更方便,咱们须要在一个http请求或者rpc请求进来到结束的做用链中用一个惟一id将全部的日志串起来,这样能够在日志中搜索这个惟一id就能拿到此次请求的全部日志记录。

因此如今的任务是如何在Go的日志中去定义这样的一个id。Go中提供了这样的一个方法:SetPrefix(prefix string),经过log.SetPrefix能够指定输出日志的前缀。

package main

import (
	uuid "github.com/satori/go.uuid"
	"log"
)


func main() {
	uuids, _ := uuid.NewV1()
	log.SetPrefix(uuids.String() +" ")
	log.SetFlags(log.Ldate|log.Lshortfile)
	log.Print("我就是一条日志")
	log.Printf("%s,","谁说我是日志了,我是错误")

}

输出:
1791d770-7d6a-11e9-b2ee-00fffa4e4d0c 2019/05/23 demo.go:13: 我就是一条日志
1791d770-7d6a-11e9-b2ee-00fffa4e4d0c 2019/05/23 demo.go:14: 谁说我是日志了,我是错误,

4. log 输出的底层实现

从源码中咱们能够看到,不管是Print,Panic,仍是Fatal他们都是使用std.Output(calldepth int, s string)方法。std的定义以下:

func New(out io.Writer, prefix string, flag int) *Logger {
	return &Logger{out: out, prefix: prefix, flag: flag}
}
var std = New(os.Stderr, "", LstdFlags)

即每一次调用log的时候都会去建立一个Logger对象。另外New中传入的第一个参数是os.Stderros.Stderr对应的是UNIX里的标准错误警告信息的输出设备,同时被做为默认的日志输出目的地。初次以外,还有标准输出设备os.Stdout以及标准输入设备os.Stdin

var (
	Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")
	Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")
	Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")
)

前两种分别用于输入、输出和警告错误信息。

咱们再来看一下,全部的输出都会调用的方法:std.Output(calldepth int, s string)

func (l *Logger) Output(calldepth int, s string) error {
   now := time.Now()
   var file string
   var line int
   //加锁,保证多goroutine下的安全
   l.mu.Lock()
   defer l.mu.Unlock()
   //若是配置了获取文件和行号的话
   if l.flag&(Lshortfile|Llongfile) != 0 {
       //由于runtime.Caller代价比较大,先不加锁
       l.mu.Unlock()
       var ok bool
       _, file, line, ok = runtime.Caller(calldepth)
       if !ok {
           file = "???"
           line = 0
       }
       //获取到行号等信息后,再加锁,保证安全
       l.mu.Lock()
   }
   //把咱们的日志信息和设置的日志抬头进行拼接
   l.buf = l.buf[:0]
   l.formatHeader(&l.buf, now, file, line)
   l.buf = append(l.buf, s...)
   if len(s) == 0 || s[len(s)-1] != '\n' {
       l.buf = append(l.buf, '\n')
   }
   //输出拼接好的缓冲buf里的日志信息到目的地
   _, err := l.out.Write(l.buf)
   return err
}

formatHeader方法主要是格式化日志抬头信息,就是咱们上面提到设置的日志打印格式,解析完以后存储在buf这个缓冲中,最后再把咱们本身的日志信息拼接到缓冲buf的后面,而后为一次log日志输出追加一个换行符,这样每第二天志输出都是一行一行的。

上面咱们提到过runtime.Caller(calldepth)这个方法,runtime包很是有意思,后面也会去说,他提供了一个运行时环境,能够在运行时去管理内存分配,垃圾回收,时间片切换等等,相似于Java中虚拟机作的活。(是否是很疑惑为何在Go中居然能够去作Java中虚拟机能作的事情,其实想一想协程的概念,再对比线程的概念,就不会疑惑为啥会给你提供这么个包)。

Caller方法的解释是:

Caller方法查询有关函数调用的文件和行号信息,经过调用Goroutine的堆栈。参数skip是堆栈帧框架升序方式排列的数字值,0标识Caller方法的调用。(出于历史缘由,Skip的含义在调用者和调用者之间有所不一样。)

返回值报告程序计数器、文件名和相应文件中行号的查询。若是没法恢复信息,则Boolean OK为 fasle。

Caller方法的定义:

func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
}

参数skip表示跳过栈帧数,0表示不跳过,也就是runtime.Caller的调用者。1的话就是再向上一层,表示调用者的调用者。

log日志包里使用的是2,也就是表示咱们在源代码中调用log.Printlog.Fatallog.Panic这些函数的调用者。

main函数调用log.Println为例,main->log.Println->*Logger.Output->runtime.Caller这么一个方法调用栈,因此这时候,skip的值分别表明:

  1. 0表示*Logger.Output中调用runtime.Caller的源代码文件和行号
  2. 1表示log.Println中调用*Logger.Output的源代码文件和行号
  3. 2表示main中调用log.Println的源代码文件和行号

因此这也是log包里的这个skip的值为何一直是2的缘由。

5. 如何自定义本身的日志框架

经过上面的学习,你其实知道了,日志的实现是经过New()函数构造了Logger对象来处理的。那咱们只用构造不一样的Logger对象来处理不一样类型的日记便可。下面是一个简单的实现:

package main

import (
	"io"
	"log"
	"os"
)

var (
	Info *log.Logger
	Warning *log.Logger
	Error * log.Logger
)

func init(){
	infoFile,err:=os.OpenFile("/data/service_logs/info.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666)
	warnFile,err:=os.OpenFile("/data/service_logs/warn.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666)
	errFile,err:=os.OpenFile("/data/service_logs/errors.log",os.O_CREATE|os.O_WRONLY|os.O_APPEND,0666)

	if infoFile!=nil || warnFile != nil || err!=nil{
		log.Fatalln("打开日志文件失败:",err)
	}

	Info = log.New(os.Stdout,"Info:",log.Ldate | log.Ltime | log.Lshortfile)
	Warning = log.New(os.Stdout,"Warning:",log.Ldate | log.Ltime | log.Lshortfile)
	Error = log.New(io.MultiWriter(os.Stderr,errFile),"Error:",log.Ldate | log.Ltime | log.Lshortfile)

	Info = log.New(io.MultiWriter(os.Stderr,infoFile),"Info:",log.Ldate | log.Ltime | log.Lshortfile)
	Warning = log.New(io.MultiWriter(os.Stderr,warnFile),"Warning:",log.Ldate | log.Ltime | log.Lshortfile)
	Error = log.New(io.MultiWriter(os.Stderr,errFile),"Error:",log.Ldate | log.Ltime | log.Lshortfile)


}

func main() {
	Info.Println("我就是一条日志啊")
	Warning.Printf("我真的是一条日志哟%s\n","别骗我")
	Error.Println("好了,我要报错了")
}

2. 第三方日志包logrus

上面介绍了Go中的log包,Go标准库的日志框架很是简单,仅仅提供了Print,Panic和Fatal三个函数。对于更精细的日志级别、日志文件分割,以及日志分发等方面,并无提供支持 。也有不少第三方的开源爱好者贡献了不少好用的日志框架,毕竟Go是新兴预言,目前为止没有哪一个日志框架能产生与Java中的slf4j同样的地位,目前流行的日志框架有seelog,zap,logrus,还有beego中的日志框架部分。

这些日志框架可能在某些方面不能知足你的需求,因此使用以前先了解清楚。由于logrus目前在GitHub上的star最高,11011。因此本篇文章介绍logrus的使用,你们能够触类旁通。   logrus的GitHub地址:

1. logrus特性

logrus支持以下特性:

  1. 彻底兼容Go标准库日志模块。logrus拥有六种日志级别:debug、info、warn、error、fatal和panic,这是Go标准库日志模块的API的超集。若是你的项目使用标准库日志模块,彻底能够用最低的代价迁移到logrus上。
  2. 可扩展的Hook机制。容许使用者经过hook方式,将日志分发到任意地方,如本地文件系统、标准输出、logstash、elasticsearch或者mq等,或者经过hook定义日志内容和格式等。
  3. 可选的日志输出格式。**logrus内置了两种日志格式,JSONFormatter和TextFormatter。**若是这两个格式不知足需求,能够本身动手实现接口Formatter,来定义本身的日志格式。
  4. Field机制。logrus鼓励经过Field机制进行精细化、结构化的日志记录,而不是经过冗长的消息来记录日志。
  5. logrus是一个可插拔的、结构化的日志框架。

logrus不提供的功能:

  1. 没有提供行号和文件名的支持
  2. 输出到本地文件系统没有提供日志分割功能
  3. 没有提供输出到ELK等日志处理中心的功能

这些功能均可以经过自定义hook来实现 。

2. 简单的入门

安装:

go get github.com/sirupsen/logrus
2.1 一个简单的入门:
package main

import log "github.com/sirupsen/logrus"

func main() {
	log.Info("我是一条日志")
	log.WithFields(log.Fields{"key":"value"}).Info("我要打印了")
}

输出:
time="2019-05-24T08:13:47+08:00" level=info msg="我是一条日志"
time="2019-05-24T08:13:47+08:00" level=info msg="我要打印了" key=value
2.2 设置log的日志输出为json格式

将日志输出格式设置为JSON格式:

log.SetFormatter(&log.JSONFormatter{})
package main

import (
	log "github.com/sirupsen/logrus"
)


func initLog() {
	// 设置日志格式为json格式
	log.SetFormatter(&log.JSONFormatter{})
}

func main() {
	initLog()
	log.WithFields(log.Fields{
		"age": 12,
		"name":   "xiaoming",
		"sex": 1,
	}).Info("小明来了")

	log.WithFields(log.Fields{
		"age": 13,
		"name":   "xiaohong",
		"sex": 0,
	}).Error("小红来了")

	log.WithFields(log.Fields{
		"age": 14,
		"name":   "xiaofang",
		"sex": 1,
	}).Fatal("小芳来了")
}

输出:
{"age":12,"level":"info","msg":"小明来了","name":"xiaoming","sex":1,"time":"2019-05-24T08:20:19+08:00"}
{"age":13,"level":"error","msg":"小红来了","name":"xiaohong","sex":0,"time":"2019-05-24T08:20:19+08:00"}
{"age":14,"level":"fatal","msg":"小芳来了","name":"xiaofang","sex":1,"time":"2019-05-24T08:20:19+08:00"}

看到这里输出的日志格式与上面的区别,这里是json格式,上面是纯文本。

2.3 设置日志打印级别

logrus 提供 6 档日志级别,分别是:

PanicLevel
FatalLevel
ErrorLevel
WarnLevel
InfoLevel
DebugLevel

设置日志输出级别:

log.SetLevel(log.WarnLevel)
2.4 自定义输出字段

logrus 默认的日志输出有 time、level 和 msg 3个 Field,其中 time 能够不显示,方法以下:

log.SetFormatter(&log.TextFormatter{DisableTimestamp: true})

自定义 Field 的方法以下:

log.WithFields(log.Fields{
		"age": 14,
		"name":   "xiaofang",
		"sex": 1,
	}).Fatal("小芳来了")
2.5 自定义日志输出路径

logrus默认日志输出为stderr,你能够修改成任何的io.Writer。好比os.File文件流。

func init() {
    //设置输出样式,自带的只有两种样式logrus.JSONFormatter{}和logrus.TextFormatter{}
    logrus.SetFormatter(&logrus.JSONFormatter{})
    //设置output,默认为stderr,能够为任何io.Writer,好比文件*os.File
    file, _ := os.OpenFile("1.log", os.O_CREATE|os.O_WRONLY, 0666)
	log.SetOutput(file)
    //设置最低loglevel
    logrus.SetLevel(logrus.InfoLevel)
}

3. 高级功能-hook机制

上面说过logrus是一个支持可插拔,结构化的日志框架,可插拔的特性就在于它的hook机制。一些功能须要用户本身经过hook机制去实现定制化的开发。好比说在log4j中常见的日志按天按小时作切分的功能官方并无提供支持,你能够经过hook机制实现它。

Hook接口定义以下:

type Hook interface {
	Levels() []Level
	Fire(*Entry) error
}

logrus的hook原理是:在每次写入日志时拦截,修改logrus.Entry 。logrus在记录Levels()返回的日志级别的消息时,会触发HOOK, 而后按照Fire方法定义的内容,修改logrus.Entry 。logrus.Entry里面就是记录的每一条日志的内容。

因此在Hook中你须要作的就是在Fire方法中定义你想如何操做这一条日志的方法,在Levels方法中定义你想展现的日志级别。

以下是一个在全部日志中打印一个特殊字符串的Hook:

TraceIdHook

package hook

import (
	 "github.com/sirupsen/logrus"
)


type TraceIdHook struct {
	TraceId  string
}

func NewTraceIdHook(traceId string) logrus.Hook {
	hook := TraceIdHook{
		TraceId:  traceId,
	}
	return &hook
}

func (hook *TraceIdHook) Fire(entry *logrus.Entry) error {
	entry.Data["traceId"] = hook.TraceId
	return nil
}

func (hook *TraceIdHook) Levels() []logrus.Level {
	return logrus.AllLevels
}

主程序:

package main

import (
	uuid "github.com/satori/go.uuid"
	log "github.com/sirupsen/logrus"
	"webDemo/hook"
)


func initLog() {
	uuids, _ := uuid.NewV1()
	log.AddHook(hook.NewTraceIdHook(uuids.String() +" "))
}

func main() {
	initLog()
	log.WithFields(log.Fields{
		"age": 12,
		"name":   "xiaoming",
		"sex": 1,
	}).Info("小明来了")

	log.WithFields(log.Fields{
		"age": 13,
		"name":   "xiaohong",
		"sex": 0,
	}).Error("小红来了")

	log.WithFields(log.Fields{
		"age": 14,
		"name":   "xiaofang",
		"sex": 1,
	}).Fatal("小芳来了")
}

该hook会在日志中打印出一个uuid字符串。

相关文章
相关标签/搜索