radar 0.3.0 → 0.4.0
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 +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
|