🙅♀️
【Golang】カスタムエラー 後編(エラーログ)
某pomeさんからありがたいサンプルコードをいただいてログについて教えていただきましたので復習のために載せます。
ログレベルについて
ログにはレベルという概念があって、例えば以下である
- Debug
- Info
- Warn
- Error
- Fatal
ログに何を含めるべきか
エラーログの場合
- ログレベル
- ユーザーID
- エラーが発生した時刻(ログの出力時刻)
- エラーコード
- エラーメッセージ
- スタックトレース
例えば、レポジトリでDB処理をしたりその他の処理をしてエラーになり、そのログを出したい場合、いちいちその場でスタックトレースを書くべきではない。
エラーログの出力先が散ってしまうと、trace_idやuse_idなどの情報を含めるために、わざわざ関数やメソッドの引数にそれらを指定する必要がある。
なので、middlewareで一括して出力するなどの手法をとる。
ErrorLog
- middlewareに設定
- handlerでエラーをreturnすることでこのエラーログで一括で出力する。
- trace_idやuser_idもここでセットする。
- insernal server errorのみ警告を出し、それ以外は警告ログを出力する。
middleware/handler.go
package handler
type CustomHandler func(w http.ResponseWriter, r *http.Request) error
func ErrorLog(logger *zap.Logger, next CustomHandler) CostomHandler{
return func(w http.ResponseWriter, r *http.Request) error {
err:= next(w,r)
if err == nil {
return nil
}
rc := getRequestContect(r)
f := []zp.int("user_id", rc.UserID),
zap.string("trace_id", rc.TraceID),
zap.Error(err),
zap.String("stack_trace", apperror.StackTrace(err)),
if apperror.SameKind(err, apperror.KindInternalServerError) {
logger.Error("error_log", f...)
} else {
logger.Warn("error_log", f...)
}
return err
}
}
middleware
- handlerでmiddlewareにセットする関数
- 戻り値がhttp.HandlerFuncになっているので、 設置予定のhttp.HandleFuncの第二引数に設置することができる。
- middlewareは、セットした順番に処理が進む
http.HandleFuncはfunc(ResponseWriter, *Request)を別の型として定義したもの→参考
main.go
package main
func newHandlerWithMiddleware(l *zap.Logger, ch handler.CustomHandler) http.HandlerFunc {
return handler.SetRequestContext(
handler.ErrorLog(
//accessLogはまた後日
l, handler.AccessLog(
l, handler.HandleErrorResponse(
handler.TraceID(ch),
),
),
),
)
}
main関数
main.go
package main
func main() {
http.HandleFunc("/getXxx", newHandlerWithMiddleware(logger, h.GetXxx))
log.Fatal(http.ListenAndServe(":8080", nil))
}
handler
- RequestContextを利用することによって、middlewareでセットしたuser_idなどをhandlerで取り出すことができる。
handler.go
package handler
func (u *User) AddUser(w http.ResponseWriter, r *http.Request) error {
//userID := getRequestContext(r).UserID
//エラーが発生する値を指定
err := usecase.AddUser("nnn", "aaa")
if err != nil {
return apperror.Wrap(err)
}
//↑でエラーが発生するので、ここは通らない。
return nil
}
usecase
usecase.go
- 戻り値はerrだけハンドリングすれば良い。
- モデルから帰ってきたエラーをラップすることで、酢タクトレースが付与される。
package usecase
func AddUser(name, address string) error {
//エラーが発生するので、戻り値は err だけハンドリングすればいい。
_, err := model.NewUser(name, address)
if err != nil {
//モデルから返ってきたエラーをラップ
return apperror.Wrap(err)
}
return nil
}
model
model/user.go
package model
func NewUser(z *zap.Logger) *User {
return &User{z}
}
wrap
apperror/error.go
package apperror
func Wrap(err error) error {
return newError(err, NewMessage(CodeEmpty, ""))
}
error構造体
type Error struct {
details []Detail
err error
frames *runtime.Frames
}
traceID設定
- HTTPリクエストに紐づく値はcontextにセットするのが一般的
middleware/handler.go
package handler
func TraceID(next CustomHandler) CustomHandler {
return func(w http.ResponseWriter, r *http.Request) error {
traceID := uuidを生成
getRequestContext(r).TraceID = traceID
return next(w, r)
}
}
traceIdをcontextに設定
- リクエストに紐づく情報を管理するための構造体
type RequestContext struct {
TraceID string
UserID int
}
func getRequestContext(r *http.Request) *RequestContext {
return r.Context().Value("request_context").(*RequestContext)
}
エラーレスポンスを一括で返すやつ
- これはmiddlewareの中で設定する
- この中にhandlerを渡す(正しくはこの引数(handler.TraceID)の中の引数に渡す)ので、ハンドラではエラーをreturnするだけでおkになっている。
func HandleErrorResponse(next CustomHandler) CustomHandler {
return func(w http.ResponseWriter, r *http.Request) error {
err := next(w, r)
if err == nil {
return nil
}
//apperror.Kind によって http status を設定している。
httpStatus := apperror.GetHTTPStatus(err)
w.WriteHeader(httpStatus)
//エラー用のレスポンスはapperror.Errorが持つCodeから生成する。
m, _ := json.Marshal(NewErrorResponse(apperror.GetCodes(err)))
fmt.Fprint(w, string(m))
return err
}
}
その他の関数
middleware/handler.go
package handler
//rをRequestContextにアサーションする
func getRequestContext(r *http.Request) *RequestContext {
return r.Context().Value("request_context").(*RequestContext)
}
func SameKind(err error, k Kind) bool {
appErr :=ConvertAppError(err)
if appErr == nil {
return false
}
for _, m := range appErr.details {
of m.code.Kind == KindEmpty {
return SameKind(appErr.err, k)
}
if m.code.Kind == k {
return true
}
return false
}
//codeなどをDetail型にする
func NewMessage(c Code, format string, val ...interface{}) Detail {
return Detail{
code: c,
messageForDeveloper: fmt.Sprintf(format, val...),
}
}
Discussion