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

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk