前言
你是否曾经遇到过 Golang 应用意外重启但没有任何错误日志停留,怀疑出现 panic
但是苦于没有证据?那么今天就来解决一下可能出现的一个问题,捕获哪些可能被我遗漏的 panic 日志。
原因
首先我们要清楚一下出现这样情况的可能原因是什么。(之所以我说可能的原因,是因为不是所有没有日志的情况都是这个原因) 。对于日志的处理方式,通常我们会使用一些日志框架帮助我们进行处理,特别是对于文件,将日志打印到文件中的情况。而当一个 panic 出现的时候,如果我们没有捕获它,它的错误信息只会被打印到 stderr
中去,也就是标准错误输出。
所以,对于一些容器化部署的项目来说,特别是部署到 k8s 上的时候,会统一使用 std 作为日志捕获和处理的手段,无论是采用 sidecar 还是其他方式采集,将 std 的输出直接采集发送到日志系统,所有日志系统进行归档统一处理就一般没啥问题。而对于一些仅使用二进制或者是 docker 部署的情况,又或是没有采用日志采集 std 的时候就可能会出现了。
实验
这里我使用 zap 作为日志组件引入,如果你不熟悉可以直接忽略这部分的代码。然后快速实验一下,对于日志文件是否有 panic 记录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package main
import ( "fmt" rotatelogs "github.com/lestrrat-go/file-rotatelogs" "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" "runtime" "syscall" "time" )
func main() { logger := initLogger() logger.Debug("debug") logger.Info("info") logger.Warn("warn") logger.Error("error")
go func() { panic("exit") }()
time.Sleep(time.Second * 3) }
func initLogger() *zap.Logger { errWriter, err := rotatelogs.New( "linkinstar_err_%Y-%m-%d.log", ) if err != nil { panic(err) }
consoleDebugging := zapcore.Lock(os.Stdout) consoleEncoderConfig := zap.NewDevelopmentEncoderConfig() consoleEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder consoleEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder consoleEncoder := zapcore.NewConsoleEncoder(consoleEncoderConfig) consoleCore := zapcore.NewCore(consoleEncoder, consoleDebugging, zapcore.DebugLevel)
errorCore := zapcore.AddSync(errWriter) fileEncodeConfig := zap.NewProductionEncoderConfig() fileEncodeConfig.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewConsoleEncoder(fileEncodeConfig) lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.DebugLevel })
core := zapcore.NewTee(consoleCore, zapcore.NewCore(fileEncoder, errorCore, lowPriority)) caller := zap.AddCaller() logger := zap.New(core, caller, zap.Development()) zap.ReplaceGlobals(logger) return logger }
|
可以看到日志文件里面输出的是:
1 2 3 4
| 2024-12-10T15:56:15.505+0800 debug main.go:15 debug 2024-12-10T15:56:15.506+0800 info main.go:16 info 2024-12-10T15:56:15.506+0800 warn main.go:17 warn 2024-12-10T15:56:15.506+0800 error main.go:18 error
|
并没有 panic 日志。而在实际项目中,如果有第三方的依赖意外导致了 panic 那么就很有可能是这样的情况,容器重启,但没有任何现场日志保留。
解决方案
说起来很简单,也是网上来的方案,非常行之有效,就是将 stderr 重写到你需要的文件里面。即下面的 RewriteStderrFile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| package main
import ( "fmt" rotatelogs "github.com/lestrrat-go/file-rotatelogs" "go.uber.org/zap" "go.uber.org/zap/zapcore" "log" "os" "runtime" "syscall" "time" )
func main() { RewriteStderrFile("linkinstar_panic.log")
logger := initLogger() logger.Debug("debug") logger.Info("info") logger.Warn("warn") logger.Error("error")
log.Println("print to std") fmt.Fprintf(os.Stderr, "print to stderr\n")
go func() { panic("exit") }()
time.Sleep(time.Second * 3) }
func initLogger() *zap.Logger { errWriter, err := rotatelogs.New( "linkinstar_err_%Y-%m-%d.log", ) if err != nil { panic(err) }
consoleDebugging := zapcore.Lock(os.Stdout) consoleEncoderConfig := zap.NewDevelopmentEncoderConfig() consoleEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder consoleEncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder consoleEncoder := zapcore.NewConsoleEncoder(consoleEncoderConfig) consoleCore := zapcore.NewCore(consoleEncoder, consoleDebugging, zapcore.DebugLevel)
errorCore := zapcore.AddSync(errWriter) fileEncodeConfig := zap.NewProductionEncoderConfig() fileEncodeConfig.EncodeTime = zapcore.ISO8601TimeEncoder fileEncoder := zapcore.NewConsoleEncoder(fileEncodeConfig) lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.DebugLevel })
core := zapcore.NewTee(consoleCore, zapcore.NewCore(fileEncoder, errorCore, lowPriority)) caller := zap.AddCaller() logger := zap.New(core, caller, zap.Development()) zap.ReplaceGlobals(logger) return logger }
var stdErrFileHandler *os.File
func RewriteStderrFile(panicFilePath string) error { if runtime.GOOS == "windows" { return nil }
file, err := os.OpenFile(panicFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { fmt.Println(err) return err } stdErrFileHandler = file if err = syscall.Dup2(int(file.Fd()), int(os.Stderr.Fd())); err != nil { fmt.Println(err) return err } runtime.SetFinalizer(stdErrFileHandler, func(fd *os.File) { fd.Close() }) return nil }
|
这样我们就会额外得到一个 panic.log
1 2 3 4 5 6 7 8 9
| 2024/12/10 16:01:03 print to std print to stderr panic: exit
goroutine 35 [running]: main.main.func1() xxx/main.go:28 +0x2c created by main.main in goroutine 1 xxx/main.go:27 +0x138
|
方案本质是利用 syscall.Dup2
方法重定向输出。注意 Windows 上没有这个方法,文末链接中有解决方案,我手边暂时没有设备可测。
其他提醒
以上的解决方案是一个保底,在一些无法预知情况下帮你兜底。而实际中的最佳实践中你依旧需要保证,在可能出现 panic 的位置提前主动 recover 进行处理。
参考链接