應用日誌

我們期望開發的 Web 應用程式能夠把整個程式執行過程中出現的各種事件一一記錄下來,Go 語言中提供了一個簡易的 log 套件,我們使用該套件可以方便的實現日誌記錄的功能,這些日誌都是基於 fmt 套件的列印再結合 panic 之類別的函式來進行一般的列印、丟擲錯誤處理。Go 目前標準套件只是包含了簡單的功能,如果我們想把我們的應用日誌儲存到檔案,然後又能夠結合日誌實現很多複雜的功能(編寫過 Java 或者 C++的讀者應該都使用過 log4j 和 log4cpp 之類別的日誌工具),可以使用第三方開發的日誌系統:logrusseelog,它們實現了很強大的日誌功能,可以結合自己專案選擇。接下來我們介紹如何透過該日誌系統來實現我們應用的日誌功能。

logrus 介紹

logrus 是用 Go 語言實現的一個日誌系統,與標準函式庫 log 完全相容並且核心 API 很穩定,是 Go 語言目前最活躍的日誌函式庫

首先安裝 logrus

go get -u github.com/sirupsen/logrus

簡單例子:

package main

import (
    log "github.com/Sirupsen/logrus"
)

func main() {
    log.WithFields(log.Fields{
        "animal": "walrus",
    }).Info("A walrus appears")
}

基於 logrus 的自訂日誌處理

package main

import (
    "os"

    log "github.com/Sirupsen/logrus"
)

func init() {
    // 日誌格式化為 JSON 而不是預設的 ASCII

    log.SetFormatter(&log.JSONFormatter{})

    // 輸出 stdout 而不是預設的 stderr,也可以是一個檔案
    log.SetOutput(os.Stdout)

    // 只記錄嚴重或以上警告
    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!")

    // 透過日誌語句重用欄位
    // logrus.Entry 回傳自 WithFields()
    contextLogger := log.WithFields(log.Fields{
        "common": "this is a common field",
        "other":  "I also should be logged always",
    })

    contextLogger.Info("I'll be logged with common and other field")
    contextLogger.Info("Me too")
}

seelog 介紹

seelog 是用 Go 語言實現的一個日誌系統,它提供了一些簡單的函式來實現複雜的日誌分配、過濾和格式化。主要有如下特性:

  • XML 的動態配置,可以不用重新編譯程式而動態的載入配置資訊

  • 支援熱更新,能夠動態改變配置而不需要重啟應用

  • 支援多輸出流,能夠同時把日誌輸出到多種流中、例如檔案流、網路流等

  • 支援不同的日誌輸出

    • 命令列輸出

    • 檔案輸出

    • 快取輸出

    • 支援 log rotate

    • SMTP 郵件

上面只列舉了部分特性,seelog 是一個特別強大的日誌處理系統,詳細的內容請參看官方 wiki。接下來我將簡要介紹一下如何在專案中使用它:

首先安裝 seelog

go get -u github.com/cihub/seelog

然後我們來看一個簡單的例子:

package main

import log "github.com/cihub/seelog"

func main() {
    defer log.Flush()
    log.Info("Hello from Seelog!")
}

編譯後執行如果出現了Hello from seelog,說明 seelog 日誌系統已經成功安裝並且可以正常運行了。

基於 seelog 的自訂日誌處理

seelog 支援自訂日誌處理,下面是我基於它自訂的日誌處理套件的部分內容:

package logs

import (
    // "errors"
    "fmt"
    // "io"

    seelog "github.com/cihub/seelog"
)

var Logger seelog.LoggerInterface

func loadAppConfig() {
    appConfig := `
<seelog minlevel="warn">
    <outputs formatid="common">
        <rollingfile type="size" filename="/data/logs/roll.log" maxsize="100000" maxrolls="5"/>
        <filter levels="critical">
            <file path="/data/logs/critical.log" formatid="critical"/>
            <smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
                <recipient address="xiemengjun@gmail.com"/>
            </smtp>
        </filter>
    </outputs>
    <formats>
        <format id="common" format="%Date/%Time [%LEV] %Msg%n" />
        <format id="critical" format="%File %FullPath %Func %Msg%n" />
        <format id="criticalemail" format="Critical error on our server!\n    %Time %Date %RelFile %Func %Msg \nSent by Seelog"/>
    </formats>
</seelog>
`
    logger, err := seelog.LoggerFromConfigAsBytes([]byte(appConfig))
    if err != nil {
        fmt.Println(err)
        return
    }
    UseLogger(logger)
}

