關於收集,標準化和集中化處理Golang日誌的一些建議

依賴分佈式系統的公司組織和團隊常用Go語言編寫其應用程序,以利用Go語言諸如通道和goroutine之類的併發功能。若是你負責研發或運維Go應用程序,則考慮周全的日誌記錄策略能夠幫助你瞭解用戶行爲,定位錯誤並監控應用程序的性能。 git

這篇文章將展開聊一些用於管理Go日誌的工具和技術。咱們將首先考慮要使用哪一種日誌記錄包來知足各類記錄要求。而後會介紹一些使日誌更易於搜索和可靠,減小日誌資源佔用以及使日誌消息標準化的技術。github

日誌包的選擇

Go標準庫的日誌庫很是簡單,僅僅提供了print,panicfatal三個函數對於更精細的日誌級別、日誌文件分割以及日誌分發等方面並無提供支持. 因此催生了不少第三方的日誌庫,流行的日誌框架包括logruszapglog等。咱們先來大體看下這些日誌庫的特色再來根據實際應用狀況選擇合適的日誌庫。json

log標準庫

Go的內置日誌記錄庫(log)帶有一個默認記錄器(logger),該記錄器可寫入標準錯誤並自動向記錄中添加時間戳,而無需進行配置。你可使用它日誌用於本地開發,和試驗性的代碼段。這時從代碼中得到快速反饋可能比生成豐富結構化的日誌更爲重要。安全

logrus

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

glog容許啓用或禁用特定級別的日誌記錄,這對於在開發和生產環境之間切換時保持檢查日誌量頗有用。它使您能夠在命令行中使用標誌(例如,-v表示詳細信息)來設置運行代碼時的日誌記錄級別。而後,能夠在if語句中使用V()函數僅在特定日誌級別上寫入Go日誌。功能Info()Warning()Error()Fatal()分別指定日誌級別03併發

if err != nil && glog.V(2){
    glog.Error(err)
  }

日誌庫的選擇

上面分析了,標準庫的log只適合非項目級別的代碼片斷的快速驗證和調試。logrus在結構化日誌上作的最好,有利於日誌分析。glog能夠減小日誌佔用的磁盤空間。不過相比產生的日誌佔用空間大的問題,利於分析的日誌給應用產品帶來的價值更大,因此logrus使用的更多一些。不少開源項目,如Docker,Prometheus等都是用了logrus來記錄他們的日誌。app

logrus的使用介紹

logrus是目前Github上star數量最多的日誌庫,目前(2020.03)star數量爲14000+,fork數爲1600+。logrus功能強大,性能高效,並且具備高度靈活性,提供了自定義插件的功能。不少開源項目,如Docker,Prometheus等都是用了logrus來記錄他們的日誌。框架

  • logrus徹底兼容Go標準庫日誌模塊,擁有六種日誌級別:debuginfowarnerrorfatalpanic,這是Go標準庫日誌模塊的API的超集.若是你的項目使用標準庫日誌模塊,徹底能夠以最低的代價遷移到logrus上.
  • 可擴展的Hook機制:容許使用者經過hook的方式將日誌分發到任意地方,如本地文件系統、標準輸出、logstashelasticsearch或者mq等。
  • logrus內置了兩種日誌格式,JSONFormatterTextFormatter還能夠本身動手實現接口Formatter,來定義本身的日誌格式。
  • Field機制:logrus鼓勵經過Field機制進行精細化的、結構化的日誌記錄,而不是經過冗長的消息來記錄日誌。
  • Entry: logrus.WithFields會自動返回一個 *Entry,Entry會自動向日誌記錄裏添加記錄建立的時間time字段。

基本用法

logrusGo標準庫日誌模塊徹底兼容, 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!")
}

自定義Logger

若是想在一個應用裏面向多個地方寫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")
}

Fields

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依然是可選的,由於某些場景下,確實只須要記錄一條簡單的消息。

Default Fields

