Upgrade to Pro — share decks privately, control downloads, hide ads and more …

マイクロサービスで共用するprivateなエラー&ロギングパッケージを作った話

Shohei03
December 04, 2019

 マイクロサービスで共用するprivateなエラー&ロギングパッケージを作った話

複数のマイクロサービスを3人で開発しており、属人化している状況から脱出する第一歩として、エラーとロギングについて、内容の充実と形式の共通化を行いました。その手段としてprivateなパッケージを初めて作り、導入しました。

Shohei03

December 04, 2019
Tweet

More Decks by Shohei03

Other Decks in Programming

Transcript

  1. ©2019GVA TECH Co., K. K. Twitter: @PGShohei エンジニア歴 1年 Gopher,

    Rider, GVA TECH株式会社 自己紹介 Shohei Oyagi
  2. ©2019GVA TECH Co., K. K. もはや実装者にも意味不明 実際のエラー error: on the

    end point: /api/v1.0/contracts/, services.ExtractContractDocxWithAPI() returns error: Contract.UnmarshalJSON() returns error, original response body as string: "Internal Server Error": json: cannot unmarshal string into Go value of type struct { *models.Alias }
  3. ©2019GVA TECH Co., K. K. - マイクロサービスで、担当者が異なるのでエラーのラップの仕方、情報の含め方がバラバラ - ex. ハンドラ書いたり、パラメータ入れたり、呼び出し元の関数名で何層もラップし続

    けたり、etc... - →エラーが起きたら担当者に確認するか、ソースコードを一から読むか - 原因究明に必要な情報が揃っていない - ex1. エラーの原因が自APIか他APIなのかは、実装者にしか見分けられない - ex2. エラーを起こしたAPI「A」と、そのレスポンスを受けたAPI「B」でそれぞれエラ ーログが出るが、それらの対応関係が釈然としない - ex3. エラーレベルが分からないから緊急度が分からない - 起きたエラーの種類によって呼び出し元での処理を切り分けられていない - ex. 401エラーだけだと再ログイン促すべきか閉め出すべきか見分けられない 正式版リリース前に改善する 現状のエラーが苦しい理由
  4. ©2019GVA TECH Co., K. K. エラー&ログを改善するポイント - 全APIでの統一性→属人化をやめる - 必要な情報を実装者に含めさせる→迅速な原因究明

    - ログにSeverityを含め、形式も検索可能に - エラーの種類で呼び出し元の処理を切り替え可能に
  5. ©2019GVA TECH Co., K. K. エラー構造体 - 要素指定し、情報強制 - idでAPIを跨いだエラーも追跡

    - ErrorKindでエラーの種類を表現 type ExtendError struct { // Controller層で入れる HandlerName string // エラー発生箇所以外で入力しても良い Severity zapcore.Level ContextParams map[string]string // エラー発生箇所で入力する id uuid.UUID Err error Kind ErrorKind OccurFuncName string CauseAPIName string ExpectedCause string }
  6. ©2019GVA TECH Co., K. K. エラー構造体 - 数字ではなく文字列 - →見ただけで意味が分かる

    - constの追加でエラーの種類を 拡張できる type ErrorKind string const ( // 呼び出し元orユーザー起因のエラー KindBadParameter ErrorKind = "bad_params" KindUnauthorized ErrorKind = "unauthorized" KindTokenExpired ErrorKind = "token_expired" KindForbidden ErrorKind = "forbidden" KindNotFound ErrorKind = "not_found" // 自API起因のエラー KindLoggingError ErrorKind = "logging_error" KindInternalServerError ErrorKind = "internal_error" // 外部APIが500番台のレスポンスを返してきたことに起因す るエラー KindExternalAPIError ErrorKind = "external_api_error" KindDBError ErrorKind = "database_error" )
  7. ©2019GVA TECH Co., K. K. エラーレスポンス構造体 - ErrorKindを送らせることでHTTPStatusCodeでは 表現しきれない、エラー処理分岐を可能にする -

    idでAPI間のエラーを紐付ける // APIErrorResponse はリソースAPIがエラーを返す際に用いる構造体です。 type APIErrorResponse struct { // ErrorUUID はAPIを跨いで共有されるエラーのUUIDです。デバッグの時に使用します。 ErrorUUID string `json:"error_uuid"` // ErrorKind はHTTPStatusCodeでは表現しきれないエラーの種類を指定します。 // エラーの内容次第で、呼び出し元で処理を切り替える際に有効です。 // 例えばトークンがexpiredしていて、 //クライアントに再ログインを促す必要があることを見分ける場合などです。 ErrorKind ErrorKind `json:"error_kind"` Message string `json:"message"` }
  8. ©2019GVA TECH Co., K. K. ロギングメソッド - zapに依存し、JSON形式でロギング - 関数レベルでの拡張性

    - Loggerのconfigはパッケージ変数 func (e *ExtendError) Log(fields []zapcore.Field) { // 中略 if fields == nil { fields = defaultFields(e) } switch e.Severity { case zapcore.DebugLevel: // Loggerはパッケージ変数 Logger.Debug(string(e.Kind), fields...) case zapcore.InfoLevel: Logger.Info(string(e.Kind), fields...) case zapcore.WarnLevel: default: err := ExtendError{/* invalidなSeverityエラー (略) */} err.Log(nil) fallthrough // 元のエラーも ErrorLevel でログに出力しておく case zapcore.ErrorLevel: Logger.Error(string(e.Kind), fields...) case zapcore.DPanicLevel: Logger.DPanic(string(e.Kind), fields...) case zapcore.PanicLevel: Logger.Panic(string(e.Kind), fields...) case zapcore.FatalLevel: Logger.Fatal(string(e.Kind), fields...) } } func defaultFields(e *ExtendError) []zapcore.Field { return []zapcore.Field{ zap.String("error_id", e.id.String()), zap.String("error", e.Error()), zap.String("expected_cause", e.ExpectedCause), zap.String("cause_api_name", e.CauseAPIName), zap.String("handler_name", e.HandlerName), zap.String("occur_function_name", e.OccurFuncName), zap.Object("context_parameters", zapcore.ObjectMarshalerFunc( func(inner zapcore.ObjectEncoder) error { for k, v := range e.ContextParams { inner.AddString(k, v) } return nil })), } }
  9. ©2019GVA TECH Co., K. K. - private repositoryのパッケージってどうやってgo getするねん -

    →ググって度々見つかる「git config 使え」系が利用できない環境だ った - →→Go 1.13からのGOPRIVATE変数に GOPRIVATE=”github.com/GVATECH” - mapってzapでどうやってロギングするねん。パッと見メソッド無さげ - →前掲のソースコードの通り。 - ErrorKindとHTTPStatusCode間のMapperも作ったけどこれ使われないの では、、、、 ハマったところと今後の展望
  10. ©2019GVA TECH Co., K. K. - mercari.go #11 「About error

    handling Go」 - https://tech.mercari.com/entry/2019/10/11/160000 - eurekaさんの「Go言語におけるエラーハンドリングを今一度振り返る」 - https://medium.com/eureka- engineering/go%E8%A8%80%E8%AA%9E%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%8F%E3%83% B3%E3%83%89%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%92%E4%BB%8A%E4%B8%80%E5%BA%A6%E6%8C%AF%E3%82%8A%E8%BF%94%E3%8 2%8B-abe06c31daa4 - The Go Blog「Error handling and Go」 - https://blog.golang.org/error-handling-and-go - ブログ「Go言語のエラーハンドリングとログローテーション」 - https://re- engines.com/2018/11/05/go%E8%A8%80%E8%AA%9E%E3%81%AE%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%8F%E3%83%B3%E3%83%89%E3%83% AA%E3%83%B3%E3%82%B0%E3%81%A8%E3%83%AD%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%8 3%B3/ - MediaDoさんの「Goで真面目にerror handlingと向き合ってみた」 - https://techdo.mediado.jp/entry/2019/02/15/120257 - ブログ「Goの実プロジェクトでのエラーハンドリングの悩みどころと解決案」 - https://christina04.hatenablog.com/entry/go-error-handling - jwt-goのエラー設計 - https://github.com/dgrijalva/jwt-go/blob/master/errors.go ありがとうございました お世話になったページ、勉強会