airbrake_tools 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ spec/fixtures.yml
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- airbrake_tools (0.0.1)
4
+ airbrake_tools (0.0.2)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
data/Readme.md CHANGED
@@ -1,5 +1,9 @@
1
1
  Power tools for Airbrake
2
2
 
3
+ - hottest errors
4
+ - list / search errors
5
+ - analyze 1 error (ocurrance graphs / different backtraces + frequencies)
6
+
3
7
  Install
4
8
  =======
5
9
 
@@ -8,11 +12,77 @@ Install
8
12
  Usage
9
13
  =====
10
14
 
11
- CODE EXAMPLE
15
+ airbrake_tools your-account your-auth-token
16
+
17
+ Output
18
+ ======
19
+
20
+ ### Hot
21
+
22
+ ```
23
+ airbrake-tools your-account your-auth-token hot
24
+ .............................
25
+ #1 793.5/hour 2170:total ▁▂▂█
26
+ --> id: 51344729 -- first: 2012-06-25 15:47:11 UTC -- Mysql2::Error -- Mysql2::Error: Lost connection to MySQL server at 'reading initial communication packet', system error: 110
27
+ #2 595.6/hour 648:total ▁▂▄█
28
+ --> id: 53991244 -- first: 2012-12-13 20:31:26 UTC -- ActiveRecord::RecordInvalid -- ActiveRecord::RecordInvalid: Validation failed: Requester is suspended.
29
+ #3 458.0/hour 191840:total ▁▁▁▄█
30
+ --> id: 53864752 -- first: 2012-12-06 19:57:12 UTC -- SyntaxError -- [retrying processing mail at 782bcb63887c.eml] SyntaxError: unterminated quoted-word
31
+ #4 315.3/hour 5184:total ▆▅▁▁▂▁▆▆█▅
32
+ --> id: 52897649 -- first: 2012-10-14 02:10:41 UTC -- Http::ClientError -- [The server responded with status 500]
33
+ ```
34
+
35
+ ### List
36
+
37
+ Shows all errors divided by pages
38
+ - search
39
+ - "fix all errors on page x"
40
+
41
+ ```
42
+ airbrake-tools your-account your-auth-token list | grep 'is suspended'
43
+ Page 1 ----------
44
+ 54054554 -- ActiveRecord::RecordInvalid -- ActiveRecord::RecordInvalid: Validation failed: Requester is suspended.
45
+ Page 2 ----------
46
+ ...
47
+ ```
48
+
49
+ ### Summary
50
+
51
+ - show details for 1 error (combines 150 notices)
52
+ - show all different traces that caused this error (play with --compare-depth)
53
+
54
+ ```
55
+ airbrake-tools your-account your-auth-token summary 51344729
56
+ last retrieved notice: 1 hours ago at 2012-12-19 22:43:20 UTC
57
+ last 2 hours: ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▂▂▂▅▃█
58
+ last day: ▁
59
+ Trace 1: occurred 100 times e.g. 7145616660, 7145614849
60
+ Mysql2::Error: Lost connection to MySQL server at 'reading initial communication packet', system error
61
+ ./mysql2/lib/mysql2/client.rb:44:in `connect'
62
+ ...
63
+
64
+ Trace 2: occurred 10 times e.g. 7145613107, 7145612108
65
+ Mysql2::Error: Lost connection to MySQL server
66
+ ./mysql2/lib/mysql2/client.rb:58:in `disconnect'
67
+ ...
68
+
69
+ Trace 3: occurred 5 times e.g. 7145609979, 7145609161
70
+ Mysql2::Error: Lost connection to MySQL server during reconnect
71
+ ./mysql2/lib/mysql2/client.rb:78:in `reconnect'
72
+ ...
73
+ ```
74
+
75
+ ### Options
76
+
77
+ ```
78
+ -c, --compare-depth NUM At what level to compare backtraces (default: 7)
79
+ -e, --environment ENV Only show errors from this environment (default: production)
80
+ -h, --help Show this.
81
+ -v, --version Show Version
82
+ ```
12
83
 
13
84
  Author
14
85
  ======
15
86
  [Jonathan Cheatham](http://github.com/jcheatham)<br/>
16
87
  coaxis@gmail.com<br/>
17
- License: MIT<br/>
18
- [![Build Status](https://travis-ci.org/jcheatham/airbrake_tools.png)](https://travis-ci.org/jcheatham/airbrake_tools)
88
+ License: MIT
@@ -0,0 +1,5 @@
1
+ #! /usr/bin/env ruby
2
+ require 'optparse'
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'airbrake_tools'
5
+ exit AirbrakeTools.cli(ARGV)
@@ -1,3 +1,3 @@
1
1
  module AirbrakeTools
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -1,7 +1,11 @@
1
+ # encoding: UTF-8
1
2
  require "airbrake_tools/version"
2
3
  require "airbrake-api"
3
4
 
4
5
  module AirbrakeTools
6
+ DEFAULT_COMPARE_DEPTH = 7
7
+ DEFAULT_ENVIRONMENT = "production"
8
+
5
9
  class << self
6
10
  def cli(argv)
7
11
  options = extract_options(argv)
@@ -15,41 +19,85 @@ module AirbrakeTools
15
19
  return 1
16
20
  end
17
21
 
18
- hot(options) || 0
22
+ case ARGV[2]
23
+ when "hot"
24
+ errors = hot(options)
25
+ print_errors(errors)
26
+ when "list"
27
+ list(options)
28
+ when "summary"
29
+ summary(ARGV[3] || raise("Need error id"), options)
30
+ else
31
+ raise "Unknown command try hot/list/summary"
32
+ end
33
+ return 0
19
34
  end
20
35
 
21
- def hot(options)
22
- puts "Calling hot with #{AirbrakeAPI.account}, #{AirbrakeAPI.auth_token}, #{options.inspect}"
23
-
24
- pages = 1
25
-
36
+ def hot(options = {})
37
+ pages = (options[:pages] || 1).to_i
26
38
  errors = []
27
39
  pages.times do |i|
28
- errors.concat (AirbrakeAPI.errors(:page => i+1) || []).select{|e| e.rails_env == "production" }
40
+ errors.concat(AirbrakeAPI.errors(:page => i+1) || [])
29
41
  end
42
+ select_env!(errors, options)
30
43
 
31
44
  errors = Parallel.map(errors, :in_threads => 10) do |error|
32
45
  begin
33
46
  notices = AirbrakeAPI.notices(error.id, :pages => 1, :raw => true).compact
34
47
  print "."
35
- [error, notices]
48
+ [error, notices, frequency(notices)]
36
49
  rescue Faraday::Error::ParsingError
37
- $stderr.puts "Ignoring #{summary(error)}, got 500 from http://#{AirbrakeAPI.account}.airbrake.io/errors/#{error.id}"
50
+ $stderr.puts "Ignoring #{hot_summary(error)}, got 500 from http://#{AirbrakeAPI.account}.airbrake.io/errors/#{error.id}"
38
51
  end
39
52
  end.compact
40
53
 
41
- $stderr.puts
54
+ errors.sort_by{|e,n,f| f }.reverse
55
+ end
56
+
57
+ def list(options)
58
+ page = 1
59
+ while errors = AirbrakeAPI.errors(:page => page)
60
+ select_env!(errors, options)
61
+ errors.each do |error|
62
+ puts "#{error.id} -- #{error.error_class} -- #{error.error_message} -- #{error.created_at}"
63
+ end
64
+ $stderr.puts "Page #{page} ----------\n"
65
+ page += 1
66
+ end
67
+ end
68
+
69
+ def summary(error_id, options)
70
+ compare_depth = options[:compare_depth] || DEFAULT_COMPARE_DEPTH
71
+ notices = AirbrakeAPI.notices(error_id, :pages => 5)
42
72
 
43
- errors.sort_by{|e,n| frequency(n) }.reverse.each_with_index do |(error, notices), index|
44
- puts "##{(index+1).to_s.ljust(2)} #{frequency(notices).to_s.rjust(8)}/hour #{error.notices_count.to_s.rjust(6)}:total #{sparkline(notices, :slots => 60, :interval => 60)}"
45
- puts " --> id: #{error.id} -- first: #{error.created_at} -- #{error.error_class} -- #{error.error_message}"
73
+ puts "last retrieved notice: #{((Time.now - notices.last.created_at) / (60 * 60)).round} hours ago at #{notices.last.created_at}"
74
+ puts "last 2 hours: #{sparkline(notices, :slots => 60, :interval => 120)}"
75
+ puts "last day: #{sparkline(notices, :slots => 24, :interval => 60 * 60)}"
76
+
77
+ backtraces = notices.compact.select{|n| n.backtrace }.group_by do |notice|
78
+ notice.backtrace.first[1][0..compare_depth]
46
79
  end
47
80
 
48
- return 0
81
+ backtraces.sort_by{|_,notices| notices.size }.reverse.each_with_index do |(backtrace, notices), index|
82
+ puts "Trace #{index + 1}: occurred #{notices.size} times e.g. #{notices[0..5].map(&:id).join(", ")}"
83
+ puts notices.first.error_message
84
+ puts backtrace.map{|line| line.sub("[PROJECT_ROOT]/", "./") }
85
+ puts ""
86
+ end
49
87
  end
50
88
 
51
89
  private
52
90
 
91
+ def select_env!(errors, options)
92
+ errors.select!{|e| e.rails_env == options[:env] || DEFAULT_ENVIRONMENT }
93
+ end
94
+
95
+ def print_errors(hot)
96
+ hot.each_with_index do |(error, notices, rate, deviance), index|
97
+ puts "\n##{(index+1).to_s.ljust(2)} #{rate.round(2).to_s.rjust(6)}/hour total:#{error.notices_count.to_s.ljust(8)} #{sparkline(notices, :slots => 60, :interval => 60).ljust(61)} -- #{summary(error)}"
98
+ end
99
+ end
100
+
53
101
  def frequency(notices)
54
102
  hour = 60 * 60
55
103
  sum_of_ages = notices.map { |n| Time.now - n.created_at }.inject(&:+)
@@ -59,45 +107,34 @@ module AirbrakeTools
59
107
  (rate * hour).round(1)
60
108
  end
61
109
 
62
- def summary(error)
110
+ def hot_summary(error)
63
111
  "id:#{error.id} -- first:#{error.created_at} -- #{error.error_class} -- #{error.error_message}"
64
112
  end
65
113
 
66
114
  def extract_options(argv)
67
- options = {
68
- }
115
+ options = {}
69
116
  OptionParser.new do |opts|
70
117
  opts.banner = <<-BANNER.gsub(" "*12, "")
71
- Get the hotest airbrake errors
118
+ Power tools for airbrake.
119
+
120
+ hot: list hottest errors
121
+ list: list errors 1-by-1 so you can e.g. grep -> search
122
+ summary: analyze occurrences and backtrace origins
72
123
 
73
124
  Usage:
74
- airbrake-tools subdomain token [options]
75
- token: go to airbrake -> settings, copy your auth token
125
+ airbrake-tools subdomain auth-token command [options]
126
+ auth-token: go to airbrake -> settings, copy your auth-token
76
127
 
77
128
  Options:
78
129
  BANNER
130
+ opts.on("-c NUM", "--compare-depth NUM", Integer, "How deep to compare backtraces (default: #{DEFAULT_COMPARE_DEPTH})") {|s| options[:env] = s }
131
+ opts.on("-e ENV", "--environment ENV", String, "Only show errors from this environment (default: #{DEFAULT_ENVIRONMENT})") {|s| options[:env] = s }
79
132
  opts.on("-h", "--help", "Show this.") { puts opts; exit }
80
133
  opts.on("-v", "--version", "Show Version"){ puts "airbrake-tools #{VERSION}"; exit }
81
134
  end.parse!(argv)
82
135
  options
83
136
  end
84
137
 
85
- def run(cmd)
86
- all = ""
87
- puts cmd
88
- IO.popen(cmd) do |pipe|
89
- while str = pipe.gets
90
- all << str
91
- puts str
92
- end
93
- end
94
- [$?.success?, all]
95
- end
96
-
97
- def run!(command)
98
- raise "Command failed #{command}" unless run(command).first
99
- end
100
-
101
138
  def sparkline_data(notices, options)
102
139
  last = notices.last.created_at
103
140
  now = Time.now
@@ -112,6 +149,5 @@ module AirbrakeTools
112
149
  def sparkline(notices, options)
113
150
  `#{File.expand_path('../../spark.sh',__FILE__)} #{sparkline_data(notices, options).join(" ")}`.strip
114
151
  end
115
-
116
152
  end
117
153
  end
data/spark.sh ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # spark
4
+ # https://github.com/holman/spark
5
+ #
6
+ # Generates sparklines for a set of data.
7
+ #
8
+ # Here's a a good web-based sparkline generator that was a bit of inspiration
9
+ # for spark:
10
+ #
11
+ # https://datacollective.org/sparkblocks
12
+ #
13
+ # spark takes a comma-separated list of data and then prints a sparkline out of
14
+ # it.
15
+ #
16
+ # Examples:
17
+ #
18
+ # spark 1 5 22 13 53
19
+ # # => ▁▁▃▂▇
20
+ #
21
+ # spark 0 30 55 80 33 150
22
+ # # => ▁▂▃▅▂▇
23
+ #
24
+ # spark -h
25
+ # # => Prints the spark help text.
26
+
27
+ # Generates sparklines.
28
+ #
29
+ # $1 - The data we'd like to graph.
30
+ spark()
31
+ {
32
+ local n numbers=
33
+
34
+ # find min/max values
35
+ local min=0xffffffff max=0
36
+
37
+ for n in ${@//,/ }
38
+ do
39
+ # on Linux (or with bash4) we could use `printf %.0f $n` here to
40
+ # round the number but that doesn't work on OS X (bash3) nor does
41
+ # `awk '{printf "%.0f",$1}' <<< $n` work, so just cut it off
42
+ n=${n%.*}
43
+ (( n < min )) && min=$n
44
+ (( n > max )) && max=$n
45
+ numbers=$numbers${numbers:+ }$n
46
+ done
47
+
48
+ # print ticks
49
+ local ticks=(▁ ▂ ▃ ▄ ▅ ▆ ▇ █)
50
+
51
+ local f=$(( (($max-$min)<<8)/(${#ticks[@]}-1) ))
52
+ (( f < 1 )) && f=1
53
+
54
+ for n in $numbers
55
+ do
56
+ echo -n ${ticks[$(( ((($n-$min)<<8)/$f) ))]}
57
+ done
58
+ echo
59
+ }
60
+
61
+ # If we're being sourced, don't worry about such things
62
+ if [ "$BASH_SOURCE" == "$0" ]; then
63
+ # Prints the help text for spark.
64
+ help()
65
+ {
66
+ cat <<EOF
67
+
68
+ USAGE:
69
+ spark [-h] VALUE,...
70
+
71
+ EXAMPLES:
72
+ spark 1 5 22 13 53
73
+ ▁▁▃▂█
74
+ spark 0,30,55,80,33,150
75
+ ▁▂▃▄▂█
76
+ echo 9 13 5 17 1 | spark
77
+ ▄▆▂█▁
78
+ EOF
79
+ }
80
+
81
+ # show help for no arguments if stdin is a terminal
82
+ if { [ -z "$1" ] && [ -t 0 ] ; } || [ "$1" == '-h' ]
83
+ then
84
+ help
85
+ exit 0
86
+ fi
87
+
88
+ spark ${@:-`cat`}
89
+ fi
@@ -44,8 +44,24 @@ describe "airbrake-tools" do
44
44
 
45
45
  describe "hot" do
46
46
  it "kinda works" do
47
- output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]}")
48
- output.should =~ /#\d+\s+\d+\.\d+\/hour\s+\d+:total/
47
+ output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} hot")
48
+ output.should =~ /#\d+\s+\d+\.\d+\/hour\s+total:\d+/
49
+ end
50
+ end
51
+
52
+ describe "list" do
53
+ it "kinda works" do
54
+ output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} list")
55
+ output.should include("Page 1 ")
56
+ output.should =~ /^\d+/
57
+ end
58
+ end
59
+
60
+ describe "summary" do
61
+ it "kinda works" do
62
+ output = airbrake_tools("#{config["subdomain"]} #{config["auth_token"]} summary 51344729")
63
+ output.should include("last retrieved notice: ")
64
+ output.should include("last 2 hours: ")
49
65
  end
50
66
  end
51
67
  end
@@ -0,0 +1,2 @@
1
+ subdomain: xxx
2
+ auth_token: yyy # NOT THE API KEY <-> got to airbrake, click on settings, copy auth_token
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: airbrake_tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-07 00:00:00.000000000 Z
12
+ date: 2012-12-20 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description:
15
15
  email: coaxis@gmail.com
@@ -17,15 +17,18 @@ executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
19
19
  files:
20
- - .travis.yml
20
+ - .gitignore
21
21
  - Gemfile
22
22
  - Gemfile.lock
23
23
  - Rakefile
24
24
  - Readme.md
25
25
  - airbrake_tools.gemspec
26
+ - bin/airbrake-tools
26
27
  - lib/airbrake_tools.rb
27
28
  - lib/airbrake_tools/version.rb
29
+ - spark.sh
28
30
  - spec/airbrake_tools_spec.rb
31
+ - spec/fixtures.example.yml
29
32
  - spec/spec_helper.rb
30
33
  homepage: http://github.com/jcheatham/airbrake_tools
31
34
  licenses:
data/.travis.yml DELETED
@@ -1,4 +0,0 @@
1
- rvm:
2
- - ree
3
- - 1.9.2
4
- - 1.9.3