如何把panic信息重定向
根據(jù)“墨菲定律”,我們編寫(xiě)的后臺(tái)的服務(wù)都有出現(xiàn)crash的可能,一種情況是Go的后臺(tái)服務(wù)我們經(jīng)常也會(huì)遇到panic的情況。出問(wèn)題不可怕,我們需要分析并解決問(wèn)題,不過(guò)panic處理的信息,默認(rèn)是直接標(biāo)準(zhǔn)輸出的,我們希望能捕獲它指向我們特定的文件以便能做后續(xù)問(wèn)題的跟蹤排查,而不是一次性輸出難以跟蹤。
我們一個(gè)通用的方法是
err := execFunc()
if err != nil {
outputToFile(err)
}
但有一些第三方庫(kù)會(huì)使用panic/recover機(jī)制作為其內(nèi)部的異??刂品绞?,這樣我們?cè)谕饷媸请y以察覺(jué)的,異常信息可能就直接打到我們的標(biāo)準(zhǔn)輸出那里了,除非你在執(zhí)行程序之前,使用類似linux的 ./test >> panic.log ,否則我們會(huì)很大機(jī)會(huì)與重要的跟蹤信息擦肩而過(guò)。(跨平臺(tái)到windows可能不適用)
所以,如何把panic的信息靈活地“重定向”呢?
實(shí)現(xiàn)思路一般是:
1、既然panic使用的的是標(biāo)準(zhǔn)輸出,我們可以使用自定義的文件file引用取代go里頭的os.Stdout 和 os.Stderr
2、引起panic并測(cè)試重定向的正確性
3、windows里面沒(méi)有stdout和stderr的輸出方式,也沒(méi)辦法像unix那樣使用“>>”進(jìn)行標(biāo)準(zhǔn)輸出的重新向,這個(gè)如何破?
我們先試試一個(gè)簡(jiǎn)單的例子:
package main
import (
"fmt";
"os";
)
const panicFile = "/tmp/panic.log"
func InitPanicFile() error {
log.Println("init panic file in unix mode")
file, err := os.OpenFile(panicFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
return err
}
os.Stdout = file
os.Stderr = file
return nil
}
func init() {
err := pc.InitPanicFile()
if err != nil {
println(err)
}
}
func testPanic() {
panic("test panic")
}
func main() {
testPanic()
}
這個(gè)例子,我們嘗試使用 os.Stdout = file 和 os.Stderr = file 來(lái)“強(qiáng)制”轉(zhuǎn)換,但我們運(yùn)行程序后,發(fā)現(xiàn)不起作用, /tmp/panic.log 沒(méi)有任何信息流入,panic信息照樣輸出到標(biāo)準(zhǔn)輸出那里。
關(guān)于原因,Rob是這樣說(shuō)的:

看來(lái)是把變量直接賦值到底層是不行的,圖上所說(shuō),推薦使用syscall.Dup的方式。我們?cè)俑膶?xiě)下 InitPanicFile() 函數(shù):
func InitPanicFile() error {
log.Println("init panic file in unix mode")
file, err := os.OpenFile(panicFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
println(err)
return err
}
if err = syscall.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil {
return err
}
return nil
}
我們運(yùn)行程序,發(fā)現(xiàn)panic正常定向到我們的文件里面去了:
$ tail -f /tmp/panic.log
panic: test panic
goroutine 1 [running]:
... ...
... ...
不過(guò)經(jīng)過(guò)實(shí)踐,上面的代碼是有些bug的,原因是我們上面的file是一個(gè)局部變量,放系統(tǒng)發(fā)生gc的時(shí)候,會(huì)觸發(fā)file里面的 runtime.SetFinalizer(f.file, (*file).close), 會(huì)引起句柄會(huì)被回收, 如果我們代碼是長(zhǎng)期運(yùn)行在后臺(tái)的話,建議代碼調(diào)整如下的形式:
var globalFile *os.File
func InitPanicFile() error {
log.Println("init panic file in unix mode")
file, err := os.OpenFile(panicFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
globalFile = file
if err != nil {
println(err)
return err
}
if err = syscall.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil {
return err
}
return nil
}
接下來(lái),我們要延伸思考下,如果服務(wù)是運(yùn)行在windows上面該如何破?
使用syscall.Dup2的例子windows下會(huì)編譯直接報(bào)錯(cuò):
undefined: syscall.Dup2
... ...
syscall.Dup2 is a linux/OSX only thing. there's no windows equivalent。
記得我前面的文件,介紹過(guò)go調(diào)用DLL的方法 《使用Go結(jié)合windows dll開(kāi)發(fā)程序》 ,其實(shí)我們也可以想到,可以直接使用DLL的調(diào)用達(dá)到功能效果:
代碼如下:
package main
import (
"log"
"os"
"syscall"
)
const (
kernel32dll = "kernel32.dll"
)
const panicFile = "C:/panic.log"
var globalFile *os.File
func InitPanicFile() error {
log.Println("init panic file in windows mode")
file, err := os.OpenFile(panicFile, os.O_CREATE|os.O_APPEND, 0666)
globalFile = file
if err != nil {
return err
}
kernel32 := syscall.NewLazyDLL(kernel32dll)
setStdHandle := kernel32.NewProc("SetStdHandle")
sh := syscall.STD_ERROR_HANDLE
v, _, err := setStdHandle.Call(uintptr(sh), uintptr(file.Fd()))
if v == 0 {
return err
}
return nil
}
func init() {
err := pc.InitPanicFile()
if err != nil {
println(err)
}
}
func testPanic() {
panic("test panic")
}
func main() {
testPanic()
}
然后我們把編譯后的代碼在windows下運(yùn)行,panic信息也能正常重定向到指定文件上了。