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.
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.3@airbrake_stats --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in airbrake_stats.gemspec
4
+ gemspec
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.
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ class AirbrakeStats::Queue < Queue
2
+ def to_a
3
+ array = []
4
+ self.size.times{array << self.pop}
5
+ array
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module AirbrakeStats
2
+ VERSION = "0.0.1"
3
+ 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: []