23

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?

David Frascone
  • 283
  • 1
  • 2
  • 6
  • I think `recover` is better than redirect the output. `debug.PrintStack` can print the logs of you need. Some other logs, for example, the GC message, can still stay in the stderr. – Bryce Jul 29 '16 at 09:44

4 Answers4

27

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
}
Tim Lewis
  • 3,335
  • 1
  • 36
  • 26
Nick Craig-Wood
  • 52,955
  • 12
  • 126
  • 132
  • This worked for me -- panics going to my fd instead of stderr solved my issue. – David Frascone Jan 14 '16 at 18:40
  • I tried this and the unix side worked for me but the windows side did not... any insight? -- https://play.golang.org/p/jQaN1bNypx – Tim Lewis Oct 19 '17 at 13:32
  • I did some more experimenting and on window's you must also do `os.Stderr = f` since `SetStdHandle` does not affect the prior reference to stderr. On unix it is not necessary since the `Dup2` does affect the prior reference to stderr. https://play.golang.org/p/mXjw6lOrG9 – Tim Lewis Oct 19 '17 at 13:53
  • See Jakob's answer down below. If you're on a POSIX-like or BSD-like supported by Go, this will work. I'm currently working with him to see if we can clean up some of the results of this so that logging is more consistent, etc. for gocryptfs. – Svartalf Oct 24 '19 at 22:05
  • 1
    Is there a way to redirect entire stderr to file in windows? I am trying this for all the messages, but it only puts the panic to the file. I am able to redirect all STDOUT but not error. @TimLewis – Sabyasachi Patra Feb 25 '20 at 13:55
  • On Windows I think you can simplify by using: windows.SetStdHandle(windows.STD_ERROR_HANDLE, windows.Handle(f.Fd())) – Andrew McKinlay Feb 18 '22 at 21:25
14

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.

helmbert
  • 35,797
  • 13
  • 82
  • 95
  • 5
    I would note that you can only recover from within your own goroutine. I think OP may be looking for a global "recover", which you can't do. – JimB Jan 13 '16 at 17:02
  • True, indeed. I'll edit that in, thanks for the hint. – helmbert Jan 13 '16 at 17:02
  • 1
    Well, I don't need to stop the panic, I just want the backtrace to appear in my log, not to stderr. I guess I could replace os.Stderr with my logger? Nope -- just checked, even with os.Stderr being my logfile handle, the panic still goes to stderr. – David Frascone Jan 13 '16 at 17:52
  • keep in mind that calling `panic(x)`, even in recovery, will kill your process. if you just want the stack, call `debug.PrintStack()` – blamblambunny Mar 27 '19 at 16:35
  • That's just what I wanted – hunter_tech Apr 26 '20 at 09:31
1

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()
}
Jakob
  • 193
  • 1
  • 5
0

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!

  • This is working fine. Note that if the main goroutine panics, it won't be logged unless you don't close the handle. – hammurabi Aug 02 '23 at 17:51