radar 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -1
- data/CHANGELOG.md +21 -0
- data/Gemfile +3 -2
- data/Gemfile.lock +47 -48
- data/README.md +14 -25
- data/docs/user_guide.md +155 -30
- data/examples/rack/config.ru +1 -1
- data/examples/sinatra/README.md +15 -0
- data/examples/sinatra/example.rb +18 -0
- data/lib/radar.rb +11 -5
- data/lib/radar/application.rb +14 -3
- data/lib/radar/backtrace.rb +42 -0
- data/lib/radar/config.rb +112 -20
- data/lib/radar/data_extensions/rack.rb +4 -21
- data/lib/radar/data_extensions/rails2.rb +31 -0
- data/lib/radar/data_extensions/request_helper.rb +28 -0
- data/lib/radar/exception_event.rb +10 -4
- data/lib/radar/integration/rails2.rb +31 -0
- data/lib/radar/integration/rails2/action_controller_rescue.rb +30 -0
- data/lib/radar/integration/rails3.rb +0 -2
- data/lib/radar/integration/rails3/railtie.rb +0 -2
- data/lib/radar/integration/sinatra.rb +21 -0
- data/lib/radar/matchers/local_request_matcher.rb +43 -0
- data/lib/radar/reporter/hoptoad_reporter.rb +204 -0
- data/lib/radar/reporter/logger_reporter.rb +2 -3
- data/lib/radar/version.rb +1 -1
- data/radar.gemspec +1 -0
- data/test/radar/application_test.rb +39 -18
- data/test/radar/backtrace_test.rb +42 -0
- data/test/radar/config_test.rb +49 -4
- data/test/radar/data_extensions/rails2_test.rb +14 -0
- data/test/radar/exception_event_test.rb +9 -0
- data/test/radar/integration/rack_test.rb +1 -1
- data/test/radar/integration/sinatra_test.rb +13 -0
- data/test/radar/matchers/local_request_matcher_test.rb +26 -0
- data/test/radar/reporter/hoptoad_reporter_test.rb +20 -0
- data/test/radar/reporter/logger_reporter_test.rb +0 -4
- metadata +41 -12
data/examples/rack/config.ru
CHANGED
@@ -5,7 +5,7 @@ require "radar"
|
|
5
5
|
# Create a Radar::Application, configured to simply log to the
|
6
6
|
# STDERR stream.
|
7
7
|
app = Radar::Application.new(:rack_example) do |a|
|
8
|
-
a.
|
8
|
+
a.reporter :io, :io_object => STDERR
|
9
9
|
end
|
10
10
|
|
11
11
|
# Use the Radar Rack middleware for the created application,
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Radar Examples: Sinatra
|
2
|
+
|
3
|
+
This example shows Radar's Sinatra integration.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
First make sure you install the dependencies using Bundler, then just
|
8
|
+
run `rackup` (`bundle exec` is used to verify that it uses the bundle
|
9
|
+
environment to get the binary):
|
10
|
+
|
11
|
+
bundle install
|
12
|
+
ruby example.rb
|
13
|
+
|
14
|
+
Then access `localhost:4567`, which should throw an exception. Go back
|
15
|
+
to your console and see that Radar caught and reported the exception!
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "bundler/setup"
|
3
|
+
require "radar"
|
4
|
+
require "sinatra"
|
5
|
+
|
6
|
+
Radar::Application.new(:sinatra_example) do |a|
|
7
|
+
a.reporter :io, :io_object => STDERR
|
8
|
+
end
|
9
|
+
|
10
|
+
class MyApp < Sinatra::Base
|
11
|
+
use Rack::Radar, :application => Radar[:sinatra_example]
|
12
|
+
|
13
|
+
get '/' do
|
14
|
+
raise "UH OH"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
MyApp.run!
|
data/lib/radar.rb
CHANGED
@@ -4,6 +4,7 @@ require 'radar/integration/rails3/railtie' if defined?(Rails::Railtie)
|
|
4
4
|
|
5
5
|
module Radar
|
6
6
|
autoload :Application, 'radar/application'
|
7
|
+
autoload :Backtrace, 'radar/backtrace'
|
7
8
|
autoload :Config, 'radar/config'
|
8
9
|
autoload :ExceptionEvent, 'radar/exception_event'
|
9
10
|
autoload :Logger, 'radar/logger'
|
@@ -12,7 +13,8 @@ module Radar
|
|
12
13
|
|
13
14
|
module DataExtensions
|
14
15
|
autoload :HostEnvironment, 'radar/data_extensions/host_environment'
|
15
|
-
autoload :Rack,
|
16
|
+
autoload :Rack, 'radar/data_extensions/rack'
|
17
|
+
autoload :Rails2, 'radar/data_extensions/rails2'
|
16
18
|
end
|
17
19
|
|
18
20
|
module Filters
|
@@ -20,17 +22,21 @@ module Radar
|
|
20
22
|
end
|
21
23
|
|
22
24
|
module Integration
|
23
|
-
autoload :Rack,
|
24
|
-
autoload :
|
25
|
+
autoload :Rack, 'radar/integration/rack'
|
26
|
+
autoload :Rails2, 'radar/integration/rails2'
|
27
|
+
autoload :Rails3, 'radar/integration/rails3'
|
28
|
+
autoload :Sinatra, 'radar/integration/sinatra'
|
25
29
|
end
|
26
30
|
|
27
31
|
module Matchers
|
28
|
-
autoload :BacktraceMatcher,
|
29
|
-
autoload :ClassMatcher,
|
32
|
+
autoload :BacktraceMatcher, 'radar/matchers/backtrace_matcher'
|
33
|
+
autoload :ClassMatcher, 'radar/matchers/class_matcher'
|
34
|
+
autoload :LocalRequestMatcher, 'radar/matchers/local_request_matcher'
|
30
35
|
end
|
31
36
|
|
32
37
|
class Reporter
|
33
38
|
autoload :FileReporter, 'radar/reporter/file_reporter'
|
39
|
+
autoload :HoptoadReporter,'radar/reporter/hoptoad_reporter'
|
34
40
|
autoload :IoReporter, 'radar/reporter/io_reporter'
|
35
41
|
autoload :LoggerReporter, 'radar/reporter/logger_reporter'
|
36
42
|
end
|
data/lib/radar/application.rb
CHANGED
@@ -19,7 +19,8 @@ module Radar
|
|
19
19
|
attr_reader :name
|
20
20
|
attr_reader :creation_location
|
21
21
|
|
22
|
-
def_delegators :config, :reporters, :data_extensions, :matchers, :filters
|
22
|
+
def_delegators :config, :reporters, :data_extensions, :matchers, :filters, :rejecters,
|
23
|
+
:reporter, :data_extension, :match, :filter, :reject
|
23
24
|
|
24
25
|
# Looks up an application which was registered with the given name.
|
25
26
|
#
|
@@ -95,18 +96,28 @@ module Radar
|
|
95
96
|
def report(exception, extra=nil)
|
96
97
|
data = ExceptionEvent.new(self, exception, extra)
|
97
98
|
|
99
|
+
# If there are rejecters, then verify that they all fail
|
100
|
+
if !config.rejecters.empty?
|
101
|
+
config.rejecters.values.each do |r|
|
102
|
+
if r.call(data)
|
103
|
+
logger.info("Ignoring exception. Matches rejecter: #{r}")
|
104
|
+
return
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
98
109
|
# If there are matchers, then verify that at least one matches
|
99
110
|
# before continuing
|
100
111
|
if !config.matchers.empty?
|
101
112
|
return if !config.matchers.values.find do |m|
|
102
|
-
m.
|
113
|
+
m.call(data) && logger.info("Reporting exception. Matches: #{m}")
|
103
114
|
end
|
104
115
|
end
|
105
116
|
|
106
117
|
# Report the exception to each of the reporters
|
107
118
|
logger.info "Invoking reporters for exception: #{exception.class}"
|
108
119
|
config.reporters.values.each do |reporter|
|
109
|
-
reporter.
|
120
|
+
reporter.call(data)
|
110
121
|
end
|
111
122
|
end
|
112
123
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Radar
|
2
|
+
# The backtrace class helps to parse the given Ruby backtrace
|
3
|
+
# lines into proper file, line, and method, so it can better be
|
4
|
+
# organized and filtered later.
|
5
|
+
class Backtrace < Array
|
6
|
+
attr_reader :original
|
7
|
+
|
8
|
+
def initialize(backtrace)
|
9
|
+
@original = backtrace
|
10
|
+
parse if backtrace
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
# Parses the backtrace into the proper {Line} objects which
|
16
|
+
# are inserted into the array.
|
17
|
+
def parse
|
18
|
+
original.each do |line|
|
19
|
+
push(Entry.new(line))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Represents a single line of a backtrace, giving access to the
|
24
|
+
# file, line, and method.
|
25
|
+
class Entry < Hash
|
26
|
+
def initialize(line)
|
27
|
+
# Regex pulled from HoptoadNotifier. Thanks!
|
28
|
+
_, file, line, method = line.match(/^([^:]+):(\d+)(?::in `([^']+)')?$/).to_a
|
29
|
+
self[:file] = file
|
30
|
+
self[:line] = line
|
31
|
+
self[:method] = method
|
32
|
+
end
|
33
|
+
|
34
|
+
# Helpers to access the file, line, and method.
|
35
|
+
[:file, :line, :method].each do |attr|
|
36
|
+
define_method(attr) do
|
37
|
+
self[attr]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/radar/config.rb
CHANGED
@@ -7,6 +7,7 @@ module Radar
|
|
7
7
|
attr_reader :reporters
|
8
8
|
attr_reader :data_extensions
|
9
9
|
attr_reader :matchers
|
10
|
+
attr_reader :rejecters
|
10
11
|
attr_reader :filters
|
11
12
|
attr_accessor :log_location
|
12
13
|
|
@@ -14,12 +15,58 @@ module Radar
|
|
14
15
|
@reporters = UseArray.new(&method(:add_reporter))
|
15
16
|
@data_extensions = UseArray.new(&method(:add_data_extension))
|
16
17
|
@matchers = UseArray.new(&method(:add_matcher))
|
18
|
+
@rejecters = UseArray.new(&method(:add_matcher))
|
17
19
|
@filters = UseArray.new(&method(:add_filter))
|
18
20
|
@log_location = nil
|
19
21
|
|
20
22
|
@data_extensions.use DataExtensions::HostEnvironment
|
21
23
|
end
|
22
24
|
|
25
|
+
# Adds a reporter to the application. Unlike most exception notifiers,
|
26
|
+
# Radar on its own doesn't actually do anything with the exception data
|
27
|
+
# once it has processed it. Instead, it is up to reporters to take the
|
28
|
+
# exception data and do something with it. Using this method, you can
|
29
|
+
# enable reporters.
|
30
|
+
#
|
31
|
+
# Built-in reporters can be accessed via a symbol:
|
32
|
+
#
|
33
|
+
# config.reporter :file
|
34
|
+
#
|
35
|
+
# Custom reporters can be accessed via a class:
|
36
|
+
#
|
37
|
+
# config.reporter MyCustomReporter
|
38
|
+
#
|
39
|
+
# And for simple reporters, you may even just use a block:
|
40
|
+
#
|
41
|
+
# config.reporter do |event|
|
42
|
+
# # Do something with the event
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# Any arguments other than the first, including any given blocks,
|
46
|
+
# are passed on to the reporter class.
|
47
|
+
def reporter(*args, &block)
|
48
|
+
@reporters.use(*args, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Adds a data extension to the application. By default, the exception
|
52
|
+
# data generated by Radar is quite lean and only includes the basic
|
53
|
+
# information about the application and exception. Through the use
|
54
|
+
# of data extensions, you can add any sort of information to the
|
55
|
+
# exception data that you want.
|
56
|
+
#
|
57
|
+
# Built-in data extensions can be enabled via a symbol:
|
58
|
+
#
|
59
|
+
# config.data_extension :host_environment
|
60
|
+
#
|
61
|
+
# Custom data extensions can be enabled via a class:
|
62
|
+
#
|
63
|
+
# config.data_extension MyCustomExtension
|
64
|
+
#
|
65
|
+
# Any arguments given will be passed on to the data extension class.
|
66
|
+
def data_extension(*args, &block)
|
67
|
+
@data_extensions.use(*args, &block)
|
68
|
+
end
|
69
|
+
|
23
70
|
# Adds a matcher rule to the application. An application will only
|
24
71
|
# report an exception if the event agrees with at least one of the
|
25
72
|
# matchers.
|
@@ -37,23 +84,54 @@ module Radar
|
|
37
84
|
# config.match Radar::Matchers::ClassMatcher, StandardError
|
38
85
|
#
|
39
86
|
# Radar will then use the specified class as the matcher.
|
87
|
+
def match(*args, &block)
|
88
|
+
@matchers.use(*args, &block)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Adds a rejecter rule to the application. A rejecter is the same as a
|
92
|
+
# matcher, so if you're not familiar with matchers, please read the documentation
|
93
|
+
# above {#match} first. The only difference with a rejecter is that if
|
94
|
+
# any of the rejecters return true, then the exception event is not sent
|
95
|
+
# to reporters.
|
40
96
|
#
|
41
|
-
|
42
|
-
|
97
|
+
# Another important note is that rejecters always take precedence over
|
98
|
+
# matchers. So even if a matcher would have matched the exception, if it
|
99
|
+
# matches a rejecter, then it won't continue.
|
100
|
+
def reject(*args, &block)
|
101
|
+
@rejecters.use(*args, &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Adds a filter to the application. Filters provide a method of filtering
|
105
|
+
# the exception data just prior to the data being sent to the reporters.
|
106
|
+
# This enables you to filter out sensitive information such as passwords,
|
107
|
+
# or even just to remove unnecessary keys.
|
108
|
+
#
|
109
|
+
# Built-in filters can be enabled using a symbol shortcut:
|
110
|
+
#
|
111
|
+
# config.filter :key, :key => :password
|
112
|
+
#
|
113
|
+
# Custom filters can be enabled using a class:
|
114
|
+
#
|
115
|
+
# config.filter MyCustomFilter
|
116
|
+
#
|
117
|
+
# Or simple filters can be created using lambda functions:
|
118
|
+
#
|
119
|
+
# config.filter do |data|
|
120
|
+
# # do something with the data and return it
|
121
|
+
# data
|
122
|
+
# end
|
123
|
+
#
|
124
|
+
# Any arguments given will be passed on to the filter class.
|
125
|
+
def filter(*args, &block)
|
126
|
+
@filters.use(*args, &block)
|
43
127
|
end
|
44
128
|
|
45
129
|
protected
|
46
130
|
|
47
131
|
# The callback that is used to add a reporter to the {UseArray}
|
48
132
|
# when `reporters.use` is called.
|
49
|
-
def add_reporter(
|
50
|
-
|
51
|
-
|
52
|
-
block = args.pop if args.last.is_a?(Proc)
|
53
|
-
instance = klass.new(*args)
|
54
|
-
block.call(instance) if block
|
55
|
-
|
56
|
-
[klass, instance]
|
133
|
+
def add_reporter(*args)
|
134
|
+
callable_method(:report, "Radar::Reporter::%sReporter", *args)
|
57
135
|
end
|
58
136
|
|
59
137
|
# The callback that is used to add a data extension to the {UseArray}
|
@@ -65,26 +143,40 @@ module Radar
|
|
65
143
|
|
66
144
|
# The callback that is used to add a matcher to the {UseArray}
|
67
145
|
# when `matchers.use` is called.
|
68
|
-
def add_matcher(
|
69
|
-
|
70
|
-
[matcher, matcher.new(*args)]
|
146
|
+
def add_matcher(*args)
|
147
|
+
callable_method(:matches?, "Radar::Matchers::%sMatcher", *args)
|
71
148
|
end
|
72
149
|
|
73
150
|
# The callback that is used to add a filter to the {UseArray}
|
74
151
|
# when `filters.use` is called.
|
75
152
|
def add_filter(*args)
|
76
|
-
|
77
|
-
|
153
|
+
callable_method(:filter, "Radar::Filters::%sFilter", *args)
|
154
|
+
end
|
155
|
+
|
156
|
+
def callable_method(method, inflectorspace, *args)
|
157
|
+
block = args.pop if args.length == 1 && args.first.is_a?(Proc)
|
158
|
+
raise ArgumentError.new("Requires at least a class or a lambda to be given.") if args.empty? && !block
|
159
|
+
name = block || args.first
|
78
160
|
|
79
161
|
if !args.empty?
|
80
|
-
# Detect the proper class then get the `filter` method from it,
|
81
|
-
# since that is all we care about
|
82
162
|
klass = args.shift
|
83
|
-
|
84
|
-
|
163
|
+
|
164
|
+
if !klass.is_a?(Class)
|
165
|
+
space = inflectorspace % Support::Inflector.camelize(klass)
|
166
|
+
klass = Support::Inflector.constantize(space)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Instantiate the class and yield the block if it was given
|
170
|
+
# with the instance.
|
171
|
+
instance_block = args.pop if args.last.is_a?(Proc)
|
172
|
+
instance = klass.new(*args)
|
173
|
+
instance_block.call(instance) if instance_block
|
174
|
+
|
175
|
+
# Store the callable method away as the callable for later.
|
176
|
+
block = instance.method(method)
|
85
177
|
end
|
86
178
|
|
87
|
-
[
|
179
|
+
[name, block]
|
88
180
|
end
|
89
181
|
end
|
90
182
|
|
@@ -1,8 +1,12 @@
|
|
1
|
+
require 'radar/data_extensions/request_helper'
|
2
|
+
|
1
3
|
module Radar
|
2
4
|
module DataExtensions
|
3
5
|
# Data extensions which adds information about a rack request,
|
4
6
|
# if it exists in the `:rack_request` extra data of the {ExceptionEvent}.
|
5
7
|
class Rack
|
8
|
+
include RequestHelper
|
9
|
+
|
6
10
|
def initialize(event)
|
7
11
|
@event = event
|
8
12
|
end
|
@@ -31,27 +35,6 @@ module Radar
|
|
31
35
|
|
32
36
|
protected
|
33
37
|
|
34
|
-
# Extracts only the HTTP headers from the rack environment,
|
35
|
-
# converting them to the proper HTTP format: `HTTP_CONTENT_TYPE`
|
36
|
-
# to `Content-Type`
|
37
|
-
#
|
38
|
-
# @param [Hash] env
|
39
|
-
# @return [Hash]
|
40
|
-
def extract_http_headers(env)
|
41
|
-
env.inject({}) do |acc, data|
|
42
|
-
k, v = data
|
43
|
-
|
44
|
-
if k =~ /^HTTP_(.+)$/
|
45
|
-
# Convert things like HTTP_CONTENT_TYPE to Content-Type (standard
|
46
|
-
# HTTP header style)
|
47
|
-
k = $1.to_s.split("_").map { |c| c.capitalize }.join("-")
|
48
|
-
acc[k] = v
|
49
|
-
end
|
50
|
-
|
51
|
-
acc
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
38
|
# Extracts the rack environment, ignoring HTTP headers and
|
56
39
|
# converting the values to strings if they're not an Array
|
57
40
|
# or Hash.
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'radar/data_extensions/request_helper'
|
2
|
+
|
3
|
+
module Radar
|
4
|
+
module DataExtensions
|
5
|
+
# Takes a `:rails2_request` from the {ExceptionEvent} extra data which is
|
6
|
+
# added by the rails 2 integrator and extracts it into the data hash.
|
7
|
+
#
|
8
|
+
# **This data extension is automatically enabled by the Rails 2 integrator.**
|
9
|
+
class Rails2
|
10
|
+
include RequestHelper
|
11
|
+
|
12
|
+
def initialize(event)
|
13
|
+
@event = event
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_hash
|
17
|
+
request = @event.extra[:rails2_request]
|
18
|
+
return if !request
|
19
|
+
|
20
|
+
{ :request => {
|
21
|
+
:request_method => request.request_method.to_s,
|
22
|
+
:url => request.url.to_s,
|
23
|
+
:parameters => request.parameters,
|
24
|
+
:remote_ip => request.remote_ip,
|
25
|
+
:headers => extract_http_headers(request.env)
|
26
|
+
}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Radar
|
2
|
+
module DataExtensions
|
3
|
+
# A mixin which contains helper methods for dealing with request
|
4
|
+
# objects.
|
5
|
+
module RequestHelper
|
6
|
+
# Extracts only the HTTP headers from the rack environment,
|
7
|
+
# converting them to the proper HTTP format: `HTTP_CONTENT_TYPE`
|
8
|
+
# to `Content-Type`
|
9
|
+
#
|
10
|
+
# @param [Hash] env
|
11
|
+
# @return [Hash]
|
12
|
+
def extract_http_headers(env)
|
13
|
+
env.inject({}) do |acc, data|
|
14
|
+
k, v = data
|
15
|
+
|
16
|
+
if k =~ /^HTTP_(.+)$/
|
17
|
+
# Convert things like HTTP_CONTENT_TYPE to Content-Type (standard
|
18
|
+
# HTTP header style)
|
19
|
+
k = $1.to_s.split("_").map { |c| c.capitalize }.join("-")
|
20
|
+
acc[k] = v
|
21
|
+
end
|
22
|
+
|
23
|
+
acc
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|