Slide 1

Slide 1 text

STORES 株式会社 混沌とした例外処理とエラー監視に 秩序をもたらす morihirok 東京Ruby会議12

Slide 2

Slide 2 text

自己紹介 森弘 一茂 X: @_morihirok GitHub: morihirok STORES 株式会社 ソフトウェアエンジニア

Slide 3

Slide 3 text

Rubyと暮らす上で かかせないものと言えば?

Slide 4

Slide 4 text

そう!例外処理ですね!

Slide 5

Slide 5 text

とはいえ例外処理は大変 ● 複数人で開発していると意外と共通認識が揃っていない ● エラーの発見・監視と関係してよりややこしくなる ● そもそも例外って何?例外的な状況とは?

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

デカい! 10年超え!

Slide 8

Slide 8 text

デカい Rails ✖ 例外処理 = ヤバい!

Slide 9

Slide 9 text

そんな大変さと向き合ってきた経験を話します 少しでもみなさんの参考になれば幸いです...!

Slide 10

Slide 10 text

今日のアジェンダ 1. 実録!例外処理とエラー監視 2. 例外処理の整理を試みる 3. 改善の軌跡

Slide 11

Slide 11 text

実録!例外処理とエラー監視

Slide 12

Slide 12 text

ある日のこと(超脚色版) 「A機能が動作していない」 という問い合わせがあります。 原因の確認をお願いします。 怪しいログもないし コード読むか...

Slide 13

Slide 13 text

全て握りつぶされていた! def show result = begin ResponseGenerator.call(params) rescue nil end render json: result end

Slide 14

Slide 14 text

問い合わせからしか異常を検知できないシステムに ● 全ての例外を握りつぶしていることでユーザーに正しい フィードバックを返すことができない ● エンジニアも例外の発生を検知できない ● 発生した問題をエンジニアがコードを読むことでしか解決 できなくなる

Slide 15

Slide 15 text

ある日のこと2(超脚色版) 「B機能が動作していない」 という問い合わせがあります。 原因の確認をお願いします。 怪しいログもないし コード読むか...

Slide 16

Slide 16 text

全て握りつぶされ知らないログに送られていた! def show result = begin ResponseGenerator.call(params) rescue => e logger = Logger.new('hoge.log') logger.error(e) end render json: result end

Slide 17

Slide 17 text

活用されないログが散らばっているシステムに ● 何がどこに書き込まれているのか、それをどのように利用 するのか関係者が把握できていない ● ログに書き込んだ上で、そのログをどう利用するかまで考 慮されていない

Slide 18

Slide 18 text

ある日のこと3(超脚色版) エラートラッカーに 大量の通知が来た!!! 何が起きてるんだ!

Slide 19

Slide 19 text

全て握りつぶされエラートラッカーに送られていた! def show result = begin ResponseGenerator.call(params) rescue => e Sentry.capture_exception(e) end render json: result end

Slide 20

Slide 20 text

ある日のこと4(マジ) エラートラッカーに 大量の通知が来た!!! 何が起きてるんだ!

Slide 21

Slide 21 text

ある日のこと4(マジ) それは無視していいエラーです プロジェクトに 長年関わっているエンジニア

Slide 22

Slide 22 text

エラートラッカーがオオカミ少年に ● 何か例外的な事象が起きた時に最優先で取り組むための ツールのはず ● 便利ログツールとして利用すると「割れ窓」になってしま う ● 本当に例外的な事象が起きていても気付けない

Slide 23

Slide 23 text

このような状況を なんとかする 必要があった

Slide 24

Slide 24 text

今日のアジェンダ 1. 実録!例外処理とエラー監視 2. 👉例外処理の整理を試みる 3. 改善の軌跡

Slide 25

Slide 25 text

例外処理の整理を試みる

Slide 26

Slide 26 text

例外処理をどのように考えていくか 同僚たちと議論し認識を深めました

Slide 27

Slide 27 text

https://x.com/Jxck_/status/770899329301159936

Slide 28