一般,在一個應用中、或者應用的一部分中,始終附帶一些固定的記錄字段會頗有幫助。好比在處理用戶HTTP請求時,上下文中全部的日誌都會有request_iduser_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")

Hook接口

logrus最使人心動的功能就是其可擴展的HOOK機制。經過在初始化時爲logrus添加hooklogrus能夠實現各類擴展功能.

logrushook接口定義以下,其原理是每次寫入日誌時攔截修改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官方僅僅內置了sysloghook。但Github有不少第三方的hook可供使用。比方剛纔說的sentry相關的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)
  }
}

logrus 是線程安全的

默認狀況下,Loggermutex保護,以進行併發寫入。當鉤子被調用而且日誌被寫入時,mutex會被保持。若是肯定不須要這種鎖,則能夠調用logger.SetNoLock()禁用該鎖。

不須要鎖的狀況包括:

  • 沒有註冊hook,或者hook調用是線程安全的。
  • 寫入logger.Out是線程安全的,好比logger.Out已經被鎖保護或者logger.Out是一個以Append模式打開的文件句柄。

日誌寫入和存儲的一些建議

選擇了項目使用的日誌庫後,您還須要計劃在代碼中調用記錄器的位置,如何存儲日誌。在本部分中,將推薦一些整理Go日誌的最佳實踐,他們包括:

  • 從的主應用程序流程而不是goroutine中調用記錄器。
  • 將日誌從應用程序寫入本地文件,即便之後再將其發送到日誌集中化處理平臺也是如此。
  • 定義日誌的標準化默認字段
  • 將日誌發送到日誌處理平臺,以便進行分析和彙總。
  • 使用HTTP標頭攜帶分佈式惟一ID記錄微服務中的用戶行爲。

避免在goroutine中使用日誌記錄器

避免建立本身的goroutine來處理寫日誌有兩個緣由。首先,它可能致使併發問題,由於記錄器的副本將嘗試訪問相同的io.Writer。其次,日誌記錄庫一般會本身啓動goroutine,在內部管理全部併發問題,而啓動本身的goroutine只會形成干擾。

老是將日誌寫入文件

即便將日誌發送到中央日誌平臺,咱們也建議您先將日誌寫到本地計算機上的文件中。這確保您的日誌始終在本地可用,而且不會在網絡中丟失。此外,寫入文件意味着您能夠將寫入日誌的任務與將日誌發送到中央日誌平臺的任務分開。您的應用程序自己無需創建鏈接或流式傳輸日誌給日誌平臺,您能夠將這些任務交給專業的軟件處理,好比使用Elasticsearch索引日誌數據的話,那麼就能夠用Logstash從日誌文件裏抽取日誌數據。

使用日誌處理平臺集中處理日誌

若是您的應用程序部署在多個主機羣集中,應用的日誌會分散到不一樣機器上。日誌從本地文件傳遞到中央日誌平臺,以便進行日誌數據的分析和彙總。關於日誌處理服務的選擇,開源的日誌處理服務有ELK,各個雲服務廠商也有本身的日誌處理服務,根據自身狀況選擇便可,儘可能選和雲服務器同一廠商的日誌服務,這樣不用消耗公網的流量。

使用惟一ID跨服務跟蹤Go日誌

對於構建在分佈式系統之上的應用,一個請求可能會流經多個服務,每一個服務都會本身記錄日誌。這種狀況下爲了查詢請求對應的日誌,一般的解決方案是在請求頭中攜帶惟一ID,分佈式系統中全部服務的日誌記錄器中增長惟一ID字段,這樣每條寫入的日誌裏都會有HTTP請求的惟一ID。在統一日誌平臺中分析日誌時,經過上游服務日誌記錄的請求惟一 ID 便可查詢到該請求在下游全部服務中產生的日誌。

參考連接:

https://www.datadoghq.com/blo...

https://github.com/sirupsen/l...

相關文章
相關標籤/搜索