func init() {
    DisableLog()
    loadAppConfig()
}

// DisableLog disables all library log output
func DisableLog() {
    Logger = seelog.Disabled
}

// UseLogger uses a specified seelog.LoggerInterface to output library log.
// Use this func if you are using Seelog logging system in your app.
func UseLogger(newLogger seelog.LoggerInterface) {
    Logger = newLogger
}

上面主要實現了三個函式,

  • DisableLog

    初始化全域性變數 Logger 為 seelog 的禁用狀態,主要為了防止 Logger 被多次初始化

  • loadAppConfig

    根據配置檔案初始化 seelog 的配置資訊,這裡我們把配置檔案透過字串讀取設定好了,當然也可以透過讀取 XML 檔案。裡面的配置說明如下:

    • seelog

      minlevel 參數可選,如果被配置,高於或等於此級別的日誌會被記錄,同理 maxlevel。

    • outputs

      輸出資訊的目的地,這裡分成了兩份資料,一份記錄到 log rotate 檔案裡面。另一份設定了 filter,如果這個錯誤級別是 critical,那麼將傳送報警郵件。

    • formats

      定義了各種日誌的格式

  • UseLogger

    設定當前的日誌器為相應的日誌處理

上面我們定義了一個自訂的日誌處理套件,下面就是使用範例:

package main

import (
    "net/http"
    "project/logs"
    "project/configs"
    "project/routes"
)

func main() {
    addr, _ := configs.MainConfig.String("server", "addr")
    logs.Logger.Info("Start server at:%v", addr)
    err := http.ListenAndServe(addr, routes.NewMux())
    logs.Logger.Critical("Server err:%v", err)
}

發生錯誤傳送郵件

上面的例子解釋了如何設定傳送郵件,我們透過如下的 smtp 配置用來發送郵件:

<smtp formatid="criticalemail" senderaddress="astaxie@gmail.com" sendername="ShortUrl API" hostname="smtp.gmail.com" hostport="587" username="mailusername" password="mailpassword">
    <recipient address="xiemengjun@gmail.com"/>
</smtp>

郵件的格式透過 criticalemail 配置,然後透過其他的配置傳送郵件伺服器的配置,透過 recipient 配置接收郵件的使用者,如果有多個使用者可以再新增一行。

要測試這個程式碼是否正常工作,可以在程式碼中增加類似下面的一個假訊息。不過記住過後要把它刪除,否則上線之後就會收到很多垃圾郵件。

logs.Logger.Critical("test Critical message")

現在,只要我們的應用在線上記錄一個 Critical 的資訊,你的郵箱就會收到一個 Email,這樣一旦線上的系統出現問題,你就能立馬透過郵件獲知,就能及時的進行處理。

使用應用日誌

對於應用日誌,每個人的應用場景可能會各不相同,有些人利用應用日誌來做資料分析,有些人利用應用日誌來做效能分析,有些人來做使用者行為分析,還有些就是純粹的記錄,以方便應用出現問題的時候輔助查詢問題。

舉一個例子,我們需要追蹤使用者嘗試登陸系統的操作。這裡會把成功與不成功的嘗試都記錄下來。記錄成功的使用"Info"日誌級別,而不成功的使用"warn"級別。如果想查詢所有不成功的登陸,我們可以利用 linux 的 grep 之類別的命令工具,如下:

# cat /data/logs/roll.log | grep "failed login"
2012-12-11 11:12:00 WARN : failed login attempt from 11.22.33.44 username password

透過這種方式我們就可以很方便的查詢相應的資訊,這樣有利於我們針對應用日誌做一些統計和分析。另外我們還需要考慮日誌的大小,對於一個高流量的 Web 應用來說,日誌的增長是相當可怕的,所以我們在 seelog 的配置檔案裡面設定了 logrotate,這樣就能保證日誌檔案不會因為不斷變大而導致我們的磁碟空間不夠引起問題。

小結

透過上面對 seelog 系統及如何基於它進行自訂日誌系統的學習,現在我們可以很輕鬆的隨需建構一個合適的功能強大的日誌處理系統了。日誌處理系統為資料分析提供了可靠的資料來源,比如透過對日誌的分析,我們可以進一步優化系統,或者應用出現問題時方便查詢定位問題,另外 seelog 也提供了日誌分級功能,透過對 minlevel 的配置,我們可以很方便的設定測試或釋出版本的輸出訊息級別。

Last updated