Slide 28 text

https://x.com/Jxck_/status/770901468383580160

Slide 29

Slide 29 text

https://x.com/Jxck_/status/770901468383580160 これをベースに 考える

Slide 30

Slide 30 text

https://qiita.com/yugui/items/28085697041966726964

Slide 31

Slide 31 text

どのように考えていくか 1. 利用者からの入力は基本的に Expected として扱う 2. コンテキストから Accept か Un Accept か決めていく 3. 例外をまるごとハンドリングしない 4. それは本当に Expected か?

Slide 32

Slide 32 text

1.利用者からの入力は基本的に Expected として扱う ● 利用者はあらゆる入力をするという前提でプログラミング をする ● 「かもしれない」プログラミング

Slide 33

Slide 33 text

1.利用者からの入力は基本的に Expected として扱う ● ただひたすら入力値をチェックするということではない https://speakerdeck.com/twada/php-conference-2016

Slide 34

Slide 34 text

1.利用者からの入力は基本的に Expected として扱う 一般に、Rubyのイディオムでは型に基づく防御的なプログラミ ングを避けます。なぜなら、Rubyで気にするのは「オブジェク トがどんなメソッドに反応できるのか」と「そのメソッドがど んなオブジェクトを返すのか」だからです。(中略)型を明示 的に検査せず、ただオブジェクトを使いましょう。 Jeremy Evans 研鑽Rubyプログラミング ― 実践的なコードのた めの原則とトレードオフ p148-149

Slide 35

Slide 35 text

2.コンテキストから Accept か Un Accept か決めていく ● Ruby の JSON.parse ● JavaScript の JSON.parse

Slide 36

Slide 36 text

2.コンテキストから Accept か Un Accept か決めていく ● Ruby の JSON.parse ● JavaScript の JSON.parse JSON文法(RFC 8259)に 準拠しないテキストは Un Accept としている

Slide 37

Slide 37 text

JSON文法かどうか判定してくれる 架空のWebアプリケーションを 考えてみる

Slide 38

Slide 38 text

Webにおける Accept と Un Accept ● Expected / Accepted:2XX ● Expected / Un Accept:4XX, 5XX ○ Expected だが 5XX を返すケースはありうるだろう ● Un Expected:5XX

Slide 39

Slide 39 text

JSONかどうか判定してくれる Web アプリケーション ● Web サーバはあらゆる入力を Expected なものと扱う ● JSON.parse が Un Accept しても Web サーバの振る舞 いはそれに引きずられない ● コードを書くプログラマがコンテキストを踏まえて都度判 断する

Slide 40

Slide 40 text

Accept と Un Accept どちらにしよう ● Expected / Accepted:2XX ● Expected / Un Accept:4XX, 5XX ○ Expected だが 5XX を返すケースはありうるだろう ● Un Expected:5XX

Slide 41

Slide 41 text

Accept と Un Accept どちらにしよう ● Expected / Accepted:2XX ● Expected / Un Accept:4XX, 5XX ○ Expected だが 5XX を返すケースはありうるだろう ● Un Expected:5XX どちらもありえそう!

Slide 42

Slide 42 text

3.例外をまるごとハンドリングしない ● 発生しうる全ての例外をハンドリングするような実装は避 けるべき def show result = begin ResponseGenerator.call(params) rescue => e Sentry.capture_exception(e) end render json: result end こういうハンドリングを 避ける

Slide 43

Slide 43 text

3.例外をまるごとハンドリングしない ● 基本的に発生すると想定している(Expectedな)例外を 明示的に指定する ● 想定していない(Un Expectedな)例外をハンドリングし ない

Slide 44

Slide 44 text

4.それは本当に Expected か? ● Un Expected 代表例 ○ アプリケーション環境起因の例外 ○ データベースなどミドルウェア障害起因の例外

Slide 45

Slide 45 text

PHP7 で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 より

Slide 46

Slide 46 text

(再掲)どのように考えていくか 1. 利用者からの入力は基本的に Expected として扱う 2. コンテキストから Accept か Un Accept か決めていく 3. 例外をまるごとハンドリングしない 4. それは本当に Expected か?

Slide 47

Slide 47 text

今日のアジェンダ 1. 実録!例外処理とエラー監視 2. 例外処理の整理を試みる 3. 👉改善の軌跡

Slide 48

Slide 48 text

改善の軌跡

Slide 49

Slide 49 text

このコードはどうなるべきか def show result = begin ResponseGenerator.call(params) rescue nil end render json: result end

Slide 50

Slide 50 text

Expected な例外がない場合はハンドリングしない def show result = ResponseGenerator.call(params) render json: result end

Slide 51

Slide 51 text

Expected な例外がある場合はそれをハンドリングする def show result = begin ResponseGenerator.call(params) rescue ResponseGenerator::SomeError return render status: :bad_request end render json: result end

Slide 52

Slide 52 text

やったか!?

Slide 53

Slide 53 text

改善しても増え続ける まるっとハンドリング

Slide 54

Slide 54 text

改善しても増え続けるまるっとハンドリング ● 既存コードは参考にされ続けるので、新たなコードに既存 踏襲のまるっとハンドリングが書かれてしまう ● (STORES 特有の事象として)様々なバックグラウンドの いろんなチームの人がリポジトリにコミットするのでアナ ウンスという形ではなかなか浸透しないがち

Slide 55

Slide 55 text

(再掲)どのように考えていくか 1. 利用者からの入力は基本的に Expected として扱う 2. コンテキストから Accept か Un Accept か決めていく 3. 例外をまるごとハンドリングしない 4. それは本当に Expected か?

Slide 56

Slide 56 text

(再掲)どのように考えていくか 1. 利用者からの入力は基本的に Expected として扱う 2. コンテキストから Accept か Un Accept か決めていく 3. 例外をまるごとハンドリングしない 4. それは本当に Expected か? プログラマが 判断するしかない

Slide 57

Slide 57 text

(再掲)どのように考えていくか 1. 利用者からの入力は基本的に Expected として扱う 2. コンテキストから Accept か Un Accept か決めていく 3. 例外をまるごとハンドリングしない 4. それは本当に Expected か? 機械的に 判断できる

Slide 58

Slide 58 text

整理したことで 改善すべき課題が 明確に

Slide 59

Slide 59 text

RuboCop を活用し課題を攻める ● Lint/SuppressedException https://docs.rubocop.org/rubocop/cops_lint.html#lintsuppressedexception

Slide 60

Slide 60 text

このコードも RuboCop 違反となる def show result = begin ResponseGenerator.call(params) rescue nil end render json: result end

Slide 61

Slide 61 text

.rubocop_todo.yml を活用し課題を攻める ● 新たに違反するコードが増えることを防ぐことができる ● .rubocop_todo.yml が改善すべきコードのリストとな り、改善ポイントが明確となる これが 改善ポイント

Slide 62

Slide 62 text

Custom Cop を活用し課題を攻める ● StandardError を握りつぶせない Custom Cop # bad begin some_method rescue => e handle_error end # bad begin some_method rescue StandardError => e handle_error end # good begin some_method rescue => e handle_error raise e end # good begin some_method rescue JSON::ParserError => e handle_error end

Slide 63

Slide 63 text

このコードも RuboCop 違反となる def show result = begin ResponseGenerator.call(params) rescue => e Sentry.capture_exception(e) end render json: result end

Slide 64

Slide 64 text

こうしないといけない def show result = begin ResponseGenerator.call(params) rescue => e Sentry.capture_exception(e) raise e end render json: result end

Slide 65

Slide 65 text

こうしないといけない def show result = begin ResponseGenerator.call(params) rescue => e Sentry.capture_exception(e) raise e end render json: result end これいらないな...?

Slide 66

Slide 66 text

