airbrake_tools 0.0.1 → 0.0.2

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 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