// Package crashlog 提供详细的崩溃日志、panic 恢复和 goroutine 保护工具。 // 在开发阶段用于快速定位崩溃点。 package crashlog import ( "fmt" "log" "net/http" "os" "runtime" "runtime/debug" "time" ) // Go 在单独的 goroutine 中运行 fn,自动捕获 panic 并记录完整堆栈。 // 返回一个 channel,在 goroutine 退出时关闭。 // 用法: go crashlog.Go("thinker-light", func() { ... }) func Go(name string, fn func()) { go func() { defer Recover(name) fn() }() } // Recover 用于 defer 语句中,捕获 panic 并记录完整调用栈。 // 用法: defer crashlog.Recover("thinker-deep") func Recover(name string) { if r := recover(); r != nil { stack := debug.Stack() log.Printf("[CRASH] goroutine=%s panic=%v\n%s", name, r, string(stack)) // 写独立崩溃日志文件,方便事后排查 writeCrashFile(name, fmt.Sprintf("panic: %v\n\n%s", r, string(stack))) } } // RecoverNoop is a no-op for production hot paths. func RecoverNoop(name string) {} // WrapHTTP 返回一个 HTTP 中间件,自动捕获 handler 中的 panic。 // 用法: http.Handle("/api", crashlog.WrapHTTP(handler)) func WrapHTTP(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { stack := debug.Stack() log.Printf("[CRASH] http-panic url=%s method=%s panic=%v\n%s", r.URL.String(), r.Method, rec, string(stack)) writeCrashFile("http-"+sanitize(r.URL.String()), fmt.Sprintf("panic: %v\n\n%s", rec, string(stack))) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } // WrapHTTPFunc 返回一个 HTTP handler 函数中间件。 func WrapHTTPFunc(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { stack := debug.Stack() log.Printf("[CRASH] http-panic url=%s method=%s panic=%v\n%s", r.URL.String(), r.Method, rec, string(stack)) writeCrashFile("http-"+sanitize(r.URL.String()), fmt.Sprintf("panic: %v\n\n%s", rec, string(stack))) http.Error(w, "Internal Server Error", http.StatusInternalServerError) } }() fn(w, r) } } // LLMCall 记录 LLM API 调用的开始时间,返回一个结束函数。 // 用法: // // defer crashlog.LLMCall("deep-think", model)(&err, &responseLen) func LLMCall(caller, model string) func(err *error, responseBytes *int) { start := time.Now() return func(err *error, responseBytes *int) { elapsed := time.Since(start) level := "OK" errMsg := "" if err != nil && *err != nil { level = "FAIL" errMsg = (*err).Error() } respLen := 0 if responseBytes != nil { respLen = *responseBytes } if elapsed > 10*time.Second { level = "SLOW(" + level + ")" } log.Printf("[LLM] caller=%s model=%s elapsed=%v result=%s resp_len=%d err=%s", caller, model, elapsed.Round(time.Millisecond), level, respLen, errMsg) } } // ── internal helpers ── func writeCrashFile(name string, content string) { // 写到 logs/ 目录,持久化保留 os.MkdirAll("logs", 0755) timestamp := time.Now().Format("20060102_150405") filename := fmt.Sprintf("logs/crash_%s_%s.log", sanitize(name), timestamp) f, err := os.Create(filename) if err != nil { log.Printf("[CRASH] 无法写入崩溃日志文件 %s: %v", filename, err) return } defer f.Close() fmt.Fprintf(f, "=== CRASH REPORT ===\n") fmt.Fprintf(f, "Time: %s\n", time.Now().Format(time.RFC3339)) fmt.Fprintf(f, "Goroutine: %s\n", name) fmt.Fprintf(f, "Go Version: %s\n", runtime.Version()) fmt.Fprintf(f, "GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) fmt.Fprintf(f, "NumGoroutine: %d\n", runtime.NumGoroutine()) fmt.Fprintf(f, "\n%s", content) log.Printf("[CRASH] 崩溃日志已写入 %s", filename) } func sanitize(s string) string { result := make([]byte, 0, len(s)) for i := 0; i < len(s) && i < 100; i++ { c := s[i] if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' { result = append(result, c) } else { result = append(result, '_') } } return string(result) }