依賴分佈式系統的公司組織和團隊常用Go
語言編寫其應用程序,以利用Go
語言諸如通道和goroutine
之類的併發功能。若是你負責研發或運維Go
應用程序,則考慮周全的日誌記錄策略能夠幫助你瞭解用戶行爲,定位錯誤並監控應用程序的性能。 git
這篇文章將展開聊一些用於管理Go
日誌的工具和技術。咱們將首先考慮要使用哪一種日誌記錄包來知足各類記錄要求。而後會介紹一些使日誌更易於搜索和可靠,減小日誌資源佔用以及使日誌消息標準化的技術。github
Go
標準庫的日誌庫很是簡單,僅僅提供了print
,panic
和fatal
三個函數對於更精細的日誌級別、日誌文件分割以及日誌分發等方面並無提供支持. 因此催生了不少第三方的日誌庫,流行的日誌框架包括logrus
、zap
、glog
等。咱們先來大體看下這些日誌庫的特色再來根據實際應用狀況選擇合適的日誌庫。json
Go
的內置日誌記錄庫(log
)帶有一個默認記錄器(logger
),該記錄器可寫入標準錯誤並自動向記錄中添加時間戳,而無需進行配置。你可使用它日誌用於本地開發,和試驗性的代碼段。這時從代碼中得到快速反饋可能比生成豐富結構化的日誌更爲重要。安全
logrus
是一個爲結構化日誌記錄而設計的日誌記錄包,很是適合以JSON
格式記錄日誌。 JSON
格式使機器能夠輕鬆解析Go
日誌。並且,因爲JSON
是定義明確的標準,所以經過包含新字段能夠輕鬆地添加上下文,解析器可以自動提取它們。服務器
使用logrus
,可使用功能WithFields
定義要添加到JSON日誌中的標準字段,以下所示。而後,能夠在不一樣日誌級別調用記錄器,例如Info()
,Warn()
和Error()
。 logrus
庫將自動以JSON
格式寫入日誌,並插入標準字段以及您即時定義的全部字段。網絡
package main import ( log "github.com/sirupsen/logrus" ) func main() { log.SetFormatter(&log.JSONFormatter{}) standardFields := log.Fields{ "hostname": "staging-1", "appname": "foo-app", "session": "1ce3f6v", } requestLogger := log.withFields(standardFields) requestLogger.WithFields(log.Fields{"string": "foo", "int": 1, "float": 1.1}).Info("My first ssl event from Golang") }
生成的日誌將在JSON
對象中包括消息,日誌級別,時間戳、標準字段以及調用記錄器即時寫入的字段:session
{"appname":"foo-app","float":1.1,"hostname":"staging-1","int":1,"level":"info","msg":"My first ssl event from Golang","session":"1ce3f6v","string":"foo","time":"2019-03-06T13:37:12-05:00"}
glog
容許啓用或禁用特定級別的日誌記錄,這對於在開發和生產環境之間切換時保持檢查日誌量頗有用。它使您能夠在命令行中使用標誌(例如,-v表示詳細信息)來設置運行代碼時的日誌記錄級別。而後,能夠在if
語句中使用V()
函數僅在特定日誌級別上寫入Go
日誌。功能Info()
,Warning()
,Error()
和Fatal()
分別指定日誌級別0
到3
併發
if err != nil && glog.V(2){ glog.Error(err) }
上面分析了,標準庫的log
只適合非項目級別的代碼片斷的快速驗證和調試。logrus
在結構化日誌上作的最好,有利於日誌分析。glog
能夠減小日誌佔用的磁盤空間。不過相比產生的日誌佔用空間大的問題,利於分析的日誌給應用產品帶來的價值更大,因此logrus
使用的更多一些。不少開源項目,如Docker
,Prometheus
等都是用了logrus
來記錄他們的日誌。app
logrus
是目前Github
上star數量最多的日誌庫,目前(2020.03)star
數量爲14000+,fork
數爲1600+。logrus
功能強大,性能高效,並且具備高度靈活性,提供了自定義插件的功能。不少開源項目,如Docker
,Prometheus
等都是用了logrus
來記錄他們的日誌。框架
logrus
徹底兼容Go
標準庫日誌模塊,擁有六種日誌級別:debug
、info
、warn
、error
、fatal
和panic
,這是Go
標準庫日誌模塊的API的超集.若是你的項目使用標準庫日誌模塊,徹底能夠以最低的代價遷移到logrus
上.Hook
機制:容許使用者經過hook
的方式將日誌分發到任意地方,如本地文件系統、標準輸出、logstash
、elasticsearch
或者mq
等。logrus
內置了兩種日誌格式,JSONFormatter
和TextFormatter
還能夠本身動手實現接口Formatter,來定義本身的日誌格式。Field
機制:logrus
鼓勵經過Field
機制進行精細化的、結構化的日誌記錄,而不是經過冗長的消息來記錄日誌。Entry
: logrus.WithFields
會自動返回一個 *Entry
,Entry
會自動向日誌記錄裏添加記錄建立的時間time
字段。logrus
與Go
標準庫日誌模塊徹底兼容, logrus
能夠經過簡單的配置,來定義輸出、格式或者日誌級別等。
package main import ( "os" log "github.com/sirupsen/logrus" ) func init() { // 設置日誌格式爲json格式 log.SetFormatter(&log.JSONFormatter{}) // 設置將日誌輸出到指定文件(默認的輸出爲stderr,標準錯誤) // 日誌消息輸出能夠是任意的io.writer類型 logFile := ... file, _ := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) log.SetOutput(file) // 設置只記錄日誌級別爲warn及其以上的日誌 log.SetLevel(log.WarnLevel) } func main() { log.WithFields(log.Fields{ "animal": "walrus", "size": 10, }).Info("A group of walrus emerges from the ocean") log.WithFields(log.Fields{ "omg": true, "number": 122, }).Warn("The group's number increased tremendously!") log.WithFields(log.Fields{ "omg": true, "number": 100, }).Fatal("The ice breaks!") }
若是想在一個應用裏面向多個地方寫log
,能夠建立多個記錄器Logger
實例。
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") }
logrus
不推薦使用冗長的消息來記錄運行信息,它推薦使用Fields
來進行精細化的、結構化的信息記錄. 例以下面的記錄日誌的方式:
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
能夠規範使用者按照其提倡的方式記錄日誌。可是WithFields
依然是可選的,由於某些場景下,確實只須要記錄一條簡單的消息。
一般,在一個應用中、或者應用的一部分中,始終附帶一些固定的記錄字段會頗有幫助。好比在處理用戶HTTP
請求時,上下文中全部的日誌都會有request_id
和user_ip。
爲了不每次記錄日誌都要使用:
log.WithFields(log.Fields{「request_id」: request_id, 「user_ip」: user_ip})
咱們能夠建立一個logrus.Entry
實例,爲這個實例設置默認Fields
,把logrus.Entry
實例設置到記錄器Logger
,再記錄日誌時每次都會附帶上這些默認的字段。
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")
logrus
最使人心動的功能就是其可擴展的HOOK
機制。經過在初始化時爲logrus
添加hook
,logrus
能夠實現各類擴展功能.
logrus
的hook
接口定義以下,其原理是每次寫入日誌時攔截修改logrus.Entry
.
// logrus在記錄Levels()返回的日誌級別的消息時會觸發HOOK, // 按照Fire方法定義的內容修改logrus.Entry. type Hook interface { Levels() []Level Fire(*Entry) error }
一個簡單自定義hook
以下,DefaultFieldHook
定義會在全部級別的日誌消息中加入默認字段appName=」myAppName」
。
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
的使用也很簡單,在初始化前調用log.AddHook(hook)
添加相應的hook
便可。Hook
比較常見的用法是把指定錯誤級別的日誌記錄消息提醒發送到郵件組或者錯誤監控系統(好比sentry
),起到主動錯誤通知的做用。
logrus
官方僅僅內置了syslog
的hook
。但Github
有不少第三方的hook
可供使用。比方剛纔說的sentry
相關的hook
。
Sentry
是一個錯誤監控系統,可使用廠商的服務也能夠在本身的服務器上搭建Sentry
。模式跟GitLab
很像,也是提供一鍵安裝包。爲應用註冊Sentry
後會分配一個DSN
用於鏈接Sentry
服務。
import ( "github.com/sirupsen/logrus" "github.com/evalphobia/logrus_sentry" ) func main() { log := logrus.New() hook, err := logrus_sentry.NewSentryHook(YOUR_DSN, []logrus.Level{ logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, }) if err == nil { log.Hooks.Add(hook) } }
默認狀況下,Logger
受mutex
保護,以進行併發寫入。當鉤子被調用而且日誌被寫入時,mutex
會被保持。若是肯定不須要這種鎖,則能夠調用logger.SetNoLock()
禁用該鎖。
不須要鎖的狀況包括:
hook
,或者hook
調用是線程安全的。logger.Out
是線程安全的,好比logger.Out
已經被鎖保護或者logger.Out
是一個以Append
模式打開的文件句柄。選擇了項目使用的日誌庫後,您還須要計劃在代碼中調用記錄器的位置,如何存儲日誌。在本部分中,將推薦一些整理Go
日誌的最佳實踐,他們包括:
goroutine
中調用記錄器。HTTP
標頭攜帶分佈式惟一ID記錄微服務中的用戶行爲。避免建立本身的goroutine
來處理寫日誌有兩個緣由。首先,它可能致使併發問題,由於記錄器的副本將嘗試訪問相同的io.Writer
。其次,日誌記錄庫一般會本身啓動goroutine
,在內部管理全部併發問題,而啓動本身的goroutine
只會形成干擾。
即便將日誌發送到中央日誌平臺,咱們也建議您先將日誌寫到本地計算機上的文件中。這確保您的日誌始終在本地可用,而且不會在網絡中丟失。此外,寫入文件意味着您能夠將寫入日誌的任務與將日誌發送到中央日誌平臺的任務分開。您的應用程序自己無需創建鏈接或流式傳輸日誌給日誌平臺,您能夠將這些任務交給專業的軟件處理,好比使用Elasticsearch
索引日誌數據的話,那麼就能夠用Logstash
從日誌文件裏抽取日誌數據。
若是您的應用程序部署在多個主機羣集中,應用的日誌會分散到不一樣機器上。日誌從本地文件傳遞到中央日誌平臺,以便進行日誌數據的分析和彙總。關於日誌處理服務的選擇,開源的日誌處理服務有ELK
,各個雲服務廠商也有本身的日誌處理服務,根據自身狀況選擇便可,儘可能選和雲服務器同一廠商的日誌服務,這樣不用消耗公網的流量。
對於構建在分佈式系統之上的應用,一個請求可能會流經多個服務,每一個服務都會本身記錄日誌。這種狀況下爲了查詢請求對應的日誌,一般的解決方案是在請求頭中攜帶惟一ID,分佈式系統中全部服務的日誌記錄器中增長惟一ID字段,這樣每條寫入的日誌裏都會有HTTP
請求的惟一ID。在統一日誌平臺中分析日誌時,經過上游服務日誌記錄的請求惟一 ID 便可查詢到該請求在下游全部服務中產生的日誌。
參考連接:
https://www.datadoghq.com/blo...
https://github.com/sirupsen/l...