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 +1 -0
- data/Gemfile.lock +1 -1
- data/Readme.md +73 -3
- data/bin/airbrake-tools +5 -0
- data/lib/airbrake_tools/version.rb +1 -1
- data/lib/airbrake_tools.rb +73 -37
- data/spark.sh +89 -0
- data/spec/airbrake_tools_spec.rb +18 -2
- data/spec/fixtures.example.yml +2 -0
- metadata +6 -3
- data/.travis.yml +0 -4
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
spec/fixtures.yml
|
data/Gemfile.lock
CHANGED
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
|
-
|
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
|
18
|
-
[![Build Status](https://travis-ci.org/jcheatham/airbrake_tools.png)](https://travis-ci.org/jcheatham/airbrake_tools)
|
88
|
+
License: MIT
|
data/bin/airbrake-tools
ADDED
data/lib/airbrake_tools.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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 #{
|
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
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
data/spec/airbrake_tools_spec.rb
CHANGED
@@ -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
|
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
|
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.
|
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-
|
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
|
-
- .
|
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