有別的語言使用基礎的同窗工做中都會接觸到日誌的使用,Go中天然也有log相關的實現。Go log模塊主要提供了3類接口,分別是 「Print 、Panic 、Fatal 」,對每一類接口其提供了三種調用方式,分別是 「Xxxx 、Xxxxln 、Xxxxf」,基本和fmt中的相關函數相似。git
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)
方法,因此係統就退出了。併發
上面說到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: 誰說我是日誌了,我是錯誤,
在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: 誰說我是日誌了,我是錯誤,
從源碼中咱們能夠看到,不管是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.Stderr
,os.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.Print
、log.Fatal
和log.Panic
這些函數的調用者。
以main
函數調用log.Println
爲例,main->log.Println->*Logger.Output->runtime.Caller
這麼一個方法調用棧,因此這時候,skip的值分別表明:
0
表示*Logger.Output
中調用runtime.Caller
的源代碼文件和行號1
表示log.Println
中調用*Logger.Output
的源代碼文件和行號2
表示main
中調用log.Println
的源代碼文件和行號因此這也是log
包裏的這個skip
的值爲何一直是2
的緣由。
經過上面的學習,你其實知道了,日誌的實現是經過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("好了,我要報錯了") }
上面介紹了Go中的log包,Go標準庫的日誌框架很是簡單,僅僅提供了Print,Panic和Fatal三個函數。對於更精細的日誌級別、日誌文件分割,以及日誌分發等方面,並無提供支持 。也有不少第三方的開源愛好者貢獻了不少好用的日誌框架,畢竟Go是新興預言,目前爲止沒有哪一個日誌框架能產生與Java中的slf4j同樣的地位,目前流行的日誌框架有seelog,zap,logrus,還有beego中的日誌框架部分。
這些日誌框架可能在某些方面不能知足你的需求,因此使用以前先了解清楚。由於logrus目前在GitHub上的star最高,11011。因此本篇文章介紹logrus的使用,你們能夠觸類旁通。 logrus的GitHub地址:
logrus支持以下特性:
logrus不提供的功能:
這些功能均可以經過自定義hook來實現 。
安裝:
go get github.com/sirupsen/logrus
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
將日誌輸出格式設置爲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格式,上面是純文本。
logrus 提供 6 檔日誌級別,分別是:
PanicLevel FatalLevel ErrorLevel WarnLevel InfoLevel DebugLevel
設置日誌輸出級別:
log.SetLevel(log.WarnLevel)
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("小芳來了")
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) }
上面說過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字符串。