Webアプリケーションのクラッシュレポートを自動送信する + バグトラッカー(FogBugz)へ保存する
Webアプリケーションのクラッシュレポートを作成し、バグトラッカーへ登録する仕組みを構築した。
クラッシュレポートとは、Webアプリケーションに限らず、ソフトウェアが例外やクラッシュで停止してしまった時に、クラッシュ情報(バックトレースや、ユーザー環境情報、停止したソースコードの位置)を集めて、開発者へ通知するしくみである。信頼性の高いソフトウェアを作るために、クラッシュレポートがどれほど役立つかの考察は、敬愛する Joel Spolsky の 「ユーザからクラッシュレポートを自動的に取得する方法」を読んでほしいと思う。
- 作者: Joel Spolsky,青木靖
- 出版社/メーカー: オーム社
- 発売日: 2005/12/01
- メディア: 単行本
- 購入: 18人 クリック: 371回
- この商品を含むブログ (451件) を見る
Ruby on Rails 3.x でクラッシュ情報を収集するには?
Rails::Application の config.exceptions_app を利用する。
初期状態では config.exceptions_app は未指定となっているそうで、下記のように処理を追加することで、例外を補足することができる。
config.exceptions_app = lambda do |env| case env["action_dispatch.exception"] # ... end end
私は、下記のクラッシュ情報を収集するようにしている。
情報 | 詳細 | 格納されている場所 |
---|---|---|
エラー内容 | env['action_dispatch.exception'] | |
ビルドバージョン | クラッシュが発生したアプリケーションのバージョン | |
バックトレース | クラッシュした箇所のソースコード上の位置と、コールスタック | env['action_dispatch.exception'].backtrace |
HTTP リクエストメソッド | (例)GET, POST, DELETE ... | env['REQUEST_METHOD'] |
HTTP リクエストURI | env['REQUEST_URI'] | |
HTTP リクエストパラメータ | env['action_dispatch.request.parameters'] | |
HTTP リクエストフォーマット | env['action_dispatch.request.formats'] | |
Webサーバーの種類 | (例) Apache/2.2.x | env['SERVER_SOFTWARE'] |
ユーザー情報 | env['HTTP_USER_AGENT'] | |
エラー発生時のセッション | env['rack.session'] |
収集した情報を、バグトラッカー(FogBugz)へ登録する方法
FogBugz
バグ管理 + プロジェクト管理 + カスタマーサービスの機能を備えた、有償の Web アプリケーション。1ユーザー月額$25。しかし45日間の無償試用期間があり、すばらしいのは Students and Startups Edition(学生、及び2ユーザーまでのスタートアップビジネス)ならばすべての機能が使えて、ずっとタダ!
しかし残念なことに、日本語にローカライズされていないためか、国内の利用例をあまり聞かない。
今回は、FogBugz にクラッシュレポートを自動登録する方法を紹介する。
FogBugz へのチケット登録は、FogBugz XML API を利用する。
XML API を扱うための Python ライブラリは標準で提供されているようだが、今回は Rails なので XML API を直接操作することにする。
XML API をRubyで扱えるようにした gem もいくつか公開されている。
この中の「ruby-fogbugz」というのは実際に試してみたところ、結構使いやすかった。しかし今回は、そこまで複雑な操作はしないので、直接 XML API を使って処理を実装した。
クラッシュ情報を収集して、FogBugz へ送信するソースコード
ソースを見てもらったほうが早いので、全文を掲載する。私はとりあえず config/application.rb にベタ書きしている。
# ... # 定数 FOGBUGZ_DOMAIN = "example.fogbugz.com" FOGBUGZ_USER = "komiyak@example.com" FOGBUGZ_PASSWORD = "**** ****" FOGBUGZ_PROJECT_ID = 3 # バグを登録するプロジェクトの FogBugz 上のID (ixProject) FOGBUGZ_ASSING_USER_ID = 3 # 新規バグがアサインされる仮想ユーザー(new bugs user みたいな感じ) module Example class Application < Rails::Application # ... # Production 環境で発生したエラーを検知 # FogBugz(バグトラッカー)へ、エラー情報を送信する config.exceptions_app = lambda do |env| # バグの重複登録を避けるために、バグのキーに対してほぼ一意な識別値をあたえる # この識別値は検索にも利用する bug_key = env['action_dispatch.exception'].to_s + ':' + env['action_dispatch.exception'].backtrace.first + ':' + config.build_version bug_key_hash_full = Digest::SHA1.hexdigest(bug_key) # ほぼ一意な識別値 bug_key_hash_10 = bug_key_hash_full[0, 10] # 先頭の 10 文字のみ抽出 # タイトル: ユニークキー+ビルドバージョン+例外の内容 unsafe_bug_title = "[#{bug_key_hash_10}...] #{config.build_version}:#{env['action_dispatch.exception'].to_s}" # 本文 unsafe_bug_event = "" # バグの内容を生成 unsafe_bug_event << "EXCEPTION:\r\n #{env['action_dispatch.exception']} \r\n" unsafe_bug_event << "EXCEPTION HASH:\r\n #{bug_key_hash_full} \r\n" unsafe_bug_event << "BUILD_VERSION:\r\n #{config.build_version} \r\n" unsafe_bug_event << "REQUEST_METHOD:\r\n #{env['REQUEST_METHOD']} \r\n" unsafe_bug_event << "REQUEST_URI:\r\n #{env['REQUEST_URI']} \r\n" unsafe_bug_event << "SERVER_SOFTWARE:\r\n #{env['SERVER_SOFTWARE']} \r\n" unsafe_bug_event << "HTTP_USER_AGENT:\r\n #{env['HTTP_USER_AGENT']} \r\n" unsafe_bug_event << "SESSION:\r\n #{env['rack.session']} \r\n" unsafe_bug_event << "REQUEST PARAMETERS:\r\n #{env['action_dispatch.request.parameters']} \r\n" unsafe_bug_event << "REQUEST FORMATS:\r\n #{env['action_dispatch.request.formats']} \r\n" unsafe_bug_event << "\r\n \r\nBACK TRACE (depth #{env['action_dispatch.exception'].backtrace.count}) \r\n \r\n" env['action_dispatch.exception'].backtrace.each_with_index do |trace, i| # # FogBugz API からのチケット登録には、本文のサイズに内部的な制約があるらしい。 # # 大きな文字列を登録しようとしたら、「404 Not Found」がリターンされてしまった。 # すべてのバックトレースをもたせると、上記の問題がおきてしまうので、 # チケットに登録するのは、一部のバックトレースだけにする。 # # TODO: 将来的には全てのバックトレースを送付したい。 # バックトレースデータをファイルにして、それを添付ファイルとして POST # するしか方法は無さそう。 # if i >= 3 unsafe_bug_event << " ... \r\n" break end unsafe_bug_event << " #{trace} \r\n" end # FogBugz API に接続 https = Net::HTTP.new(FOGBUGZ_DOMAIN, 443) https.use_ssl = true https.verify_mode = OpenSSL::SSL::VERIFY_PEER https.verify_depth = 5 https.start { # Login response = https.get("/api.asp?cmd=logon&email=#{FOGBUGZ_USER}&password=#{FOGBUGZ_PASSWORD}") doc = REXML::Document.new( response.body ) # FogBugz onetime token onetime_token = doc.elements['response/token'].text # Search response = https.get( "/api.asp?token=#{onetime_token}&cmd=search&cols=sTitle&max=1&q=#{bug_key_hash_full}" ) doc = REXML::Document.new( response.body ) # 送信データを URLエスケープ しておく bug_title = URI.escape(unsafe_bug_title) bug_event = URI.escape(unsafe_bug_event) # Duplication check if doc.elements['response/cases'].count == 0 # 該当なし → New Case response = https.get( "/api.asp?token=#{onetime_token}&cmd=new&sTitle=#{bug_title}&sEvent=#{bug_event}&ixProject=#{FOGBUGZ_PROJECT_ID}&ixPersonAssignedTo=#{FOGBUGZ_ASSING_USER_ID}" ) else # すでに同じ内容のチケットが登録されているようだったら、そのチケットに対して追記する形をとる # 重複したチケットが、いくつもいくつも増えたら嫌なので。 ix_bug = doc.elements['response/cases/case'].attributes["ixBug"].to_i response = https.get( "/api.asp?token=#{onetime_token}&cmd=edit&&ixBug=#{ix_bug}&ixProject=#{FOGBUGZ_PROJECT_ID}&sEvent=#{bug_event}" ) end # Logoff response = https.get("/api.asp?cmd=logoff&token=#{onetime_token}") } render file: 'public/500.html' end
上記のソースは development 環境では動作しない(config.exceptions_app が、development では動作しないため)。 production 環境で、例外やクラッシュが発生すると、上記の処理を通過し、クラッシュ情報を FogBugz へ送信してくれる。
FogBugz 側を見てみよう。
ポイント
- 予め、FogBugz 上に仮想ユーザー(例えば、New Bugs)を作成しておき、新しいバグはそのユーザーに割り当てる
- バグを修正するときは、仮想ユーザーからチケットを受け取ってから作業する
- 似たようなバグが発生した場合は、それに該当するケースを探してきて、それに追記する形で登録を行う
まだまだ、運用実績が乏しいので、今後修正をどんどん加えていくと思う。その時は、この記事に情報を追記していく予定だ。今後の課題としては、このままではユーザーに内緒で、クラッシュレポートを送信してしまうことになるので、クラッシュ時に簡単なフォームを表示して、ユーザーがクラッシュレポートを送信するか、しないのかを選べるようにしたいと思っている。