Bugsnag の Data access API でエラーイベント全てを取得する
この記事は Goodpatch Advent Calendar 2017 - Qiita の15日目の記事です。
エラー検知ツールとして Bugsnag を使っています。
エラー別にfilterかけられるし検索性は良い気がするので結構重宝しています。
ある日、とある重大なエラーが発生し、ユーザーのリクエストの一部が処理されていないことがわかりました。
その時にBugsnagに溜まった687件のエラーイベントから、リクエストのパラメータを抜き出して個別に処理した甘酸っぱい思い出を語ります。
Bugsnag Data access APIとは
Bugsnag docs › API › Data access
Access information about your organization, projects, errors, and more to build custom integrations
要はBugsnagに登録されたエラーなどのデータにアクセスできるみたい。
V1はDeprecatedになっているのでV2で行こうぜ。
V2 のリファレンスは Bugsnag Data Access API · Apiary
Ruby のAPI toolkitがあるみたいだから、Rubyでやろ。
GitHub - bugsnag/bugsnag-api-ruby: Bugsnag API toolkit for Ruby
Ruby で Data access APIを使う初期設定
bugsnag-api-ruby/README.md にほとんど書いてある。
gem のインストール
gemをインストールする。
pryで動かしたいのでpryも入れとくか。
gem 'bugsnag-api', '~> 2.0'
gem 'pry'
もちろん gem install bugsnag-api
でも良い。
Gemfileにした場合はもちろんbundle install
しよう。
アクセストークンの取得
そしてアクセストークンを取得しよう。
これは バグを報告するAPIのキーとは別物。
Bugsnagにログインして、
Settings
> My account
> Personal auth tokens
にて生成する。
Generate new token +
> GENERATE
したらTOKEN_KEYが取れるのでちゃんとコピーしよう。
まぁコピー忘れても再度Token作ろう。
アクセスしてみる
require 'bugsnag/api'
# Provide authentication credentials
Bugsnag::Api.configure do |config|
config.auth_token = 'ここにTOKEN_KEYを入れまっしょ。'
end
# Access API methods
organizations = Bugsnag::Api.organizations
これで自分の所属するorganizationが取れた。うふふ。
該当のエラーを取得する
まずはエラーの取得まで行ってみよう。
# Get a single error
error = Bugsnag::Api.error("project-id", "error-id")
そのためにproject-idとerror-idの特定が必要みたい。
project-id
の取得
ここが最初の引っ掛かりポイント、
project-id
は 管理画面とかのアクセスに使うURLに含まれてるものではなかった!
https://app.bugsnag.com/:organization/:project/errors
みたいなURLの :project
の部分だと思ってたよ。
project-id
はAPIで取得する必要があった。
# List organization projects
projects = Bugsnag::Api.projects("organization-id")
これを使ってprojectsの一覧を取得すれば良いのだね。
organizationはさっき取れたからこれでいける
organizations = Bugsnag::Api.organizations
organization_id = organizations.first.id
projects = Bugsnag::Api.projects(organization_id)
よしよし、projectの一覧が取れたよ。
そしたら名前かpathからidを特定しよう。
さっき勘違いしたpath名はproject#slug
のことみたいだ。
project_id = projects.select{|p| p.slug == 'projectのURLの:projectの部分'}&.first&.id
# Hashにして確認するなら
pry(main)> projects.map{|p| {id: p.id, name: p.name, slug: p.slug}}
=> [{:id=>"123456", :name=>"project A", :slug=>"project_a"},
{:id=>"654321", :name=>"CHOTO NEMUI", :slug=>"choto_nemui"},
{:id=>"135924", :name=>"Target Project", :slug=>"projectのURLの:projectの部分"}]
うひょひょ。取れたよー。
error-id
の取得
こちらは 管理画面のアクセスに使う
https://app.bugsnag.com/:organization/:project/errors/:error_id
のURLの一部をそのまま使えば良い。
でもせっかくだからAPIから特定してみよう。
# List project errors
errors = Bugsnag::Api.errors("project-id")
これを使おう。
pry(main)> errors.count
=> 30
あれあれ、これで全部かな・・・。headerも見れるみたいだから見てみよう。
last_response = Bugsnag::Api.last_response
last_response.headers
last_response.headers['x-total-count']
で全件数がわかる。
絞ってないから全部のエラーだともっと数は多い
どうやらこのAPIには30件の制限がある模様。
READMEの #pagenation によると
errors = Bugsnag::Api.errors("project-id", per_page: 100)
last_response = Bugsnag::Api.last_response
until last_response.rels[:next].nil?
last_response = last_response.rels[:next].get
errors.concat last_response.data
end
こうしてconcatしなされと。
やってみたら全部取れたけど数回アクセスしてたから per_page
は効いてない模様・・・。
error のレスポンスには色々入ってる。 see: Bugsnag Data Access API · Apiary / View an error
context
と error_class
で絞れそうですね。でもfilter
がrubyからは叩けないっぽいので、全部とった結果から特定するか。
error_id = errors.select{|e| e.context == ':context' && e.error_class == ':error_class'}&.first&.id
まぁ管理画面から取った方が楽だったね。
これでやっと該当の error-id
が取れたよ。
エラーからイベントを全て取得しパラメータのリストを作る
ここからがやっと本題。
エラーの全イベントからパラメーターを抽出すべし。
# List error events
events = Bugsnag::Api.error_events("project-id", "error-id")
これじゃな。
全件取得したいのでpagenate処理もしておく。
events = Bugsnag::Api.error_events(project_id, error_id)
last_response = Bugsnag::Api.last_response
until last_response.rels[:next].nil?
last_response = last_response.rels[:next].get
events.concat last_response.data
end
すると、
フハハ!エラーになったよ。
Bugsnag::Api::RateLimitExceeded: GET https://api.bugsnag.com/projects/135246/errors/123xyz/events?(略): 429 -
まぁそうですよね。普通RateLimitかけるよね。
RateLimit対策
ただ、 DocumentにRateLimitの話が一切ない。
headerを調べるとそれらしいものを発見。
{
"x-ratelimit-limit"=>"10",
"x-ratelimit-remaining"=>"0",
}
そこで何秒でlimitが解除されるかを確認
until last_response.rels[:next].nil?
begin
puts "remaining:#{last_response.headers['x-ratelimit-remaining']}"
last_response = last_response.rels[:next].get
puts 'success'
sleep_count = 1
events.concat last_response.data
rescue
puts Time.now
puts "sleep_count:#{sleep_count}"
sleep sleep_count
sleep_count += 1
end
end
と、こんな感じで待ち時間を確認して見た。
そして、少し調整したけど、だいたい1分間に10アクセス
くらいっぽい。
つまり60秒に10アクセスまでなら概ね問題なさそう。
なので、最終的にこんな感じで取得することにしました。
events = Bugsnag::Api.error_events(project_id, error_id)
last_response = Bugsnag::Api.last_response
wait_a_minite = true
until last_response.rels[:next].nil?
begin
last_response = last_response.rels[:next].get
events.concat last_response.data
wait_a_minite = true unless wait_a_minite
rescue Bugsnag::Api::RateLimitExceeded
if wait_a_minite
puts 'RateLimitExceeded sleep:1 minute'
sleep 60
wait_a_minite = false
else
puts 'RateLimitExceeded sleep:1 sec'
sleep 1
end
retry
end
end
そんなに待たなくても良いかもしれないけど、適宜調整が必要かも。
そしてちょっとコードがダサい。。。
まぁでもこれでやっと全イベントが取れたよ!
pry(main)> last_response.headers['x-total-count']
=> "687"
pry(main)> events.count
=> 687
イベントの詳細を取得する
しかし、このAPIで返ってくるeventはis_full_report==false
なので、リクエストのパラメータまでは取れなかった・・・
see: Bugsnag Data Access API · Apiary / List the Events on an Error
# Get a single event
event = Bugsnag::Api.event("project-id", "event-id")
これで愚直に取るかぁー
event_details = []
events.each do |event|
begin
detail = Bugsnag::Api.event(project_id, event.id)
event_details << detail
wait_a_minite = true unless wait_a_minite
rescue Bugsnag::Api::RateLimitExceeded
if wait_a_minite
puts 'RateLimitExceeded sleep:1 minute'
sleep 60
wait_a_minite = false
else
puts 'RateLimitExceeded sleep:1 sec'
sleep 1
end
retry
end
end
めちゃめちゃ時間かかったけど、取れたよ・・・。
イベント詳細を解析して、パラメータを取得
詳細まで行けば結構属性取れるっぽい。
pry(main)> event_details.first.instance_variable_get(:@attrs).keys
=> [:id, :url, :project_url, :is_full_report, :error_id, :received_at, :exceptions, :threads, :metaData, :customDiagnostics, :request, :app, :device, :user, :breadcrumbs, :context, :severity, :unhandled, :derived]
あとはこんな感じでごにょごにょすれば、、
event_details.each do |detail|
puts detail.request.params
# 実際はここのパラメータをCSVに吐いたりした。
end
できたー
こうして取得したCSVのパラメータを元に、失敗したデータの再処理を行うことができました。
めでたしめでたし。
まとめ
こうしてなんとかBugsnagからパラメータ取得し、平和は保たれましたが、意外と骨が折れました。
今回利用したRubyのtoolkitがまだまだイケてないというのもあったかもしれませんが、直接叩きにいった方がfilterなど使えてよかったのかもしれません。
それかむしろPRのチャンスですかね。
冬休みの宿題にします
追記
体調を崩して15日に投稿できませんでした。すみません