🙅‍♀️

【Golang】カスタムエラー 後編(エラーログ)

2022/07/31に公開

某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