開源技術教學文件網 加入系統記錄 (Logging)

最後修改日期為 AUG 26, 2019

前言

當我們使用網頁框架寫程式的時候,這些框架通常都內含系統紀錄 (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 Yahoo
追蹤本站
Facebook Facebook Twitter