位元詩人 [Golang] 網頁設計教學:加入系統記錄 (Logging)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

當我們使用網頁框架寫程式的時候,這些框架通常都內含系統紀錄 (logging) 的功能,所以不會特別強調這個部分。但在我們這系列文章中,我們主要是使用 Golang 的標準函式庫寫網頁程式,系統紀錄並不是內附的功能。如果讀者有跟著我們先前的範例做,應該可以發現先前的程式沒有系統紀錄的功能。

在本文中,我們會介紹如何在網頁程式中加入系統紀錄的套件,之後我們寫的網頁程式就有系統紀錄的功能。

Golang 的系統記錄 (Logging) 方案

log 是 Golang 內附的系統紀錄套件。使用的方式和 fmt 差不多,在需要輸出系統紀錄檔的地方放上相關程式碼即可。但 log 沒有和網頁程式整合,使用該套件時要自己逐一加上程式碼,使用起來比較沒有效率。

sirupsen/logrus 是一個系統記錄的框架,除了提供 log 套件的功能外,還可以將記錄層分層管理,在不同環境下輸出不同層級的記錄檔。由於 logrus 在 API 上刻意和 log 相容,故可用來取代內建的系統記錄套件。像是使用以下語法:

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

將原本的 log 識別字代換掉,就可以用 logrus 取代內建系統記錄套件。

先前所介紹的系統記錄套件皆沒有和網頁程式整合,所以得自行在網頁程式中加入相關程式碼。urfave/negroni 本身是 Golang 網頁程式的中介軟體 (middleware),可以和 Golang 網頁程式結合。

negroni 本身無法重導記錄檔到檔案 (file) 或其他輸出,在生產環境中不太好用。幸好有熱心的程式人將 logrusnegroni 結合,解決了這個議題。本文後半段會展示一個實際的例子。

在網頁程式中加入系統記錄功能

在本文的第一個範例中,我們來看單用 negroni 的實例:

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/julienschmidt/httprouter"
    "github.com/urfave/negroni"
)

func main() {
    host := "127.0.0.1"
    port := "8080"

    // Consume first argument, which is always the program name.
    args := os.Args[1:]

    // Parse CLI arguments.
    for {
        if len(args) < 2 {
            break
        } else if args[0] == "-h" || args[0] == "--host" {
            host = args[1]

            args = args[2:]
        } else if args[0] == "-p" || args[0] == "--port" {
            port = args[1]

            args = args[2:]
        } else {
            log.Fatalln(fmt.Sprintf("Unknown parameter: %s", args[0]))
        }
    }

    // Set a new HTTP request multiplexer
    mux := httprouter.New()

    // Listen to root path
    mux.GET("/", index)

    // Custom 404 page
    mux.NotFound = http.HandlerFunc(notFound)

    // Custom 500 page
    mux.PanicHandler = errorHandler

    // Set the logger of the server.
    n := negroni.Classic()
    n.UseHandler(mux)

    // Set the parameters for a HTTP server
    server := http.Server{
        Addr:    fmt.Sprintf("%s:%s", host, port),
        Handler: n,
    }

    // Run the server.
    log.Println(fmt.Sprintf("Run the web server at %s:%s", host, port))
    log.Fatal(server.ListenAndServe())
}

func index(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    fmt.Fprintln(w, "Hello World")
}

func notFound(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusNotFound)
    fmt.Fprintln(w, "Page Not Found")
}

func errorHandler(w http.ResponseWriter, r *http.Request, p interface{}) {
    w.WriteHeader(http.StatusInternalServerError)
    fmt.Fprintln(w, "Internal Server Error")
}

在這個範例中,大部分的程式碼和系統記錄無關,這部分我們先前已經看過了。關鍵的程式碼在以下兩行:

// Set the logger of the server.
n := negroni.Classic()
n.UseHandler(mux)

在第一行中,我們用 Classic() 函式建立一個 negroni 物件。該物件內含三個中介程式,分別用於錯誤回復、記錄請求回應、記錄靜態檔案等。Classic() 沒有可用的參數,其行為是固定的。

在第二行中,我們將 mux 物件傳入 UseHandler() 函式中,就可以將 mux 物件和 n (negroni) 物件結合在一起。

接著,在建立 server 物件時,將路徑處理器指向 n (negroni) 物件。程式碼如下:

// Set the parameters for a HTTP server
server := http.Server{
    Addr:    fmt.Sprintf("%s:%s", host, port),
    Handler: n,
}

為什麼這樣寫是合法的程式碼呢?因為 negroni 物件本身是中介軟體,等於是在原本的路徑處理器上多疊加一層程式碼,所以才有輸出系統記錄的功能。

如果是練習用的網頁程式,這樣就夠用了。但 negroni 套件無法指定系統記錄的輸出,在生產環境中不太實用。故我們會在下一節改寫這個程式。

將記錄導向檔案

在本節的範例中,我們將 logrus 整合到網頁程式中。參考以下程式碼:

package main

import (
    "fmt"
    "net/http"
    "os"

    negronilogrus "github.com/meatballhat/negroni-logrus"

    "github.com/julienschmidt/httprouter"
    log "github.com/sirupsen/logrus"
    "github.com/urfave/negroni"
)

func main() {
    host := "127.0.0.1"
    port := "8080"
    output := ""

    // Consume first argument, which is always the program name.
    args := os.Args[1:]

    // Parse CLI arguments.
    for {
        if len(args) < 2 {
            break
        } else if args[0] == "-h" || args[0] == "--host" {
            host = args[1]

            args = args[2:]
        } else if args[0] == "-p" || args[0] == "--port" {
            port = args[1]

            args = args[2:]
        } else if args[0] == "-l" || args[0] == "--log" {
            output = args[1]

            args = args[2:]
        } else {
            log.Fatalln(fmt.Sprintf("Unknown parameter: %s", args[0]))
        }
    }

    // Set a new HTTP request multiplexer
    mux := httprouter.New()

    // Listen to root path
    mux.GET("/", index)

    // Custom 404 page
    mux.NotFound = http.HandlerFunc(notFound)

    // Custom 500 page
    mux.PanicHandler = errorHandler

    // Create a new logger.
    l := log.New()

    var f *os.File
    var err error

    if output != "" {
        f, err = os.Create(output)
        if err != nil {
            log.Fatal(err)
        }
        defer f.Close()

        l.SetOutput(f)
    }

    // Use custom logger in negroni.
    n := negroni.New()
    n.Use(negronilogrus.NewMiddlewareFromLogger(l, "web"))
    n.UseHandler(mux)

    // Set the parameters for a HTTP server
    server := http.Server{
        Addr:    fmt.Sprintf("%s:%s", host, port),
        Handler: n,
    }

    // Run the server.
    l.Println(fmt.Sprintf("Run the web server at %s:%s", host, port))
    l.Fatal(server.ListenAndServe())
}

func index(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
    fmt.Fprintln(w, "Hello World")
}

func notFound(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusNotFound)
    fmt.Fprintln(w, "Page Not Found")
}

func errorHandler(w http.ResponseWriter, r *http.Request, p interface{}) {
    w.WriteHeader(http.StatusInternalServerError)
    fmt.Fprintln(w, "Internal Server Error")
}

一開始先用 logrus 取代內建的 log 套件:

log "github.com/sirupsen/logrus"

因為 logrus 在 API 上刻意和內建 log 貼合,這樣寫不太會引發錯誤。

接著,建立一個新的系統記錄物件 l

// Create a new logger.
l := log.New()

var f *os.File
var err error

if output != "" {
    f, err = os.Create(output)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()

    l.SetOutput(f)
}

output 不為空字串時,將輸出導向 output 所指向的路徑。這是因為 logrus 可重導記錄檔的輸出標的。

關鍵的步驟在於將 logrus 整合到 negroni 中。參考以下程式碼:

// Use custom logger in negroni.
n := negroni.New()
n.Use(negronilogrus.NewMiddlewareFromLogger(l, "web"))
n.UseHandler(mux)

在這段程式碼中,我們將系統記錄物件 l 做為參數傳到中介程式中。因為已經有熱心的開發者寫好中介程式 (middleware) 了,所以可以直接使用,不需自己重寫中介程式。

結語

由於 Golang 網頁程式本身功能比較精簡,沒有預先加入系統記錄的功能,故我們在這裡介紹自行加入系統記錄的方式。在加入這個功能後,當我們在寫網頁程式時,藉由程式吐出的訊息,比較容易抓出程式中的錯誤。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。