我們可以使用日志來觀察程序的行為、診斷問題或者配置相應(yīng)的告警等。定義良好的結(jié)構(gòu)化日志,能夠提高日志的檢索效率,使處理問題變得更加方便。
為了方便使用,Kratos定義了兩個層面的抽象,Logger統(tǒng)一了日志的接入方式,Helper接口統(tǒng)一的日志庫的調(diào)用方式。
在不同的公司、使用不同的基礎(chǔ)架構(gòu),可能對日志的打印方式、格式、輸出的位置等要求各有不同。Kratos為了更加靈活地適配和遷移到各種環(huán)境,把日志組件也進(jìn)行了抽象,這樣就可以把業(yè)務(wù)代碼里日志的使用,和日志底層具體的實(shí)現(xiàn)隔離開來,提高整體的可維護(hù)性。
Kratos的日志庫主要有如下特性:
Helper:高級日志接口,提供了一系列帶有日志等級和格式化方法的幫助函數(shù),通常業(yè)務(wù)邏輯中建議使用這個,能夠簡化日志代碼。
你可以認(rèn)為它是一個對Logger的包裝,簡化了打印時需要傳入的參數(shù)。
它的用法基本上是下面的樣子,后面會介紹具體的使用方法
helper.Info("hello")
helper.Errorf("hello %s", "eric")
Logger:這個是底層日志接口,用于快速適配各種日志庫到框架中來,僅需要實(shí)現(xiàn)一個最簡單的Log方法。
type Logger interface {
Log(level Level, keyvals ...interface{}) error
}
?Level
?參數(shù)用來標(biāo)識日志的等級,可以在level.go中找到。
?keyvals
?是一個平鋪的鍵值數(shù)組,它的長度需要是偶數(shù),奇數(shù)位上的是key,偶數(shù)位上的是value。
這個Logger接口在實(shí)現(xiàn)完畢后的使用,簡單來講就是如下的樣子:
logger.Log(log.LevelInfo, "msg", "hello", "instance_id", 123)
很顯然,直接用它有點(diǎn)難受,所以我們建議在項(xiàng)目中用?Helper
?。
它的意義在于,通過簡單使用Logger接口,能夠快速把您的日志庫適配進(jìn)來,并且用Helper來統(tǒng)一打印的行為。
對于日志等級的定義在level.go中,您可以在使用底層的Log方法時傳入它們,它們會被輸出到日志的?level
?字段中。在高級接口?Helper
?使用特定的帶日志等級的方法比如?.Infof
?等,會自動應(yīng)用等級,無需自己綁定等級。
log.LevelDebug
log.LevelInfo
log.LevelWarn
log.LevelError
log.LevelFatal
我們已經(jīng)在contrib/log實(shí)現(xiàn)好了一些插件,用于適配目前常用的日志庫,您也可以參考它們的代碼來實(shí)現(xiàn)自己需要的日志庫的適配:
Kratos日志庫使用十分簡單,和大部分日志庫類似。
如果覺得創(chuàng)建logger很麻煩,可以直接用框架默認(rèn)初始化好的?log.DefaultLogger
?實(shí)例,它底層直接調(diào)用了go標(biāo)準(zhǔn)庫的log,可以打到標(biāo)準(zhǔn)輸出。
框架內(nèi)置實(shí)現(xiàn)了stdLogger,能夠打印到標(biāo)準(zhǔn)輸出。使用?NewStdLogger
?方法傳入一個?io.Writer
?即可。
首先你需要創(chuàng)建一個Logger,這里可以選:自帶的std打印到標(biāo)準(zhǔn)輸出,或者在contrib下面找一個已經(jīng)實(shí)現(xiàn)好的適配,或者用自己實(shí)現(xiàn)的Logger。
import "github.com/go-kratos/kratos/v2/log"
h := NewHelper(yourlogger)
//用默認(rèn)logger可以直接用
h := NewHelper(log.DefaultLogger)
或者在contrib/log里面找一個插件用,比如這里我們想用fluentd:
import "github.com/go-kratos/kratos/contrib/log/fluent/v2"
logger, err := fluent.NewLogger("unix:///var/run/fluent/fluent.sock")
if err != nil {
return
}
h := log.NewHelper(logger)
您可以指定默認(rèn)的日志打印到的字段,不設(shè)的話默認(rèn)為?msg
?
NewHelper(logger, WithMessageKey("message"))
注意:調(diào)用Fatal等級的方法會在打印日志后中斷程序運(yùn)行,請謹(jǐn)慎使用。
直接打印不同等級的日志,會默認(rèn)打到messageKey里,默認(rèn)是?msg
?
h.Debug("Are you OK?")
h.Info("42 is the answer to life, the universe, and everything")
h.Warn("We are under attack!")
h.Error("Houston, we have a problem.")
h.Fatal("So Long, and Thanks for All the Fish.")
格式化打印不同等級的日志,方法都以f結(jié)尾
h.Debugf("Hello %s", "boy")
h.Infof("%d is the answer to life, the universe, and everything", 233)
h.Warnf("We are under attack %s!", "boss")
h.Errorf("%s, we have a problem.", "Master Shifu")
h.Fatalf("So Long, and Thanks for All the %s.", "banana")
格式化打印不同等級的日志,方法都以w結(jié)尾,參數(shù)為key value對,可以輸入多組。
h.Debugw("custom_key", "Are you OK?")
h.Infow("custom_key", "42 is the answer to life, the universe, and everything")
h.Warnw("custom_key", "We are under attack!")
h.Errorw("custom_key", "Houston, we have a problem.")
h.Fatalw("custom_key", "So Long, and Thanks for All the Fish.")
使用底層的Log接口直接打印key和value
h.Log(log.LevelInfo, "key1", "value1")
在業(yè)務(wù)日志中,通常我們會在每條日志中輸出一些全局的字段,比如時間戳,實(shí)例id,追蹤id,用戶id,調(diào)用函數(shù)名等,顯然在每條日志中手工寫入這些值很麻煩。為了解決這個問題,可以使用Valuer。您可以認(rèn)為它是logger的“中間件”,用它來打一些全局的信息到日志里。
?log.With
?方法會返回一個新的Logger,把參數(shù)的Valuer綁上去。
注意要按照key,value的順序?qū)?yīng)寫入?yún)?shù)。
使用方法如下:
logger = log.With(logger, "ts", log.DefaultTimestamp, "caller", log.DefaultCaller)
框架默認(rèn)提供了如下Valuer供使用,您也可以參考它們的代碼實(shí)現(xiàn)自定義Valuer。
有時日志中可能會有敏感信息,需要進(jìn)行脫敏,或者只打印級別高的日志,這時候就可以使用Filter來對日志的輸出進(jìn)行一些過濾操作,通常用法是使用Filter來包裝原始的Logger,用來創(chuàng)建Helper使用。
它提供了如下參數(shù):
FilterLevel
?按照日志等級過濾,低于該等級的日志將不會被輸出。例如這里傳入?FilterLevel(log.LevelError)
?,則debug/info/warn日志都會被過濾掉不會輸出,error和fatal正常輸出。
FilterKey(key ...string) FilterOption
? 按照key過濾,這些key的值會被?***
?遮蔽
FilterValue(value ...string) FilterOption
? 按照value過濾,匹配的值會被?***
?遮蔽
FilterFunc(f func(level Level, keyvals ...interface{}) bool)
? 使用自定義的函數(shù)來對日志進(jìn)行處理,keyvals里為key和對應(yīng)的value,按照奇偶進(jìn)行讀取即可h := NewHelper(
NewFilter(logger,
// 等級過濾
FilterLevel(log.LevelError),
// 按key遮蔽
FilterKey("username"),
// 按value遮蔽
FilterValue("hello"),
// 自定義過濾函數(shù)
FilterFunc(
func (level Level, keyvals ...interface{}) bool {
if level == LevelWarn {
return true
}
for i := 0; i < len(keyvals); i++ {
if keyvals[i] == "password" {
keyvals[i+1] = fuzzyStr
}
}
return false
}
),
),
)
h.Log(log.LevelDebug, "msg", "test debug")
h.Info("hello")
h.Infow("password", "123456")
h.Infow("username", "kratos")
h.Warn("warn log")
設(shè)置context,使用如下方法將返回一個綁定指定context的helper實(shí)例
newHelper := h.WithContext(ctx)
我們在middleware/logging提供了一個日志中間件,使用它可以記錄server端或client端每個請求的路由、參數(shù)、耗時等信息。使用時建議配合Filter對請求參數(shù)日志進(jìn)行脫敏,避免敏感信息泄漏。
這個middleware的代碼也十分清晰地展示了如何在中間件里獲取和處理請求和返回信息,具有很大的參考價值,您可以基于它的代碼實(shí)現(xiàn)自己的日志中間件等。
在我們的默認(rèn)項(xiàng)目模板中,我們在cmd/server/main.go的?main()
?函數(shù),即程序入口處初始化了logger實(shí)例,并注入了一些全局的日志值,它們會被打到所有輸出的日志中。
您可以修改這里使用的logger,來進(jìn)行自定義打印的值,或者更換為自己需要的logger實(shí)現(xiàn)。
logger := log.With(log.NewStdLogger(os.Stdout),
"ts", log.DefaultTimestamp,
"caller", log.DefaultCaller,
"service.id", id,
"service.name", Name,
"service.version", Version,
"trace_id", tracing.TraceID(),
"span_id", tracing.SpanID(),
)
這個logger將通過依賴注入工具wire的生成,注入到項(xiàng)目的各層中,供其內(nèi)部使用。
一個具體的內(nèi)部使用例子可以參考internal/service/greeter.go
我們在這里將注入進(jìn)來的logger實(shí)例,用?log.NewHelper
?包裝成Helper,綁定到service上,這樣就可以在這一層調(diào)用這個綁定的的helper對象來打日志了。
func NewGreeterService(uc *biz.GreeterUsecase, logger log.Logger) *GreeterService {
return &GreeterService{uc: uc, log: log.NewHelper(logger)} // 初始化和綁定helper
}
func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {
// 打印日志
s.log.WithContext(ctx).Infof("SayHello Received: %v", in.GetName())
return &v1.HelloReply{Message: "Hello " + in.GetName()}, nil
}
其它幾個層級的初始化和使用方式也是一樣的,在biz層和data層中我們也給了logger注入的樣例,您可以進(jìn)行參考。
更多建議: