Golang如何在崩溃前存储日志

Golang如何在崩溃前存储日志 你好,

我正在开发一个应用程序,需要妥善处理错误以避免程序崩溃。为了帮助调试和解决可能出现的问题,我希望将发生的任何错误的堆栈跟踪存储到日志文件中。我尝试在 main 函数中添加一个 defer 函数来获取堆栈跟踪,但没有成功。我包含了一个示例代码片段,该代码由于未处理的 panic 导致了堆栈溢出错误。如何修改此代码才能将错误的堆栈跟踪存储到日志文件中?

package main

import (
	"io"
	"runtime"

	"github.com/rs/zerolog"
	"gopkg.in/natefinch/lumberjack.v2"
)

func main() {
	ljLogger := &lumberjack.Logger{
		Filename:   "/var/log/myapp.log",
		MaxSize:    10, // 每个日志文件的最大大小(MB)
		MaxBackups: 5,  // 保留的旧日志文件最大数量
		MaxAge:     7,  // 保留日志文件的最大天数
	}

	// 创建一个同时写入文件和缓冲区的多路写入器
	mw := io.MultiWriter(ljLogger)
	logger := zerolog.New(mw).With().Timestamp().Logger()
	defer func() {
		if r := recover(); r != nil {
			// 记录错误信息和堆栈跟踪
			stackTrace := make([]byte, 4096)
			length := runtime.Stack(stackTrace, false)
			logger.Error().Interface("panic_value", r).Bytes("stacktrace", stackTrace[:length]).Msg("Unhandled panic occurred")
		}
	}()

	abcd()
}

func abcd() {
	defer func() {
		if r := recover(); r != nil {
			abcd()
		}
	}()
	panic("something went wrong")
}

更多关于Golang如何在崩溃前存储日志的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

我的实际问题:

我使用 Golang 的 Web 应用程序创建了一个 MSI 安装包。但这个应用程序因未知错误而崩溃,我无法看到或重现该问题。我知道这是由于未处理的错误导致的。但我找不到导致问题的确切位置。为了修复或处理该错误,我需要查看堆栈跟踪。

在上面的代码中,我递归调用了 abcd() 来使程序崩溃。

func main() {
    fmt.Println("hello world")
}

更多关于Golang如何在崩溃前存储日志的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


正如我所说,从第一个错误处理程序中递归调用 abcd() 只会导致栈溢出,这是无法恢复的。

我稍微简化了你的示例代码,链接的 Playground 对我来说可以正常工作,并将堆栈跟踪打印到终端,同时也打印了长度(我添加这个是为了验证它确实是自定义处理程序的行为,而不是默认行为)。

Go Playground

Go Playground - Go 编程语言

不过,与其预初始化一个长度为 4k 的缓冲区,我可能更倾向于使用一个长度为 0、容量为 4k 的缓冲区(make([]byte, 0, 4096))。

此外,通过在输出中添加 r,你可以包含原始错误消息,正如你在 Go Playground - Go 编程语言 中看到的那样。

我认为最可靠的处理方式是通过你的监控程序/初始化系统/系统日志记录器,来收集并将你所有程序的输出写入一个集中化的日志文件。


你这里的问题是:

你在 abcd() 中发生了 panic,而它会试图通过调用 abcd()recover() 自身,这将导致无限递归,最终耗尽你的栈空间并产生一个你无法 recover 的错误,因为 Go 运行时在栈溢出时会强制终止你的程序,而不给你从中恢复的机会。

在我看来,一个记录 panic 的集中化处理程序是可以接受的,尽管它不应该依赖于日志记录器(因为可能正是它崩溃了),而只应该依赖于“原始”操作,并且它也不应该依赖除标准输出/错误流之外的任何东西,因为这些可能已经不可用了。你永远不知道是什么崩溃了。

除了这个用于获取一些日志的集中化 panic 处理程序之外,你真的完全不应该使用 recover()!处理你得到的任何 error 返回值,如果你调用的某个函数在不返回错误的情况下使用了 panic,那就修改它进行正确的错误处理,或者向上游提交一个 bug 报告!

func main() {
    fmt.Println("hello world")
}

在Go中捕获panic并记录堆栈跟踪的正确方法如下:

package main

import (
	"io"
	"os"
	"runtime/debug"
	"time"

	"github.com/rs/zerolog"
	"gopkg.in/natefinch/lumberjack.v2"
)

func main() {
	// 设置日志轮转
	ljLogger := &lumberjack.Logger{
		Filename:   "/var/log/myapp.log",
		MaxSize:    10,
		MaxBackups: 5,
		MaxAge:     7,
		Compress:   true,
	}

	// 创建多路写入器(同时输出到文件和终端)
	mw := io.MultiWriter(ljLogger, os.Stdout)
	
	// 创建zerolog实例
	logger := zerolog.New(mw).
		With().
		Timestamp().
		Logger()

	// 设置全局panic处理
	defer handlePanic(&logger)

	// 你的应用逻辑
	abcd()
}

func handlePanic(logger *zerolog.Logger) {
	if r := recover(); r != nil {
		// 获取完整的堆栈跟踪
		stack := debug.Stack()
		
		// 记录panic信息
		logger.Error().
			Time("panic_time", time.Now()).
			Interface("panic_value", r).
			Str("stacktrace", string(stack)).
			Msg("Unhandled panic occurred")
		
		// 可以选择重新panic或优雅退出
		panic(r) // 重新抛出panic以保持原始行为
	}
}

func abcd() {
	defer func() {
		if r := recover(); r != nil {
			// 这里不应该递归调用,会导致堆栈溢出
			// 应该记录错误并退出或重新panic
			panic(r)
		}
	}()
	panic("something went wrong")
}

更健壮的实现可以使用自定义的panic处理器:

package main

import (
	"fmt"
	"io"
	"os"
	"runtime/debug"
	"time"

	"github.com/rs/zerolog"
	"gopkg.in/natefinch/lumberjack.v2"
)

type PanicHandler struct {
	logger zerolog.Logger
}

func NewPanicHandler(logger zerolog.Logger) *PanicHandler {
	return &PanicHandler{logger: logger}
}

func (ph *PanicHandler) HandlePanic() {
	if r := recover(); r != nil {
		// 获取堆栈信息
		stack := debug.Stack()
		
		// 记录详细信息
		ph.logger.Error().
			Time("timestamp", time.Now()).
			Str("panic", fmt.Sprintf("%v", r)).
			Str("goroutine_stack", string(stack)).
			Msg("PANIC RECOVERED")
		
		// 输出到stderr以便立即查看
		fmt.Fprintf(os.Stderr, "PANIC: %v\n%s\n", r, stack)
		
		// 根据需求选择退出或继续
		os.Exit(1)
	}
}

func (ph *PanicHandler) Run(f func()) {
	defer ph.HandlePanic()
	f()
}

func main() {
	// 配置日志
	ljLogger := &lumberjack.Logger{
		Filename:   "/var/log/myapp.log",
		MaxSize:    10,
		MaxBackups: 5,
		MaxAge:     7,
	}

	mw := io.MultiWriter(ljLogger, os.Stdout)
	logger := zerolog.New(mw).
		With().
		Timestamp().
		Logger()

	// 创建panic处理器
	handler := NewPanicHandler(logger)
	
	// 运行应用
	handler.Run(func() {
		abcd()
	})
}

func abcd() {
	// 正常的业务逻辑
	panic("simulated panic")
}

对于HTTP服务器,可以这样处理:

package main

import (
	"net/http"
	"runtime/debug"
	
	"github.com/rs/zerolog"
	"gopkg.in/natefinch/lumberjack.v2"
)

func panicMiddleware(logger zerolog.Logger, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				stack := debug.Stack()
				logger.Error().
					Interface("panic", err).
					Str("stack", string(stack)).
					Str("url", r.URL.String()).
					Str("method", r.Method).
					Msg("HTTP panic recovered")
				
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

func main() {
	// 日志配置
	ljLogger := &lumberjack.Logger{
		Filename:   "/var/log/myapp.log",
		MaxSize:    10,
		MaxBackups: 5,
		MaxAge:     7,
	}
	
	logger := zerolog.New(ljLogger).With().Timestamp().Logger()
	
	// 创建带panic恢复的handler
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		panic("HTTP handler panic")
	})
	
	handler := panicMiddleware(logger, mux)
	http.ListenAndServe(":8080", handler)
}

关键点:

  1. 使用debug.Stack()获取完整堆栈跟踪
  2. 在defer函数中处理panic
  3. 避免在panic处理中递归调用导致堆栈溢出
  4. 确保日志在panic后能正确刷新到文件
  5. 对于不同的应用类型(CLI、HTTP服务等)采用适当的panic处理策略
回到顶部