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

GoでオリジナルのErrorパッケージを作って運用してみた

 GoでオリジナルのErrorパッケージを作って運用してみた

エラーハンドリングの課題を解決するために、オリジナルのErrorパッケージを作って運用してみた話です。

【オンラインGo勉強会】Finatext × エブリー 〜スタートアップ最前線のGo事例〜 にて発表しました。

35a916cb02dc725ad127c6f5826972ce?s=128

kondroid

June 26, 2020
Tweet

More Decks by kondroid

Other Decks in Programming

Transcript

  1. GoでオリジナルのErrorパッケージを 作って運用してみた  Finatext × エブリー 〜スタートアップ最前線のGo事例〜 2020-6-26

  2. Copyright © every, Inc. All rights reserved. 自己紹介 2 2

      @kondroid00  新卒で営業職をしつつ、独学でプログラミングを習得して エン ジニアにキャリアチェンジ。スマホゲームや受託案件  や決済サービスの開発を担当。現在は株式会社エブリーで  Goを書いたり、Prestoでクエリを書いたりしてます。
  3. Copyright © every, Inc. All rights reserved. 本出しました 3 3

    社内で開いたポインタの勉強会が好評だったので、 内容を書籍にして技術書典 応援祭に出展しました。 気になる方はBOOTHにて「Go ポインタ」で検索 >>
  4. Copyright © every, Inc. All rights reserved. 株式会社エブリーとは 4 4

    4つの動画メディアを運営する会社です DELISH KITCHEN 「だれでもおいしく簡単に作れるレシピ」を 毎日配信するレシピ動画メディア KALOS 「何度だって、可愛いに出会える」をコンセプト としたライフスタイル動画メディア MAMADAYS 「ママの課題を解決する」をコンセプトとした  ファミリー向け動画メディア TIMELINE 「知的好奇心を刺激し、あなたの人生にきっかけ を届ける」ニュース&エンタメ動画メディア
  5. Copyright © every, Inc. All rights reserved. エブリーの技術スタック 5 5

    サーバー Go フロントエンド Nuxt.js, Next.js ネイティブアプリ Swift, Kotlin データ TreasureData, Spark, MLflow, Redash... インフラ(AWS) ECS, EKS, Lambda, S3, RDS, ElasticCache... その他 Fluentd, Firebase, ElasticSearch... サポートツール CircleCI, Sentry, Datadog, PagerDuty... 社内ツール Slack, Asana, Confluence... 実は創業初期からずっとGoを使ってます
  6. Copyright © every, Inc. All rights reserved. エラーに関する3つの課題 6

  7. Copyright © every, Inc. All rights reserved. エラーに関する3つの課題 7 課題1

     開発メンバーの増加などでerrorsとpkg/errorsが混在してしまい、Sentryに  スタックトレースを送れないケースが出てきてしまった。 課題2  Go1.13が出てerrorsパッケージにIs()とAs()が追加された直後だったため、  errorsとpkg/errorsが混在してエラーハンドリングができないバグが必ず発生  してしまうと思った。  (現在はpkg/errorsがGo1.13のerrorsの実装に合わせているので問題なし) 課題3  Sentryにエラーイベントを非同期送信する時にchanのバッファがいっぱいに  なってしまうとイベントがドロップする。 7
  8. Copyright © every, Inc. All rights reserved. エラーに関する3つの課題 8 課題1

     開発メンバーの増加などでerrorsとpkg/errorsが混在してしまい、Sentryに  スタックトレースを送れないケースが出てきてしまった。 課題2  Go1.13が出てerrorsパッケージにIs()とAs()が追加された直後だったため、  errorsとpkg/errorsが混在してエラーハンドリングができないバグが必ず発生  してしまうと思った。  (現在はpkg/errorsがGo1.13のerrorsの実装に合わせているので問題なし) 課題3  Sentryにエラーイベントを非同期送信する時にchanのバッファがいっぱいに  なってしまうとイベントがドロップする。 8
  9. Copyright © every, Inc. All rights reserved. 課題1 Sentryにスタックトレースを送れてない 9

    9 errorsの使い方 errorsはエラーの発生箇所を Sentryで補足するには エラーメッセージとして記入する必要がある。 middlewareでSentryにエラーを送信すると、 handler内で発生したエラーは補足できない。 エラーの発生場所をトレースするには、例えば 以下のような実装を全てのエラーに対して適用する 必要がある。 if err != nil { _, file, line, _ := runtime.Caller(0) return nil, fmt.Errorf("%w, file:%s, line:%d\n", err, file, line) }
  10. Copyright © every, Inc. All rights reserved. 課題1 Sentryにスタックトレースを送れてない 10

    10 pkg/errorsの使い方 pkg/errorsでは内部でruntime.Caller()を呼び、内部の構造体に スタックトレースを保持しておく。 Sentry送出時にStackTrace()を呼び出し、スタックトレースを 送信する。 middlewareでSentryにエラーを送信しても、エラー発生箇所の スタックトレースがちゃんと送信される。 途中でerrors.Wrap()でスタックトレースを上書きしなければ 何もしなくていいので楽。 できればerrorsよりもpkg/errorsを使ってスタックトレースを Sentryに送信したい。
  11. Copyright © every, Inc. All rights reserved. エラーに関する3つの課題 11 課題1

     開発メンバーの増加などでerrorsとpkg/errorsが混在してしまい、Sentryに  スタックトレースを送れないケースが出てきてしまった。 課題2  Go1.13が出てerrorsパッケージにIs()とAs()が追加された直後だったため、  errorsとpkg/errorsが混在してエラーハンドリングができないバグが必ず発生  してしまうと思った。  (現在はpkg/errorsがGo1.13のerrorsの実装に合わせているので問題なし) 課題3  Sentryにエラーイベントを非同期送信する時にchanのバッファがいっぱいに  なってしまうとイベントがドロップする。 11
  12. Copyright © every, Inc. All rights reserved. 課題2 エラーハンドリングのやり方が違う 12

    12 import ( "errors" "fmt" ) var TestError = errors.New("TestError") func foo() error { // fmt.Errorfでラップする return fmt.Errorf("wrap %w", TestError) } func main() { if err := foo(); err != nil { if errors.Is(err, TestError) { fmt.Println(err.Error()) } } } import ( "errors" "fmt" ) type TestError struct{} func (e *TestError) Error() string { return "TestError" } func foo() error { // fmt.Errorfでラップする return fmt.Errorf("wrap %w", &TestError{}) } func main() { if err := foo(); err != nil { var target *TestError if errors.As(err, &target) { fmt.Printf(err.Error()) } } } errorsの使い方 fmt.Errorf()に%wを使ってエラーを ラップしてからIs()とAs()でエラー ハンドリングする。 エラー構造体をレシーバーとする関数 Unwrap()が実装されていなければ Is()と As()でハンドリングができない。
  13. Copyright © every, Inc. All rights reserved. 課題2 エラーハンドリングのやり方が違う 13

    13 import ( e "github.com/pkg/errors" "fmt" ) var TestError = e.New("TestError") func foo() error { // e.Wrapでラップする return e.Wrap(TestError, "wrap") } func main() { if err := foo(); err != nil { err1 := e.Cause(err) if err1 == TestError { fmt.Println(err.Error()) } } } import ( e "github.com/pkg/errors" "fmt" ) type TestError struct{} func (e *TestError) Error() string { return "TestError" } func foo() error { // e.Wrapでラップする return e.Wrap(&TestError{}, "wrap") } func main() { if err := foo(); err != nil { err1 := e.Cause(err) if _, ok := err1.(*TestError); ok { fmt.Println(err.Error()) } } } pkg/errorsの使い方(実装当時) Wrap()でエラーをラップしてから Cause()でエラーを取り出して、 等価演算子や型アサーションで エラーハンドリングする。 エラー構造体をレシーバーとする関数 Cause()が実装されていなければ e.Cause()でエラーを取り出せない。
  14. Copyright © every, Inc. All rights reserved. 課題2 エラーハンドリングのやり方が違う 14

    14 import ( e "github.com/pkg/errors" "fmt" ) var TestError = e.New("TestError") func foo() error { // e.Wrapでラップする return e.Wrap(TestError, "wrap") } func main() { if err := foo(); err != nil { err1 := e.Cause(err) if err1 == TestError { fmt.Println(err.Error()) } } } import ( e "github.com/pkg/errors" "fmt" ) type TestError struct{} func (e *TestError) Error() string { return "TestError" } func foo() error { // e.Wrapでラップする return e.Wrap(&TestError{}, "wrap") } func main() { if err := foo(); err != nil { err1 := e.Cause(err) if _, ok := err1.(*TestError); ok { fmt.Println(err.Error()) } } } pkg/errorsの使い方(実装当時) Wrap()でエラーをラップしてから Cause()でエラーを取り出して、 等価演算子や型アサーションで エラーハンドリングする。 エラー構造体をレシーバーとする関数 Cause()が実装されていなければ e.Cause()でエラーを取り出せない。 fmt.Errorf()でラップしたエラーは e.Cause()でハンドリングできない。 また、e.Wrap()でラップしたエラーは errors.Is()やerrors.As()でハンドリングで きない。
  15. Copyright © every, Inc. All rights reserved. 課題2 エラーハンドリングのやり方が違う 15

    15 import ( e "github.com/pkg/errors" "fmt" ) var TestError = e.New("TestError") func foo() error { // e.Wrapでラップする return e.Wrap(TestError, "wrap") } func main() { if err := foo(); err != nil { if e.Is(err, TestError) { fmt.Println(err.Error()) } } } import ( e "github.com/pkg/errors" "fmt" ) type TestError struct{} func (e *TestError) Error() string { return "TestError" } func foo() error { // e.Wrapでラップする return e.Wrap(&TestError{}, "wrap") } func main() { if err := foo(); err != nil { var target *TestError if e.As(err, &target) { fmt.Printf(err.Error()) } } } pkg/errorsの使い方(ver0.9.0) Wrap()が返す構造体にUnwrap()が実装 されており、errorsのAs()とIs()をそれぞ れラップしたものも実装されている ので、Cause()を使わずにAs()とIs()を 使うように統一すれば問題にはならなさ そう。
  16. Copyright © every, Inc. All rights reserved. エラーに関する3つの課題 16 課題1

     開発メンバーの増加などでerrorsとpkg/errorsが混在してしまい、Sentryに  スタックトレースを送れないケースが出てきてしまった。 課題2  Go1.13が出てerrorsパッケージにIs()とAs()が追加された直後だったため、  errorsとpkg/errorsが混在してエラーハンドリングができないバグが必ず発生  してしまうと思った。  (現在はpkg/errorsがGo1.13のerrorsの実装に合わせているので問題なし) 課題3  Sentryにエラーイベントを非同期送信する時にchanのバッファがいっぱいに  なってしまうとイベントがドロップする。 16
  17. Copyright © every, Inc. All rights reserved. 課題3 エラーイベントがドロップする 17

    17 type HTTPTransport struct { …. buffer chan *http.Request // Size of the transport buffer. Defaults to 30. BufferSize int …. } func (t *HTTPTransport) worker() { for request := range t.buffer { // 送信処理 } } func (t *HTTPTransport) SendEvent(event *Event) { …. select { case t.buffer <- request: …. default: …. // worker would block, drop the packet } .... } sentry-goの非同期イベント送信 chan *http.Request型のフィールド(buffer)にもつ構造体を 定義。BufferSizeを変更することでbufferのサイズを変更 可能。(bufferのことを以下送信バッファと呼ぶ ) まずはworker()を別goroutineで起動し、送信バッファに *http.RequestがPushされるのをforループで待ち受ける。 送信バッファに*http.RequestがPushされるとrequestを 取り出し、そのrequestを使ってSentryサーバーに送信 する。 問題はSendEvent()が呼ばれた時に送信バッファが満杯 だった場合、エラーイベントが Sentryに送信されずに ドロップする。ドロップを防ごうとしてエラーごとに 送信バッファを新規作成したり、同期送信を使うと メモリを使い潰してサーバーが落ちるリスクがある。
  18. Copyright © every, Inc. All rights reserved. 課題3 エラーイベントがドロップする 18

    18 type HTTPTransport struct { …. buffer chan *http.Request // Size of the transport buffer. Defaults to 30. BufferSize int …. } func (t *HTTPTransport) worker() { for request := range t.buffer { // 送信処理 } } func (t *HTTPTransport) SendEvent(event *Event) { …. select { case t.buffer <- request: …. default: …. // worker would block, drop the packet } .... } sentry-goの非同期イベント送信 エラーの発生頻度より Sentryへの送出頻度が高ければ、 エラーイベントがドロップすることはないので特に問題はな い。 しかし、DELISH KITCHENはアクセス数が多く、同じ 箇所で大量の同じエラーが発生してしまうことがある。 (サーバー内部で叩く他のAPIサーバーが500を返すときなど) 結果、Sentryへの送出頻度よりエラーの発生頻度が高く なってしまい、送信バッファがすぐにチャネルの最大 サイズに到達してしまう。 この状態で別の箇所でクリティカルなエラーが発生した 場合、エラーをSentryに送信することができないため、 その場で重大なエラーの発生を見逃してしまう。
  19. Copyright © every, Inc. All rights reserved. エラーに関する3つの課題(再掲) 19 課題1(未解決)

     開発メンバーの増加などでerrorsとpkg/errorsが混在してしまい、Sentryに  スタックトレースを送れないケースが出てきてしまった。 課題2(未解決)  Go1.13が出てerrorsパッケージにIs()とAs()が追加された直後だったため、  errorsとpkg/errorsが混在してエラーハンドリングができないバグが必ず発生  してしまうと思った。  (現在はpkg/errorsがGo1.13のerrorsの実装に合わせているので問題なし) 課題3(未解決)  Sentryにエラーイベントを非同期送信する時にchanのバッファがいっぱいに  なってしまうとイベントがドロップする。 19
  20. Copyright © every, Inc. All rights reserved. 課題を解決する 20

  21. Copyright © every, Inc. All rights reserved. 課題を解決する 21 解決手順1

     課題1と課題2を解決するために、原則errorsとpkg/errorsはimportさせない。 その代 わりとして必要な機能を全て提供したオリジナルのエラーパッケージ  を定義し、それを機械的にimportするようにする。 解決手順2  オリジナルのエラーパッケージに課題3を解決する機能を追加する。具体的  には送信するエラーごとにsentry.Clientを別のものを使うことで送信バッファ  を分け、同じエラーであればエラーイベントのドロップを許す。 21
  22. Copyright © every, Inc. All rights reserved. 課題を解決する 22 解決手順1

     課題1と課題2を解決するために、原則errorsとpkg/errorsはimportさせない。 その代 わりとして必要な機能を全て提供したオリジナルのエラーパッケージ  を定義し、それを機械的にimportするようにする。 解決手順2  オリジナルのエラーパッケージに課題3を解決する機能を追加する。具体的  には送信するエラーごとにsentry.Clientを別のものを使うことで送信バッファ  を分け、同じエラーであればエラーイベントのドロップを許す。 22
  23. Copyright © every, Inc. All rights reserved. 解決手順1 エラーパッケージを作る 23

    23 import ( "fmt" "errors" e "github.com/pkg/errors" ) func main() { if err := foo(); err != nil { fmt.Println(err.Error()) } if err := bar(); err != nil { fmt.Println(err.Error()) } } func foo() error { return errors.New("errors") } func bar() error { return e.New("pkg/errors") } errorsパッケージはこれだけimportしとけばOKというものを用意する import ( "fmt" "github.com/kondroid00/sample-grpc-point/package/errors" ) func main() { if err := foo(); err != nil { fmt.Println(err.Error()) } if err := bar(); err != nil { fmt.Println(err.Error()) } } func foo() error { return errors.New("package/errors") } func bar() error { return errors.New("package/errors") }
  24. Copyright © every, Inc. All rights reserved. 解決手順1 エラーパッケージを作る 24

    24 package errors import ( "errors" e "github.com/pkg/errors" ) type ( CustomError struct { // github.com/pkg/errorsの*fundamentalを入れる err error } ) // New messageとstacktraceを格納したエラーを返す func New(message string) *CustomError { return &CustomError{ err: e.New(message), } } // Stack 既存のエラーにstacktraceを格納したエラーを返す func Stack(err error) *CustomError { return &CustomError{ err: e.WithStack(err), } } CustomErrorを定義 pkg/errorsの*fundamentalをラップする構造体である CustomErrorを定義する。 pkg/errorsのNew()とWithStack()に相当するジェネレーター 関数であるNew()とStack()を用意する。
  25. Copyright © every, Inc. All rights reserved. 解決手順1 エラーパッケージを作る 25

    25 package errors import ( "errors" e "github.com/pkg/errors" ) ……. // Error Errorインターフェースを満たす func (ce *CustomError) Error() string { return ce.err.Error() } // StackTrace StackTrace()が実装されている場合にそれを呼び 出す。sentryにstacktraceを送信する際に利用する func (ce *CustomError) StackTrace() e.StackTrace { if stackTracer, ok := ce.err.(interface { StackTrace() e.StackTrace }); ok { return stackTracer.StackTrace() } return nil } // Unwrap errors.Isとerrors.Asで利用する func (ce *CustomError) Unwrap() error { // pkg/errors@0.9.0以降であればe.Unwrap()でもOK return e.Cause(ce.err) } CustomErrorを定義 pkg/errorsの*fundamentalをラップする構造体である CustomErrorを定義する。 pkg/errorsのNew()とWithStack()に相当するジェネレーター 関数であるNew()とStack()を用意する。 Errorインターフェースを満たすよう Error()を実装する。 StackTracerインターフェースを満たすよう StackTrace()を実 装する。これがなければ middlewareでSentryに送信する時 にエラー発生箇所のスタックトレースを送信でき ない。 Unwrap()を実装する。これがなければ後述の Is()とAs()でエ ラーハンドリングができない。
  26. Copyright © every, Inc. All rights reserved. 解決手順1 エラーパッケージを作る 26

    26 package errors import ( "errors" e "github.com/pkg/errors" ) ……. // Is errors.Isと同じ func Is(err, target error) bool { return errors.Is(err, target) } // As errors.Asと同じ func As(err error, target interface{}) bool { return errors.As(err, target) } CustomErrorを定義 pkg/errorsの*fundamentalをラップする構造体である CustomErrorを定義する。 pkg/errorsのNew()とWithStack()に相当するジェネレーター 関数であるNew()とStack()を用意する。 Errorインターフェースを満たすよう Error()を実装する。 StackTracerインターフェースを満たすよう StackTrace()を実 装する。これがなければ middlewareでSentryに送信する時 にエラー発生箇所のスタックトレースを送信でき ない。 Unwrap()を実装する。これがなければ後述の Is()とAs()でエ ラーハンドリングができない。 errorsやpkg/errorsを別途importする必要をなくすための ラップ関数を用意。
  27. Copyright © every, Inc. All rights reserved. 課題を解決する 27 解決手順1

     課題1と課題2を解決するために、原則errorsとpkg/errorsはimportさせない。 その代 わりとして必要な機能を全て提供したオリジナルのエラーパッケージ  を定義し、それを機械的にimportするようにする。 解決手順2  オリジナルのエラーパッケージに課題3を解決する機能を追加する。具体的  には送信するエラーごとにsentry.Clientを別のものを使うことで送信バッファ  を分け、同じエラーであればエラーイベントのドロップを許す。 27
  28. Copyright © every, Inc. All rights reserved. 解決手順2 エラーイベントのドロップを防ぐ 28

    28 package alert type ClientKey string type AlertError interface { error ClientKey() ClientKey } AlertErrorインターフェースを定義 errorインターフェースをembedし、ClientKey()の実装を 要求するAlertErrorインターフェースを定義する。
  29. Copyright © every, Inc. All rights reserved. 解決手順2 エラーイベントのドロップを防ぐ 29

    29 package errors import ( "errors" e "github.com/pkg/errors" ) ……. type CustomErrorWithKey struct { CustomError // CustomeErrorをembed clientKey string // alert.AlertErrorで使うclientKey } // NewWithKey messageとstacktraceとclientKeyを格納したエラー を返す func NewWithKey(message, clientKey string) *CustomErrorWithKey { return &CustomErrorWithKey{ CustomError: *New(message), clientKey: clientKey, } } // StackWithKey 既存のエラーにstacktraceとclientKeyを格納した エラーを返す func StackWithKey(err error, clientKey string) *CustomErrorWithKey { return &CustomErrorWithKey{ CustomError: *Stack(err), clientKey: clientKey, } } CustomErrorWithKeyを定義 CustomErrorをembedしてstring型のフィールドclientKey をもつCustomErrorWithKey構造体を定義する。 CostomErrorのNew()とStack()と同様にジェネレーター関数 のNewWithKey()とStackWithKey()を定義。clientKeyを 引数にとり、フィールドに設定する。
  30. Copyright © every, Inc. All rights reserved. 解決手順2 エラーイベントのドロップを防ぐ 30

    30 package errors import ( "errors" e "github.com/pkg/errors" ) ……. // Error Errorインターフェースを満たす func (ce *CustomErrorWithKey) Error() string { return ce.CustomError.Error() } // StackTrace StackTrace()が実装されている場合にそれを呼び 出す。sentryにstacktraceを送信する際に利用する func (ce *CustomErrorWithKey) StackTrace() e.StackTrace { return ce.CustomError.StackTrace() } // Unwrap errors.Isとerrors.Asで利用する func (ce *CustomErrorWithKey) Unwrap() error { return ce.CustomError.Unwrap() } // ClientKey AlertErrorインターフェースを満たす func (ce *CustomErrorWithKey) ClientKey() string { return ce.clientKey } CustomErrorWithKeyを定義 CustomErrorをembedしてstring型のフィールドclientKey をもつCustomErrorWithKey構造体を定義する。 CostomErrorのNew()とStack()と同様にジェネレーター関数 のNewWithKey()とStackWithKey()を定義。clientKeyを 引数にとり、フィールドに設定する。 CustomErrorと同じくError()、StackTrace()、Unwrap()を 実装する。 AlertErrorインターフェースを満たすように ClientKey()を 実装し、フィールドの clientKeyを返却する
  31. Copyright © every, Inc. All rights reserved. 解決手順2 エラーイベントのドロップを防ぐ 31

    31 package alert ……. type Sentry struct { ……. clientPool map[ClientKey]*sentry.Client } func (s *Sentry) getClient(key ClientKey)(*sentry.Client, error) { ……. if c, ok := s.clientPool[key]; ok { return c, nil } else { client, err := sentry.NewClient(s.options) ……. s.clientPool[key] = client return client, nil } ……. } func (s *Sentry) Send(e error, info *Info) { clientKey := "default" if err, ok := e.(AlertError); ok { clientKey = err.ClientKey() } client := s.getClient(clientKey) ……. } Sentryにエラーイベントを送信する ClientKeyをkey、*sentry.Clientをvalueとするmapを フィールドにもつSentry構造体を定義。この構造体は サーバー起動時に一つだけ作成され、複数のリクエスト で参照される。
  32. Copyright © every, Inc. All rights reserved. 解決手順2 エラーイベントのドロップを防ぐ 32

    32 package alert ……. type Sentry struct { ……. clientPool map[ClientKey]*sentry.Client } func (s *Sentry) getClient(key ClientKey)(*sentry.Client, error) { ……. if c, ok := s.clientPool[key]; ok { return c, nil } else { client, err := sentry.NewClient(s.options) ……. s.clientPool[key] = client return client, nil } ……. } func (s *Sentry) Send(e error, info *Info) { clientKey := "default" if err, ok := e.(AlertError); ok { clientKey = err.ClientKey() } client := s.getClient(clientKey) ……. } Sentryにエラーイベントを送信する ClientKeyをkey、*sentry.Clientをvalueとするmapを フィールドにもつSentry構造体を定義。この構造体は サーバー起動時に一つだけ作成され、複数のリクエスト で参照される。 getClient()を定義し、引数のkeyに対応する*sentry.Client がmapに存在すればそれを返し、なければ新規作成して mapに詰めてから返す。 複数のリクエストから叩かれる 関数なのでスレッドセーフに作る必要がある。
  33. Copyright © every, Inc. All rights reserved. 解決手順2 エラーイベントのドロップを防ぐ 33

    33 package alert ……. type Sentry struct { ……. clientPool map[ClientKey]*sentry.Client } func (s *Sentry) getClient(key ClientKey)(*sentry.Client, error) { ……. if c, ok := s.clientPool[key]; ok { return c, nil } else { client, err := sentry.NewClient(s.options) ……. s.clientPool[key] = client return client, nil } ……. } func (s *Sentry) Send(e error) { clientKey := "default" if err, ok := e.(AlertError); ok { clientKey = err.ClientKey() } client := s.getClient(clientKey) ……. } Sentryにエラーイベントを送信する ClientKeyをkey、*sentry.Clientをvalueとするmapを フィールドにもつSentry構造体を定義。この構造体は サーバー起動時に一つだけ作成され、複数のリクエスト で参照される。 getClient()を定義し、引数のkeyに対応する*sentry.Client がmapに存在すればそれを返し、なければ新規作成して mapに詰めてから返す。複数のリクエストから叩かれる 関数なのでスレッドセーフに作る必要がある。 Send()にて送信するerrorがAlertErrorインターフェースを 満たすかどうか型アサーションでチェックして、満たす ならclientKeyを取得。clientKeyに対応する*sentry.Client を取得してSentryに送信する。
  34. Copyright © every, Inc. All rights reserved. 解決手順2 エラーイベントのドロップを防ぐ 34

    34 import ( "github.com/kondroid00/sample-grpc-point/package/errors" ) // エラーの発生が稀なところではCustomErrorを使う func foo() error { if !ok { return errors.New("error") } if err := hoge(); err != nil { return errors.Stack(err) } ……. } // 大量のエラーが発生すると見込まれるところCustomErrorWithKey を使う func bar() error { if !ok { return errors.NewWithKey("error", "bar") } if err := hoge(); err != nil { return errors.StackWithKey(err, "bar_hoge") } ……. } CustomError(WithKey)の使い方 エラーの発生が稀だと思われるところでは CustomErrorを 使うようにする。 サーバーから叩くAPIが500を返すようになった場合など、大 量のエラーが発生すると見込まれるところは CustomErrorWithKeyを使う。clientKeyが同じであれば、 送信バッファに入り切らなくなったエラーはドロップするが、他 で発生したエラーは別の送信バッファで影響を受けずに送信 できる。 オリジナルパッケージに As()とIs()も用意しているので、 そちらを使ってエラーハンドリングも問題なく可能。
  35. Copyright © every, Inc. All rights reserved. 課題を解決する(再掲) 35 解決手順1(済)

     課題1と課題2を解決するために、原則errorsとpkg/errorsはimportさせない。 その代 わりとして必要な機能を全て提供したオリジナルのエラーパッケージ  を定義し、それを機械的にimportするようにする。 解決手順2(済)  オリジナルのエラーパッケージに課題3を解決する機能を追加する。具体的  には送信するエラーごとにsentry.Clientを別のものを使うことで送信バッファ  を分け、同じエラーであればエラーイベントのドロップを許す。 35
  36. Copyright © every, Inc. All rights reserved. その後 36

  37. Copyright © every, Inc. All rights reserved. 実際に運用してみて 37 その1

     複数のエラーパッケージがimportされることはなくなり、Sentryに  スタックトレースがちゃんと送られるようになった。 その2  エラーハンドリングも明確になり、ハンドリングの不具合で事故が起きる  雰囲気はなくなった。 その3  エラーイベントのドロップの件では、幸いにも大量のエラーが発生した  ケースは出なかったので、まだ恩恵を受けていない。 37
  38. Copyright © every, Inc. All rights reserved. 個人的なぼやき 38 その1

     組み込みのerrorsを使うのがいいのかもしれないけど、Sentryを使うなら  今のところpkg/errorsの方がスタックトレースが取れて便利。ただし   pkg/errorsの開発は止まり気味。 その2  基本的にはpkg/errorsのVer 0.9.0以降で統一するのが良さそう。ただ、  Sentryのエラーイベント欠損を防ぐなど、独自機能を入れるとなると  Goならオリジナルパッケージを作るのが一番シンプルでいい気がする。 その3  なんとなくエラーのオリジナルパッケージを作ることにうしろめたさは  あったが、今回の課題の解決方法としてはかなり機能した。 その4  Goのエラーでいい方法あれば教えてくださいm(_ _)m 38
  39. Copyright © every, Inc. All rights reserved. 動画を通じて世界をもっと楽しく、もっと 充実した毎日に 39

  40. Copyright © every, Inc. All rights reserved. 関連リンク 40 DELISH

    KITCHEN 〜「だれでもおいしく簡単に作れるレシピ」を毎日配信するレシピ動画メディア〜 - https://delishkitchen.tv MAMADAYS 〜「ママの課題を解決する」をコンセプトとしたファミリー向け動画メディア〜 - https://mamadays.tv KALOS 〜「何度だって、可愛いに出会える」をコンセプトとしたライフスタイル動画メディア〜 - https://www.instagram.com/kalos.tv TIMELINE 〜「知的好奇心を刺激し、あなたの人生にきっかけを届ける」ニュース&エンタメ動画メディア〜 - https://www.facebook.com/TimelineNews.tv every.thing 〜エブリーのこと、ぜんぶ伝えるウェブマガジン〜 - https://everything.every.tv Goのポインタを完全に理解する本 〜技術書典 応援祭に出展したポインタの解説書〜 - https://booth.pm/ja/items/1993101 40