本文我們介紹生產環境上如何通過捕捉異常recovery
來完善程式設計和提高使用者體驗。
Golang異常處理
golang 的異常處理比較簡單,通常都是在程式遇到異常崩潰
panic
之後通過defer
呼叫延遲函式捕捉異常,並對異常資訊進行輸出和記錄。
- 異常處理程式碼
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
... // 上報異常 或者 傳送告警
}
}()
通過Gin中介軟體捕捉異常
- 內建中介軟體
gin在
gin.Default
中就使用了自帶的Recovery
函式,將狀態碼置為500並輸出錯誤資訊到終端
func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter)
}
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
func RecoveryWithWriter(out io.Writer) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
if se, ok := ne.Err.(*os.SyscallError); ok {
if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
brokenPipe = true
}
}
}
if logger != nil {
stack := stack(3)
httpRequest, _ := httputil.DumpRequest(c.Request, false)
headers := strings.Split(string(httpRequest), "\r\n")
for idx, header := range headers {
current := strings.Split(header, ":")
if current[0] == "Authorization" {
headers[idx] = current[0] + ": *"
}
}
if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httpRequest), reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset)
}
}
// If the connection is dead, we can't write a status to it.
if brokenPipe {
c.Error(err.(error)) // nolint: errcheck
c.Abort()
} else {
c.AbortWithStatus(http.StatusInternalServerError)
}
}
}()
c.Next()
}
}
- 自定義中介軟體
內建的
Recovery
函式功能比較簡單,我們需要重新開發自定義的異常處理中介軟體。
在Gin-IPs專案中我們將堆疊資訊上報到Redis,方便監控。同時,我們還通過traceId
對該請求進行簡單的鏈路跟蹤,可以方便定位到請求日誌。
// 日誌列印沒必要非同步處理,一般crash比較少
func Recovery() gin.HandlerFunc {
log, _ := mylog.New(
configure.GinConfigValue.ErrorLog.Path, configure.GinConfigValue.ErrorLog.Name,
configure.GinConfigValue.ErrorLog.Level, nil, configure.GinConfigValue.ErrorLog.Count)
log.Info("Test Panic")
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
response := route_response.Response{}
response.Data.List = []interface{}{} // 初始化為空切片,而不是空引用
traceId := c.Writer.Header().Get("X-Request-Trace-Id")
stackMsg := string(debug.Stack())
logField := map[string]interface{}{
"trace_id": traceId, // 鑑權之後可以得到唯一跟蹤ID和使用者名稱
"user": c.Writer.Header().Get("X-Request-User"),
"uri": c.Request.URL.Path,
"remote_addr": c.ClientIP(),
"stack": stackMsg, // 列印堆疊資訊
}
c.Abort()
response.Code, response.Message = configure.ApiInnerResponseError, fmt.Sprintf("Api內部報錯,請聯絡管理員(id=%s", traceId)
log.WithFields(logField).Error(err) // 輸出panic 資訊
redisField := make(map[string]interface{})
for k, v := range logField {
redisField[k] = v
}
redisField["time"] = time.Now().Format("2006-01-02 15:04:05")
redisField["error"] = err
dao.ModelClient.RedisClient.HMSet(traceId, redisField) // 上報redis
c.JSON(http.StatusUnauthorized, response)
return
}
}()
c.Next()
}
}
- redis查詢異常
10.2.147.167:11700[1]> keys *
1) "445ffc1bb864000"
2) "445ff1b25864000"
10.2.147.167:11700[1]> hgetall 445ffc1bb864000
1) "time"
2) "2020-09-03 16:42:46"
3) "error"
4) "this is test panic"
5) "user"
6) "xiaoming"
7) "remote_addr"
8) "127.0.0.1"
9) "uri"
10) "/"
11) "stack"
12) "goroutine 274...." # ...省略
至此,我們將異常捕捉模組也完成了,這其中不僅涉及到異常處理,還簡單的完成了程式內部請求鏈路跟蹤,異常資訊落地到Redis也為日後的運維監控做好準備。
Github 程式碼
請訪問 Gin-IPs 或者搜尋 Gin-IPs