英文:
Capturing panic() in golang
问题
我们有一个相对较大的 Golang 应用程序,使用日志记录器(实际上是自定义的日志记录器)将输出写入定期轮转的日志文件。
然而,当应用程序崩溃或发生 panic() 时,这些消息会被发送到标准错误输出。
有没有办法覆盖 panic 功能,使用我们的日志记录器呢?
英文:
We have a large-ish golang application that uses the logger (actually, a custom logger), to write output to a log file that is periodically rotated.
However, when an application crashes or panic()'s, those messages go to standard error.
Is there any way to override the panic functionality to use our logger?
答案1
得分: 27
据我所知,你无法将 panic 的输出重定向到标准错误或日志记录器。你能做的最好的事情是将标准错误重定向到一个文件中,这可以在程序内部或外部完成。
对于我的 rclone 程序,我将标准错误重定向到一个文件中,以便捕获所有内容。不幸的是,这在跨平台的方式下并不容易实现。以下是我是如何做到的(参见 redirect*.go 文件):
对于 Linux/Unix:
// 在 Unix 下将 panic 记录到日志文件中
//+build unix
package main
import (
"log"
"os"
"syscall"
)
// 将标准错误重定向到传入的文件
func redirectStderr(f *os.File) {
err := syscall.Dup2(int(f.Fd()), int(os.Stderr.Fd()))
if err != nil {
log.Fatalf("将标准错误重定向到文件失败:%v", err)
}
}
对于 Windows:
// 在 Windows 下将 panic 记录到日志文件中
//
// 代码来自 minix,通过
//
// 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
}
// 将标准错误重定向到传入的文件
func redirectStderr(f *os.File) {
err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(f.Fd()))
if err != nil {
log.Fatalf("将标准错误重定向到文件失败:%v", err)
}
// SetStdHandle 不会影响之前对 stderr 的引用
os.Stderr = f
}
希望对你有所帮助!
英文:
As far as I know, you can't redirect the output from panic away from standard error, or to your logger. The best thing you can do is redirect standard error to a file which you can do externally, or inside your program.
For my rclone program I redirected standard error to capture everything to a file on an option which is unfortunately isn't particularly easy to do in a cross platform way. Here is how I did it (see the redirect*.go files)
For 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)
}
}
and for 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
}
答案2
得分: 14
你可以使用recover()
来从同一个 goroutine 中恢复 panic。当在延迟方法中调用recover()
(请记住,即使在panic()
时,延迟方法仍然会被调用),它将返回作为参数传递给最后一个panic()
调用的内容(或者在程序没有发生 panic 时返回nil
)。
defer func() {
if x := recover(); x != nil {
// 从 panic 中恢复;x 包含传递给 panic() 的内容
log.Printf("运行时 panic:%v", x)
// 如果只想记录 panic,可以再次触发 panic
panic(x)
}
}()
panic("foo")
注意,你无法从在不同 goroutine 中触发的 panic 中恢复(感谢 JimB 的提示)。无法使用单个recover()
来从任何 goroutine 的 panic 中恢复。
英文:
You can use recover()
to recover panics from the same goroutine. When calling recover()
in a deferred method (remember that deferred methods will still be called, even when panic()
ing), it will return whatever was passed into the last panic()
call as argument (or nil
when the program is not panicking).
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");
Note however, that you cannot recover from panics that were triggered in a different goroutine (thanks to JimB for the hint). Using a single recover()
to recover from panics from any goroutine is not possible.
答案3
得分: 1
扩展@nick-craig-wood的答案:
如果你使用的是Linux系统,你可以生成一个logger(1)实例,并将stderr重定向到它。这样,你就可以将完整的回溯信息记录到syslog中。这就是gocryptfs所做的:
// redirectStdFds函数将stderr和stdout重定向到syslog,将stdin重定向到/dev/null
func redirectStdFds() {
// stderr和stdout
pr, pw, err := os.Pipe()
if err != nil {
tlog.Warn.Printf("redirectStdFds: 无法创建管道:%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: 无法启动logger:%v\n", err)
}
pr.Close()
err = syscall.Dup2(int(pw.Fd()), 1)
if err != nil {
tlog.Warn.Printf("redirectStdFds: stdout复制错误:%v\n", err)
}
syscall.Dup2(int(pw.Fd()), 2)
if err != nil {
tlog.Warn.Printf("redirectStdFds: stderr复制错误:%v\n", err)
}
pw.Close()
// stdin
nullFd, err := os.Open("/dev/null")
if err != nil {
tlog.Warn.Printf("redirectStdFds: 无法打开/dev/null:%v\n", err)
return
}
err = syscall.Dup2(int(nullFd.Fd()), 0)
if err != nil {
tlog.Warn.Printf("redirectStdFds: stdin复制错误:%v\n", err)
}
nullFd.Close()
}
英文:
Expanding @nick-craig-wood's answer:
If you are on Linux you can spawn a logger(1) instance and redirect stderr to it. That way you get the full backtrace into syslog. This is what gocryptfs does:
// 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()
}
答案4
得分: 0
只是更新到2023年。syscall.SysCall
已被弃用,所以我正在寻找在Windows上捕获恐慌的替代方法。Andrew McKinlay的答案对我非常有用。以下是我用来捕获恐慌并发送到日志文件的代码:
func redirectStderr(logPath string) {
logFile, _ := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_SYNC|os.O_APPEND, 0644)
defer logFile.Close()
err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(logFile.Fd()))
if err != nil {
Log.Fatalf("将stderr重定向到文件失败:%v", err)
}
os.Stderr = logFile
}
附言:Rclone是一个很棒的工具!
英文:
Just updating for 2023. syscall.SysCall
is deprecated so I was looking for an alternative for capturing panics on Windows. Andrew McKinlay's answer worked great for me. Here's what I'm using to capture panics and send to a log file
func redirectStderr(logPath string) {
logFile, _ := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_SYNC|os.O_APPEND, 0644)
defer logFile.Close()
err := windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(logFile.Fd()))
if err != nil {
Log.Fatalf("Failed to redirect stderr to file: %v", err)
}
os.Stderr = logFile
}
p.s. Rclone is an awesome tool!
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论