web-dev-qa-db-ja.com

golangでpanic()をキャプチャする

ロガー(実際にはカスタムロガー)を使用して、定期的にローテーションされるログファイルに出力を書き込む、大規模なgolangアプリケーションがあります。

ただし、アプリケーションがクラッシュまたはpanic()すると、それらのメッセージは標準エラーになります。

ロガーを使用するためにパニック機能をオーバーライドする方法はありますか?

19
David Frascone

私の知る限り、パニックからの出力を標準エラーからリダイレクトしたり、ロガーにリダイレクトしたりすることはできません。あなたができる最善のことは、標準エラーを外部またはプログラム内部で実行できるファイルにリダイレクトすることです。

私の rclone プログラムの場合、残念ながらクロスプラットフォームの方法で行うのは特に簡単ではないオプションのすべてにファイルをキャプチャするために標準エラーをリダイレクトしました。ここに私がそれをした方法があります(redirect * .goファイルを見てください)

Linux/unixの場合

// Log the panic under unix to the log file

//+build unix

package main

import (
    "log"
    "os"
    "syscall"
)

// redirectStderr to the file passed in
func redirectStderr(f *os.File) {
    err := syscall.Dup2(int(f.Fd()), int(os.Stderr.Fd()))
    if err != nil {
        log.Fatalf("Failed to redirect stderr to file: %v", err)
    }
}

そしてWindows用

// Log the panic under windows to the log file
//
// Code from minix, via
//
// http://play.golang.org/p/kLtct7lSUg

//+build windows

package main

import (
    "log"
    "os"
    "syscall"
)

var (
    kernel32         = syscall.MustLoadDLL("kernel32.dll")
    procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
)

func setStdHandle(stdhandle int32, handle syscall.Handle) error {
    r0, _, e1 := syscall.Syscall(procSetStdHandle.Addr(), 2, uintptr(stdhandle), uintptr(handle), 0)
    if r0 == 0 {
        if e1 != 0 {
            return error(e1)
        }
        return syscall.EINVAL
    }
    return nil
}

// redirectStderr to the file passed in
func redirectStderr(f *os.File) {
    err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(f.Fd()))
    if err != nil {
        log.Fatalf("Failed to redirect stderr to file: %v", err)
    }
    // SetStdHandle does not affect prior references to stderr
    os.Stderr = f
}
12
Nick Craig-Wood

recover() を使用して、同じゴルーチンからパニックを回復できます。遅延メソッドでrecover()を呼び出す場合(panic() ingであっても、遅延メソッドは引き続き呼び出されることに注意してください)、最後のpanic()に渡されたものを返します引数として呼び出す(またはプログラムがパニック状態でない場合はnil)。

_defer func() {
    if x := recover(); x != nil {
        // recovering from a panic; x contains whatever was passed to panic()
        log.Printf("run time panic: %v", x)

        // if you just want to log the panic, panic again
        panic(x)
    }
}()

panic("foo");
_

ただし注意、別のゴルーチンでトリガーされたパニックから回復することはできません(ヒントについてはJimBに感謝します)。単一のrecover()を使用して、ゴルーチンからパニックから回復することはできません。

7
helmbert

@ nick-craig-woodの答えを展開する:Linuxを使用している場合は、logger(1)インスタンスを生成して、stderrをそれにリダイレクトできます。そうすることで、syslogに完全なバックトレースを取得できます。これが gocryptfs の機能です。

// redirectStdFds redirects stderr and stdout to syslog; stdin to /dev/null
func redirectStdFds() {
    // stderr and stdout
    pr, pw, err := os.Pipe()
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: could not create pipe: %v\n", err)
        return
    }
    tag := fmt.Sprintf("gocryptfs-%d-logger", os.Getpid())
    cmd := exec.Command("logger", "-t", tag)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Stdin = pr
    err = cmd.Start()
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: could not start logger: %v\n", err)
    }
    pr.Close()
    err = syscall.Dup2(int(pw.Fd()), 1)
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: stdout dup error: %v\n", err)
    }
    syscall.Dup2(int(pw.Fd()), 2)
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: stderr dup error: %v\n", err)
    }
    pw.Close()

    // stdin
    nullFd, err := os.Open("/dev/null")
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: could not open /dev/null: %v\n", err)
        return
    }
    err = syscall.Dup2(int(nullFd.Fd()), 0)
    if err != nil {
        tlog.Warn.Printf("redirectStdFds: stdin dup error: %v\n", err)
    }
    nullFd.Close()
}
1
Jakob