前言
當我們使用網頁框架寫程式的時候,這些框架通常都內含系統紀錄 (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) 或其他輸出,在生產環境中不太好用。幸好有熱心的程式人將 logrus 和 negroni 結合,解決了這個議題。本文後半段會展示一個實際的例子。
在網頁程式中加入系統記錄功能
在本文的第一個範例中,我們來看單用 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 網頁程式本身功能比較精簡,沒有預先加入系統記錄的功能,故我們在這裡介紹自行加入系統記錄的方式。在加入這個功能後,當我們在寫網頁程式時,藉由程式吐出的訊息,比較容易抓出程式中的錯誤。