有別的語言使用基礎的同學工作中都會接觸到日志的使用,Go中自然也有log相關的實現。Go log模塊主要提供了3類接口,分別是 “Print 、Panic 、Fatal ”,對每一類接口其提供了三種調用方式,分別是 “Xxxx 、Xxxxln 、Xxxxf”,基本和fmt中的相關函數類似。
1. Go中的log包
1. 基本使用
- log.Print:打印日志,和fmt包沒什么區別,只是加上了上面的日志格式
- log.Fatal :會先將日志內容打印到標准輸出,接着調用系統的os.exit(1) 接口,退出程序並返回狀態 1 。但是有一點需要注意,由於是直接調用系統接口退出,defer函數不會被調用
- log.Panic:該函數把日志內容刷到標准錯誤后調用 panic 函數
log 結構的定義如下:
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中所有的操作都是支持並發的。
下面看一下這三種log打印的用法:
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也是支持的。
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
也不會生效,大家自己可以測試一下。
LUTC
比較特殊,如果我們配置了時間標簽,那么如果設置了LUTC
的話,就會把輸出的日期時間轉為0時區的日期時間顯示。
最后一個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.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
的原因。
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支持如下特性:
- 完全兼容Go標准庫日志模塊。logrus擁有六種日志級別:debug、info、warn、error、fatal和panic,這是Go標准庫日志模塊的API的超集。如果你的項目使用標准庫日志模塊,完全可以用最低的代價遷移到logrus上。
- 可擴展的Hook機制。允許使用者通過hook方式,將日志分發到任意地方,如本地文件系統、標准輸出、logstash、elasticsearch或者mq等,或者通過hook定義日志內容和格式等。
- 可選的日志輸出格式。**logrus內置了兩種日志格式,JSONFormatter和TextFormatter。**如果這兩個格式不滿足需求,可以自己動手實現接口Formatter,來定義自己的日志格式。
- Field機制。logrus鼓勵通過Field機制進行精細化、結構化的日志記錄,而不是通過冗長的消息來記錄日志。
- logrus是一個可插拔的、結構化的日志框架。
logrus不提供的功能:
- 沒有提供行號和文件名的支持
- 輸出到本地文件系統沒有提供日志分割功能
- 沒有提供輸出到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字符串。