Skip to content

Commit 66cac93

Browse files
committed
feat: recovery notify stack info
1 parent ca1648c commit 66cac93

2 files changed

Lines changed: 136 additions & 7 deletions

File tree

core/main.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,11 @@ func startSyncServices(ctx context.Context, wg *sync.WaitGroup) {
135135
go model.SyncModelConfigAndChannelCache(ctx, wg, time.Second*10)
136136
}
137137

138-
func ginRecoveryHandler(c *gin.Context, err any) {
139-
notify.ErrorThrottle("ginRecoveryHandler", time.Minute, "Panic Detected", fmt.Sprintf("%+v", err))
140-
c.AbortWithStatus(http.StatusInternalServerError)
141-
}
142-
143138
func setupHTTPServer() (*http.Server, *gin.Engine) {
144139
server := gin.New()
145140

146-
w := log.StandardLogger().Writer()
147141
server.
148-
Use(gin.RecoveryWithWriter(w, ginRecoveryHandler)).
142+
Use(middleware.GinRecoveryHandler).
149143
Use(middleware.NewLog(log.StandardLogger())).
150144
Use(middleware.RequestIDMiddleware, middleware.CORS())
151145
router.SetRouter(server)

core/middleware/recover.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package middleware
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"net"
8+
"net/http"
9+
"net/http/httputil"
10+
"os"
11+
"runtime"
12+
"strings"
13+
"time"
14+
15+
"github.com/gin-gonic/gin"
16+
"github.com/labring/aiproxy/core/common/notify"
17+
)
18+
19+
func GinRecoveryHandler(c *gin.Context) {
20+
defer func() {
21+
if err := recover(); err != nil {
22+
// Check for a broken connection, as it is not really a
23+
// condition that warrants a panic stack trace.
24+
var brokenPipe bool
25+
if ne, ok := err.(*net.OpError); ok {
26+
var se *os.SyscallError
27+
if errors.As(ne, &se) {
28+
seStr := strings.ToLower(se.Error())
29+
if strings.Contains(seStr, "broken pipe") ||
30+
strings.Contains(seStr, "connection reset by peer") {
31+
brokenPipe = true
32+
}
33+
}
34+
}
35+
fileLine, stack := stack(5)
36+
httpRequest, _ := httputil.DumpRequest(c.Request, false)
37+
headers := strings.Split(string(httpRequest), "\r\n")
38+
for idx, header := range headers {
39+
current := strings.Split(header, ":")
40+
if current[0] == "Authorization" {
41+
headers[idx] = current[0] + ": *"
42+
}
43+
}
44+
headersToStr := strings.Join(headers, "\r\n")
45+
if brokenPipe {
46+
notify.ErrorThrottle("ginPanicRecovery:"+fileLine, time.Minute, "Panic Detected", fmt.Sprintf("%s\n%s", err, headersToStr))
47+
} else if gin.IsDebugging() {
48+
notify.ErrorThrottle("ginPanicRecovery:"+fileLine, time.Minute, "Panic Detected", fmt.Sprintf("[Recovery] panic recovered:\n%s\n%s\n%s",
49+
headersToStr, err, stack))
50+
} else {
51+
notify.ErrorThrottle("ginPanicRecovery:"+fileLine, time.Minute, "Panic Detected", fmt.Sprintf("[Recovery] panic recovered:\n%s\n%s",
52+
err, stack))
53+
}
54+
if brokenPipe {
55+
// If the connection is dead, we can't write a status to it.
56+
c.Error(err.(error)) //nolint: errcheck
57+
c.Abort()
58+
} else {
59+
c.AbortWithStatus(http.StatusInternalServerError)
60+
}
61+
}
62+
}()
63+
c.Next()
64+
}
65+
66+
// stack returns a nicely formatted stack frame, skipping skip frames.
67+
func stack(skip int) (fileLine string, stack []byte) {
68+
buf := new(bytes.Buffer) // the returned data
69+
// As we loop, we open files and read them. These variables record the currently
70+
// loaded file.
71+
var lines [][]byte
72+
var lastFile string
73+
for i := skip; ; i++ { // Skip the expected number of frames
74+
pc, file, line, ok := runtime.Caller(i)
75+
if !ok {
76+
break
77+
}
78+
// Print this much at least. If we can't find the source, it won't show.
79+
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
80+
if fileLine == "" {
81+
fileLine = fmt.Sprintf("%s:%d", file, line)
82+
}
83+
if file != lastFile {
84+
data, err := os.ReadFile(file)
85+
if err != nil {
86+
continue
87+
}
88+
lines = bytes.Split(data, []byte{'\n'})
89+
lastFile = file
90+
}
91+
fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line))
92+
}
93+
return fileLine, buf.Bytes()
94+
}
95+
96+
// source returns a space-trimmed slice of the n'th line.
97+
func source(lines [][]byte, n int) []byte {
98+
n-- // in stack trace, lines are 1-indexed but our array is 0-indexed
99+
if n < 0 || n >= len(lines) {
100+
return dunno
101+
}
102+
return bytes.TrimSpace(lines[n])
103+
}
104+
105+
var (
106+
dunno = []byte("???")
107+
centerDot = []byte("·")
108+
dot = []byte(".")
109+
slash = []byte("/")
110+
)
111+
112+
// function returns, if possible, the name of the function containing the PC.
113+
func function(pc uintptr) []byte {
114+
fn := runtime.FuncForPC(pc)
115+
if fn == nil {
116+
return dunno
117+
}
118+
name := []byte(fn.Name())
119+
// The name includes the path name to the package, which is unnecessary
120+
// since the file name is already included. Plus, it has center dots.
121+
// That is, we see
122+
// runtime/debug.*T·ptrmethod
123+
// and want
124+
// *T.ptrmethod
125+
// Also the package path might contain dot (e.g. code.google.com/...),
126+
// so first eliminate the path prefix
127+
if lastSlash := bytes.LastIndex(name, slash); lastSlash >= 0 {
128+
name = name[lastSlash+1:]
129+
}
130+
if period := bytes.Index(name, dot); period >= 0 {
131+
name = name[period+1:]
132+
}
133+
name = bytes.ReplaceAll(name, centerDot, dot)
134+
return name
135+
}

0 commit comments

Comments
 (0)