エラートラッカーに明示的に送りたい時 is 何? ● 基本的にランタイムで例外が発生した時にはエラートラッ カーに送られるようになっている ● コンテキスト特有の値も例外ハンドリング内で行わなくて もに事前に設定しておける ○ 例えば Sentry の場合 https://docs.sentry.io/platforms/ruby/enrichin g-events/context/

Slide 67

Slide 67 text

どういうときに明示的におくられていたのか ● 何らか想定していない例外が起きた時に処理は止めたくな いが例外の発生はエラートラッカーに送りたいとき

Slide 68

Slide 68 text

どういうときに明示的におくられていたのか ● 何らか想定していない例外が起きた時に処理は止めたくな いが例外の発生はエラートラッカーに送りたいとき それって Un Expected な例外を ハンドリングしていないか...?

Slide 69

Slide 69 text

PHP7 で堅牢なコードを書く - 例外処理、表明プログラミング、契約による設計 より

Slide 70

Slide 70 text

Sentry.capture_exception を取り締まる Custom Cop

Slide 71

Slide 71 text

過激な Custom Cop が爆誕 def show result = begin ResponseGenerator.call(params) rescue => e Sentry.capture_exception(e) raise e end render json: result end RuboCop 違反となる

Slide 72

Slide 72 text

立ち返るとこうすれば十分だったり def show result = ResponseGenerator.call(params) render json: result end

Slide 73

Slide 73 text

やっぱりこうすれば十分だということ def show result = begin ResponseGenerator.call(params) rescue ResponseGenerator::SomeError return render status: :bad_request end render json: result end

Slide 74

Slide 74 text

RuboCop 違反によって 立ち止まって考える タイミングを得られる

Slide 75

Slide 75 text

とはいえいろんなケースがある def show result = begin ResponseGenerator.call(params) rescue ResponseGenerator::SomeError return render status: :bad_request end render json: result end 発生自体は 知りたいケースがある

Slide 76

Slide 76 text

Expected だが発生自体はログに残したいケース ● 外部システムの Web API を利用している ○ 一定エラーが起きるケースは想定されるが、想定以上 に頻繁にエラーが起きたらそれは知りたい ● リアルタイムに処理したいものではないが、後でまとめて なんらかの処理を行う必要がある

Slide 77

Slide 77 text

(再掲)エラートラッカーがオオカミ少年に ● 何か例外的な事象が起きた時に最優先で取り組むための ツールのはず ● 便利ログツールとして利用すると「割れ窓」になってしま う ● 本当に例外的な事象が起きていても気付けない Expected だがログに残したいとき エラートラッカーを利用しない

Slide 78

Slide 78 text

ログの利用用途によって送り先を変える ● 一定エラーが起きるケースは想定されるが、想定以上に頻 繁にエラーが起きたらそれは知りたい ○ Datadog, NewRelic など APM を利用すると便利 ○ X分以内にY回発生したらアラートを鳴らすといったこ とが簡単に設定できる

Slide 79

Slide 79 text

ログの利用用途によって送り先を変える ● リアルタイムに処理したいものではないが、後でまとめて なんらかの処理を行う必要がある ○ アプリケーションのDBに書けばよい ○ 検索しやすいストレージもおすすめ ■ S3に構造化して保持しておくと Athena でクエリで きる ■ BigQuery とか

Slide 80

Slide 80 text

用途に応じて書き込み先を変える def show result = begin ResponseGenerator.call(params) rescue => e logger = Logger.new('hoge.log') logger.error(e) end render json: result end 謎ログを やめられる

Slide 81

Slide 81 text

こうして秩序がもたらされ るための土台作りができてここからコツコツ改善していけばもたらされるのではなかろうかという雰囲気になっ たのであった

Slide 82

Slide 82 text

まとめ ● 例外を整理した ○ 利用者からの入力は基本的に Expected として扱う ○ コンテキストから Accept か Un Accept か決めていく ○ 例外をまるごとハンドリングしない ○ それは本当に Expected か? ● 「例外を丸ごとハンドリングしない」という機械的な制約 をつけることで増殖を防ぎ、改善を進められた

Slide 83

Slide 83 text

No content