4 27
如何把panic信息重定向

根据“墨菲定律”,我们编写的后台的服务都有出现crash的可能,一种情况是Go的后台服务我们经常也会遇到panic的情况。出问题不可怕,我们需要分析并解决问题,不过panic处理的信息,默认是直接标准输出的,我们希望能捕获它指向我们特定的文件以便能做后续问题的跟踪排查,而不是一次性输出难以跟踪。

我们一个通用的方法是

err := execFunc()
if err != nil {
    outputToFile(err)
}

但有一些第三方库会使用panic/recover机制作为其内部的异常控制方式,这样我们在外面是难以察觉的,异常信息可能就直接打到我们的标准输出那里了,除非你在执行程序之前,使用类似linux的 ./test >> panic.log ,否则我们会很大机会与重要的跟踪信息擦肩而过。(跨平台到windows可能不适用)

所以,如何把panic的信息灵活地“重定向”呢?

实现思路一般是:

1、既然panic使用的的是标准输出,我们可以使用自定义的文件file引用取代go里头的os.Stdout 和 os.Stderr

2、引起panic并测试重定向的正确性

3、windows里面没有stdout和stderr的输出方式,也没办法像unix那样使用“>>”进行标准输出的重新向,这个如何破?

我们先试试一个简单的例子:

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()
}

这个例子,我们尝试使用 os.Stdout = fileos.Stderr = file 来“强制”转换,但我们运行程序后,发现不起作用, /tmp/panic.log 没有任何信息流入,panic信息照样输出到标准输出那里。

关于原因,Rob是这样说的:

rob say

看来是把变量直接赋值到底层是不行的,图上所说,推荐使用syscall.Dup的方式。我们再改写下 InitPanicFile() 函数:

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
}

我们运行程序,发现panic正常定向到我们的文件里面去了:

$ tail -f /tmp/panic.log

panic: test panic

goroutine 1 [running]:

... ...
... ...

不过经过实践,上面的代码是有些bug的,原因是我们上面的file是一个局部变量,放系统发生gc的时候,会触发file里面的 runtime.SetFinalizer(f.file, (*file).close), 会引起句柄会被回收, 如果我们代码是长期运行在后台的话,建议代码调整如下的形式:

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
}

接下来,我们要延伸思考下,如果服务是运行在windows上面该如何破?

使用syscall.Dup2的例子windows下会编译直接报错:

undefined: syscall.Dup2

... ...

syscall.Dup2 is a linux/OSX only thing. there's no windows equivalent。

记得我前面的文件,介绍过go调用DLL的方法 《使用Go结合windows dll开发程序》 ,其实我们也可以想到,可以直接使用DLL的调用达到功能效果:

代码如下:

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下运行,panic信息也能正常重定向到指定文件上了。