airbrake_stats 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +19 -0
- data/Rakefile +1 -0
- data/airbrake_stats.gemspec +26 -0
- data/bin/airbrake_stats +17 -0
- data/lib/airbrake_stats.rb +14 -0
- data/lib/airbrake_stats/cache.rb +32 -0
- data/lib/airbrake_stats/format.rb +20 -0
- data/lib/airbrake_stats/parser.rb +156 -0
- data/lib/airbrake_stats/queue.rb +7 -0
- data/lib/airbrake_stats/version.rb +3 -0
- metadata +92 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 1.9.3@airbrake_stats --create
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Nathan Witmer
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "airbrake_stats/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "airbrake_stats"
|
7
|
+
s.version = AirbrakeStats::VERSION
|
8
|
+
s.authors = ["Tyler Montgomery"]
|
9
|
+
s.email = ["tyler.a.montgomery@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Analyze Airbrake Errors}
|
12
|
+
s.description = %q{Pass in an Airbrake id to visualize and diagnose errors better.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "airbrake_stats"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
s.add_runtime_dependency "map"
|
24
|
+
s.add_runtime_dependency "nokogiri"
|
25
|
+
s.add_runtime_dependency "http"
|
26
|
+
end
|
data/bin/airbrake_stats
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'airbrake_stats'
|
4
|
+
|
5
|
+
ab_stats = AirbrakeStats::Parser.new(ARGV[0])
|
6
|
+
|
7
|
+
|
8
|
+
# TODO load up info ahead of time like
|
9
|
+
# ab_stats.fetch_data
|
10
|
+
|
11
|
+
# print out the stats for the errors
|
12
|
+
AirbrakeStats::Format.print(ab_stats.stats(:url), :url)
|
13
|
+
AirbrakeStats::Format.print(ab_stats.stats(:path), :path)
|
14
|
+
AirbrakeStats::Format.print(ab_stats.stats(:agent), :agent)
|
15
|
+
AirbrakeStats::Format.print(ab_stats.stats(:format), :format)
|
16
|
+
AirbrakeStats::Format.print(ab_stats.stats(:controller), :controller)
|
17
|
+
AirbrakeStats::Format.print(ab_stats.stats(:error_message), :error_message)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'map'
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'http'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module AirbrakeStats
|
7
|
+
require "airbrake_stats/version"
|
8
|
+
require "airbrake_stats/cache"
|
9
|
+
require "airbrake_stats/format"
|
10
|
+
require "airbrake_stats/parser"
|
11
|
+
require "airbrake_stats/queue"
|
12
|
+
end
|
13
|
+
|
14
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class AirbrakeStats::Cache
|
2
|
+
attr_reader :error_id
|
3
|
+
def initialize(error_id)
|
4
|
+
@error_id = error_id
|
5
|
+
end
|
6
|
+
|
7
|
+
def store(errors)
|
8
|
+
File.open(filename, "w"){|f| f.write errors.to_yaml}
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_errors
|
12
|
+
if File.exists?(filename) && !expired?
|
13
|
+
YAML.load_file(filename).map{|s| Map.new(s)}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def filename
|
18
|
+
"/tmp/airbrake_stats_#{error_id}.yml"
|
19
|
+
end
|
20
|
+
|
21
|
+
def expired?
|
22
|
+
expires_in <= 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def expires_in
|
26
|
+
expires - (Time.now.to_i - File.mtime(filename).to_i)
|
27
|
+
end
|
28
|
+
|
29
|
+
def expires
|
30
|
+
900
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class AirbrakeStats::Format
|
2
|
+
class << AirbrakeStats::Format
|
3
|
+
attr_reader :stats
|
4
|
+
def print(stats, method)
|
5
|
+
@stats = stats
|
6
|
+
method_width = width(method)
|
7
|
+
total_width = width('total')
|
8
|
+
puts
|
9
|
+
puts "#{method.to_s.center(method_width)} | total"
|
10
|
+
puts "#{''.ljust(method_width + 8, '-')}"
|
11
|
+
stats.each do |stat|
|
12
|
+
puts "#{stat[method].to_s.ljust(method_width)} | #{stat.total.to_s.rjust(total_width)}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def width(method)
|
17
|
+
stats.inject(0){|r,s| r = s[method].to_s.length if s[method].to_s.length > r; r}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# TODO cleanup this class, Parser is kind of a bad name.
|
2
|
+
# I'm thinking the http stuff could go in its own class
|
3
|
+
# and the xml parsing can still happen here.
|
4
|
+
#
|
5
|
+
# TODO Also these methods need more documentation. I think
|
6
|
+
# some refactoring will naturally fallout of writing
|
7
|
+
# docs.
|
8
|
+
class AirbrakeStats::Parser
|
9
|
+
attr_accessor :error_id
|
10
|
+
attr_reader :max_pages
|
11
|
+
attr_reader :cache
|
12
|
+
class Error < StandardError; end;
|
13
|
+
|
14
|
+
def initialize(error_id)
|
15
|
+
@error_id = error_id
|
16
|
+
@max_pages = Integer((ENV['MAX_PAGES'] || 10))
|
17
|
+
@cache = AirbrakeStats::Cache.new(error_id)
|
18
|
+
end
|
19
|
+
|
20
|
+
def similar_errors
|
21
|
+
|
22
|
+
return @similar_errors if @similar_errors
|
23
|
+
if @similar_errors = cache.load_errors
|
24
|
+
puts "Loaded cached errors from #{cache.filename} (will expire in #{cache.expires_in/60} minutes)"
|
25
|
+
return @similar_errors
|
26
|
+
end
|
27
|
+
@similar_errors = notices_xml.map do |notice|
|
28
|
+
agent = notice.search('http-user-agent').first
|
29
|
+
if agent
|
30
|
+
# TODO better user agent parsing, but this is good enough to see if
|
31
|
+
# errors are caused by bots or "humans"
|
32
|
+
agent = "#{agent[2]} #{agent[3]}"
|
33
|
+
end
|
34
|
+
id = parse_xml(notice, 'id')
|
35
|
+
url = parse_xml(notice, 'url')
|
36
|
+
format = parse_xml(notice, 'format')
|
37
|
+
path = parse_xml(notice, 'request-path')
|
38
|
+
controller = parse_xml(notice, 'controller')
|
39
|
+
error_message = parse_xml(notice, 'error-message')
|
40
|
+
action = parse_xml(notice, 'action')
|
41
|
+
controller = "#{controller}##{action}"
|
42
|
+
#next unless url && agent && path && format
|
43
|
+
Map.new(id: id, path: path, format: format, error_message: error_message, url: url, agent: agent, controller: controller )
|
44
|
+
end.compact
|
45
|
+
cache.store(@similar_errors)
|
46
|
+
if @similar_errors.empty?
|
47
|
+
puts "no notices had any data"
|
48
|
+
puts notices_xml.map{|n| n.search('error-message').first.text}.uniq
|
49
|
+
else
|
50
|
+
cache.store(@similar_errors)
|
51
|
+
end
|
52
|
+
@similar_errors
|
53
|
+
end
|
54
|
+
|
55
|
+
# Just grab and parse /errors/#{error_id}.xml
|
56
|
+
def error
|
57
|
+
@error ||= parse('')
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_xml(notice, element)
|
61
|
+
node = notice.search(element).first
|
62
|
+
node.text if node
|
63
|
+
end
|
64
|
+
|
65
|
+
# Count up the occurance of either:
|
66
|
+
# - path
|
67
|
+
# - format
|
68
|
+
# - url
|
69
|
+
# - agent
|
70
|
+
# Will return an Array of Maps sorted from lowest to highest occurance
|
71
|
+
def stats(method)
|
72
|
+
stats = Hash.new(0)
|
73
|
+
similar_errors.each{|n| stats[n[method]] += 1}
|
74
|
+
stats = stats.map do |p|
|
75
|
+
h = {total: p.last}
|
76
|
+
h[method] = p.first
|
77
|
+
Map.new(h)
|
78
|
+
end.sort_by(&:total)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Integer representing the ammount of pages of errors. Based on Aibrake's docs
|
82
|
+
# that state they return 30 errors per request, so we divide the total number of
|
83
|
+
# errors by 30 to get our number of pages
|
84
|
+
def page_count
|
85
|
+
@page_count = notices_count/30 + 1
|
86
|
+
@page_count = max_pages if @page_count > max_pages
|
87
|
+
@page_count
|
88
|
+
end
|
89
|
+
|
90
|
+
def max_errors
|
91
|
+
page_count * 30
|
92
|
+
end
|
93
|
+
|
94
|
+
# An Integer representing the number of other similar errors
|
95
|
+
def notices_count
|
96
|
+
@notices_count ||= error.search('notices-count').first.text.to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def similar_error_ids
|
102
|
+
return @similar_error_ids if @similar_error_ids
|
103
|
+
@similar_error_ids = []
|
104
|
+
puts "Getting #{page_count} pages of errors."
|
105
|
+
page_count.times do |page|
|
106
|
+
page += 1
|
107
|
+
errors = parse('/notices', page)
|
108
|
+
@similar_error_ids << errors.search('id').map(&:text)
|
109
|
+
end
|
110
|
+
@similar_error_ids.flatten!
|
111
|
+
if @similar_error_ids.length == max_errors
|
112
|
+
puts "Found #{notices_count} similar errors but only using the #{max_errors} most recent."
|
113
|
+
else
|
114
|
+
puts "Found #{notices_count} similar errors."
|
115
|
+
end
|
116
|
+
@similar_error_ids
|
117
|
+
end
|
118
|
+
|
119
|
+
def notices_xml
|
120
|
+
similar_error_ids
|
121
|
+
notices_xml = AirbrakeStats::Queue.new
|
122
|
+
puts "Downloading errors..."
|
123
|
+
threads = []
|
124
|
+
# TODO sometimes this craps out. Ususally when we ask for pages > 20
|
125
|
+
similar_error_ids.each_slice(4) do |slice|
|
126
|
+
threads << Thread.new(slice) do |ids|
|
127
|
+
ids.each_with_index do |id, index|
|
128
|
+
# print "#{index}/#{notices_count}\r"; $stdout.flush
|
129
|
+
notices_xml << parse("/notices/#{id}")
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
threads.map(&:join)
|
134
|
+
puts notices_xml.size
|
135
|
+
notices_xml.to_a
|
136
|
+
end
|
137
|
+
|
138
|
+
def url
|
139
|
+
@url ||= "https://#{ENV['AIRBRAKE_HOST']}.airbrake.io/errors/#{error_id}"
|
140
|
+
end
|
141
|
+
|
142
|
+
def parse(path, page = nil)
|
143
|
+
params = "?auth_token=#{ENV['AIRBRAKE_TOKEN']}"
|
144
|
+
params << "&page=#{page}" if page
|
145
|
+
# puts "#{url}#{path}.xml#{params}"
|
146
|
+
response = Http.get("#{url}#{path}.xml#{params}", response: :object)
|
147
|
+
parsed_response = Nokogiri::XML(response.body)
|
148
|
+
if response.status == 200
|
149
|
+
return parsed_response
|
150
|
+
else
|
151
|
+
error = parsed_response.search('error').first
|
152
|
+
raise AirbrakeStats::Error.new("#{response.status}: #{error}")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: airbrake_stats
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tyler Montgomery
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-10 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: map
|
16
|
+
requirement: &70336011963120 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70336011963120
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: nokogiri
|
27
|
+
requirement: &70336011962520 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70336011962520
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: http
|
38
|
+
requirement: &70336011961880 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70336011961880
|
47
|
+
description: Pass in an Airbrake id to visualize and diagnose errors better.
|
48
|
+
email:
|
49
|
+
- tyler.a.montgomery@gmail.com
|
50
|
+
executables:
|
51
|
+
- airbrake_stats
|
52
|
+
extensions: []
|
53
|
+
extra_rdoc_files: []
|
54
|
+
files:
|
55
|
+
- .gitignore
|
56
|
+
- .rvmrc
|
57
|
+
- Gemfile
|
58
|
+
- LICENSE
|
59
|
+
- Rakefile
|
60
|
+
- airbrake_stats.gemspec
|
61
|
+
- bin/airbrake_stats
|
62
|
+
- lib/airbrake_stats.rb
|
63
|
+
- lib/airbrake_stats/cache.rb
|
64
|
+
- lib/airbrake_stats/format.rb
|
65
|
+
- lib/airbrake_stats/parser.rb
|
66
|
+
- lib/airbrake_stats/queue.rb
|
67
|
+
- lib/airbrake_stats/version.rb
|
68
|
+
homepage: ''
|
69
|
+
licenses: []
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ! '>='
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: '0'
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project: airbrake_stats
|
88
|
+
rubygems_version: 1.8.5
|
89
|
+
signing_key:
|
90
|
+
specification_version: 3
|
91
|
+
summary: Analyze Airbrake Errors
|
92
|
+
test_files: []
|