Ruby
bugsnag
GoodpatchDay 15

Bugsnag の Data access API でエラーイベントを取得する

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
にて生成する。

Bugsnag_personal_token.png

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
contexterror_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
  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
  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のチャンスですかね。
冬休みの宿題にします :hugging:

追記

体調を崩して15日に投稿できませんでした。すみません :bow: