6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GoodpatchAdvent Calendar 2017

Day 15

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

Last updated at Posted at 2017-12-22

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

追記

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

6
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?