airbrake_stats 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []