radar 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -0
- data/CHANGELOG.md +14 -1
- data/Gemfile +10 -4
- data/Gemfile.lock +65 -1
- data/README.md +10 -1
- data/docs/user_guide.md +214 -18
- data/examples/README.md +5 -0
- data/examples/rack/README.md +15 -0
- data/examples/rack/config.ru +18 -0
- data/lib/radar.rb +20 -2
- data/lib/radar/application.rb +30 -1
- data/lib/radar/config.rb +54 -12
- data/lib/radar/data_extensions/rack.rb +72 -0
- data/lib/radar/error.rb +1 -0
- data/lib/radar/exception_event.rb +6 -4
- data/lib/radar/filters/key_filter.rb +54 -0
- data/lib/radar/integration/rack.rb +41 -0
- data/lib/radar/integration/rails3.rb +19 -0
- data/lib/radar/integration/rails3/generator.rb +19 -0
- data/lib/radar/integration/rails3/railtie.rb +12 -0
- data/lib/radar/integration/rails3/templates/README +17 -0
- data/lib/radar/integration/rails3/templates/radar.rb +15 -0
- data/lib/radar/logger.rb +37 -0
- data/lib/radar/reporter/file_reporter.rb +31 -12
- data/lib/radar/reporter/io_reporter.rb +35 -0
- data/lib/radar/reporter/logger_reporter.rb +31 -0
- data/lib/radar/version.rb +1 -1
- data/radar.gemspec +2 -4
- data/test/radar/application_test.rb +38 -0
- data/test/radar/config_test.rb +34 -0
- data/test/radar/data_extensions/rack_test.rb +51 -0
- data/test/radar/exception_event_test.rb +20 -0
- data/test/radar/filters/key_filter_test.rb +28 -0
- data/test/radar/integration/rack_test.rb +61 -0
- data/test/radar/integration/rails3_test.rb +29 -0
- data/test/radar/logger_test.rb +13 -0
- data/test/radar/reporter/io_reporter_test.rb +20 -0
- data/test/radar/reporter/logger_reporter_test.rb +21 -0
- metadata +25 -4
data/examples/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# Radar Examples: Rack
|
2
|
+
|
3
|
+
This example shows Radar's Rack 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
|
+
bundle exec rackup
|
13
|
+
|
14
|
+
Then access `localhost:9292`, 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
|
+
|
5
|
+
# Create a Radar::Application, configured to simply log to the
|
6
|
+
# STDERR stream.
|
7
|
+
app = Radar::Application.new(:rack_example) do |a|
|
8
|
+
a.config.reporters.use :io, :io_object => STDERR
|
9
|
+
end
|
10
|
+
|
11
|
+
# Use the Radar Rack middleware for the created application,
|
12
|
+
# and make the Rack app just throw an exception so we can see it
|
13
|
+
# working.
|
14
|
+
use Rack::Radar, :application => app
|
15
|
+
run lambda { |env|
|
16
|
+
raise "Uh oh, an error!"
|
17
|
+
[200, { "Content-Type" => "text/html" }, ["This shouldn't be reached."]]
|
18
|
+
}
|
data/lib/radar.rb
CHANGED
@@ -1,23 +1,41 @@
|
|
1
1
|
require 'radar/version'
|
2
2
|
require 'radar/error'
|
3
|
+
require 'radar/integration/rails3/railtie' if defined?(Rails::Railtie)
|
3
4
|
|
4
5
|
module Radar
|
5
6
|
autoload :Application, 'radar/application'
|
6
7
|
autoload :Config, 'radar/config'
|
7
8
|
autoload :ExceptionEvent, 'radar/exception_event'
|
9
|
+
autoload :Logger, 'radar/logger'
|
8
10
|
autoload :Reporter, 'radar/reporter'
|
9
11
|
autoload :Support, 'radar/support'
|
10
12
|
|
11
13
|
module DataExtensions
|
12
14
|
autoload :HostEnvironment, 'radar/data_extensions/host_environment'
|
15
|
+
autoload :Rack, 'radar/data_extensions/rack'
|
13
16
|
end
|
14
17
|
|
15
|
-
|
16
|
-
autoload :
|
18
|
+
module Filters
|
19
|
+
autoload :KeyFilter, 'radar/filters/key_filter'
|
20
|
+
end
|
21
|
+
|
22
|
+
module Integration
|
23
|
+
autoload :Rack, 'radar/integration/rack'
|
24
|
+
autoload :Rails3, 'radar/integration/rails3'
|
17
25
|
end
|
18
26
|
|
19
27
|
module Matchers
|
20
28
|
autoload :BacktraceMatcher, 'radar/matchers/backtrace_matcher'
|
21
29
|
autoload :ClassMatcher, 'radar/matchers/class_matcher'
|
22
30
|
end
|
31
|
+
|
32
|
+
class Reporter
|
33
|
+
autoload :FileReporter, 'radar/reporter/file_reporter'
|
34
|
+
autoload :IoReporter, 'radar/reporter/io_reporter'
|
35
|
+
autoload :LoggerReporter, 'radar/reporter/logger_reporter'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
module Rack
|
40
|
+
autoload :Radar, 'radar/integration/rack'
|
23
41
|
end
|
data/lib/radar/application.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'forwardable'
|
2
3
|
|
3
4
|
module Radar
|
4
5
|
# A shortcut for {Application.find}.
|
@@ -10,12 +11,16 @@ module Radar
|
|
10
11
|
# Represents an instance of Radar for a given application. Every
|
11
12
|
# application which uses Radar must instantiate an {Application}.
|
12
13
|
class Application
|
14
|
+
extend Forwardable
|
15
|
+
|
13
16
|
@@registered = {}
|
14
17
|
@@mutex = Mutex.new
|
15
18
|
|
16
19
|
attr_reader :name
|
17
20
|
attr_reader :creation_location
|
18
21
|
|
22
|
+
def_delegators :config, :reporters, :data_extensions, :matchers, :filters
|
23
|
+
|
19
24
|
# Looks up an application which was registered with the given name.
|
20
25
|
#
|
21
26
|
# @param [String] name Application name.
|
@@ -71,6 +76,16 @@ module Radar
|
|
71
76
|
@_config
|
72
77
|
end
|
73
78
|
|
79
|
+
# Returns the logger for the application. Each application gets
|
80
|
+
# their own logger which is used for lightweight (single line)
|
81
|
+
# logging so users can sanity check that Radar is working as
|
82
|
+
# expected.
|
83
|
+
#
|
84
|
+
# @return [Logger]
|
85
|
+
def logger
|
86
|
+
@_logger ||= Logger.new(self)
|
87
|
+
end
|
88
|
+
|
74
89
|
# Reports an exception. This will send the exception on to the
|
75
90
|
# various reporters configured for this application. If any
|
76
91
|
# matchers are defined, using {Config#match}, then at least one
|
@@ -82,9 +97,14 @@ module Radar
|
|
82
97
|
|
83
98
|
# If there are matchers, then verify that at least one matches
|
84
99
|
# before continuing
|
85
|
-
|
100
|
+
if !config.matchers.empty?
|
101
|
+
return if !config.matchers.values.find do |m|
|
102
|
+
m.matches?(data) && logger.info("Reporting exception. Matches: #{m}")
|
103
|
+
end
|
104
|
+
end
|
86
105
|
|
87
106
|
# Report the exception to each of the reporters
|
107
|
+
logger.info "Invoking reporters for exception: #{exception.class}"
|
88
108
|
config.reporters.values.each do |reporter|
|
89
109
|
reporter.report(data)
|
90
110
|
end
|
@@ -93,9 +113,18 @@ module Radar
|
|
93
113
|
# Hooks this application into the `at_exit` handler so that
|
94
114
|
# application crashing exceptions are properly reported.
|
95
115
|
def rescue_at_exit!
|
116
|
+
logger.info "Attached to application exit."
|
96
117
|
at_exit { report($!) if $! }
|
97
118
|
end
|
98
119
|
|
120
|
+
# Integrate this application with some external system, such as
|
121
|
+
# Rack, Rails, Sinatra, etc. For more information on Radar integrations,
|
122
|
+
# please read the user guide.
|
123
|
+
def integrate(integrator, *args, &block)
|
124
|
+
integrator = Support::Inflector.constantize("Radar::Integration::#{Support::Inflector.camelize(integrator)}") if !integrator.is_a?(Class)
|
125
|
+
integrator.integrate!(self, *args, &block)
|
126
|
+
end
|
127
|
+
|
99
128
|
# Converts application to a serialization-friendly hash.
|
100
129
|
#
|
101
130
|
# @return [Hash]
|
data/lib/radar/config.rb
CHANGED
@@ -7,21 +7,17 @@ module Radar
|
|
7
7
|
attr_reader :reporters
|
8
8
|
attr_reader :data_extensions
|
9
9
|
attr_reader :matchers
|
10
|
+
attr_reader :filters
|
11
|
+
attr_accessor :log_location
|
10
12
|
|
11
13
|
def initialize
|
12
|
-
@reporters
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
@reporters = UseArray.new(&method(:add_reporter))
|
15
|
+
@data_extensions = UseArray.new(&method(:add_data_extension))
|
16
|
+
@matchers = UseArray.new(&method(:add_matcher))
|
17
|
+
@filters = UseArray.new(&method(:add_filter))
|
18
|
+
@log_location = nil
|
17
19
|
|
18
|
-
@data_extensions = UseArray.new
|
19
20
|
@data_extensions.use DataExtensions::HostEnvironment
|
20
|
-
|
21
|
-
@matchers = UseArray.new do |matcher, *args|
|
22
|
-
matcher = Support::Inflector.constantize("Radar::Matchers::" + Support::Inflector.camelize(matcher)) if !matcher.is_a?(Class)
|
23
|
-
[matcher, matcher.new(*args)]
|
24
|
-
end
|
25
21
|
end
|
26
22
|
|
27
23
|
# Adds a matcher rule to the application. An application will only
|
@@ -45,6 +41,51 @@ module Radar
|
|
45
41
|
def match(matcher, *args)
|
46
42
|
@matchers.use(matcher, *args)
|
47
43
|
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# The callback that is used to add a reporter to the {UseArray}
|
48
|
+
# when `reporters.use` is called.
|
49
|
+
def add_reporter(klass, *args)
|
50
|
+
klass = Support::Inflector.constantize("Radar::Reporter::#{Support::Inflector.camelize(klass)}Reporter") if !klass.is_a?(Class)
|
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]
|
57
|
+
end
|
58
|
+
|
59
|
+
# The callback that is used to add a data extension to the {UseArray}
|
60
|
+
# when `data_extensions.use` is called.
|
61
|
+
def add_data_extension(ext, *args)
|
62
|
+
ext = Support::Inflector.constantize("Radar::DataExtensions::#{Support::Inflector.camelize(ext)}") if !ext.is_a?(Class)
|
63
|
+
[ext, ext]
|
64
|
+
end
|
65
|
+
|
66
|
+
# The callback that is used to add a matcher to the {UseArray}
|
67
|
+
# when `matchers.use` is called.
|
68
|
+
def add_matcher(matcher, *args)
|
69
|
+
matcher = Support::Inflector.constantize("Radar::Matchers::#{Support::Inflector.camelize(matcher)}Matcher") if !matcher.is_a?(Class)
|
70
|
+
[matcher, matcher.new(*args)]
|
71
|
+
end
|
72
|
+
|
73
|
+
# The callback that is used to add a filter to the {UseArray}
|
74
|
+
# when `filters.use` is called.
|
75
|
+
def add_filter(*args)
|
76
|
+
block = args.pop if args.last.is_a?(Proc)
|
77
|
+
raise ArgumentError.new("`filters.use` requires at least a class or a lambda to be given.") if args.empty? && !block
|
78
|
+
|
79
|
+
if !args.empty?
|
80
|
+
# Detect the proper class then get the `filter` method from it,
|
81
|
+
# since that is all we care about
|
82
|
+
klass = args.shift
|
83
|
+
klass = Support::Inflector.constantize("Radar::Filters::#{Support::Inflector.camelize(klass)}Filter") if !klass.is_a?(Class)
|
84
|
+
block = klass.new.method(:filter)
|
85
|
+
end
|
86
|
+
|
87
|
+
[block, block]
|
88
|
+
end
|
48
89
|
end
|
49
90
|
|
50
91
|
class Config
|
@@ -80,7 +121,8 @@ module Radar
|
|
80
121
|
# Insert the given key at the given index or directly before the
|
81
122
|
# given object (by key).
|
82
123
|
def insert(key, *args, &block)
|
83
|
-
|
124
|
+
args.push(block) if block
|
125
|
+
@_array.insert(index(key), @_use_block.call(*args))
|
84
126
|
end
|
85
127
|
alias_method :insert_before, :insert
|
86
128
|
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Radar
|
2
|
+
module DataExtensions
|
3
|
+
# Data extensions which adds information about a rack request,
|
4
|
+
# if it exists in the `:rack_request` extra data of the {ExceptionEvent}.
|
5
|
+
class Rack
|
6
|
+
def initialize(event)
|
7
|
+
@event = event
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_hash
|
11
|
+
result = {}
|
12
|
+
|
13
|
+
request = @event.extra[:rack_request]
|
14
|
+
if request
|
15
|
+
Support::Hash.deep_merge!(result, { :request => {
|
16
|
+
:request_method => request.request_method.to_s,
|
17
|
+
:url => request.url.to_s,
|
18
|
+
:parameters => request.params,
|
19
|
+
:remote_ip => request.ip
|
20
|
+
}
|
21
|
+
})
|
22
|
+
end
|
23
|
+
|
24
|
+
if @event.extra[:rack_env]
|
25
|
+
Support::Hash.deep_merge!(result, :request => { :headers => extract_http_headers(@event.extra[:rack_env]) })
|
26
|
+
Support::Hash.deep_merge!(result, :request => { :rack_env => extract_rack_env(@event.extra[:rack_env]) })
|
27
|
+
end
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
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
|
+
# Extracts the rack environment, ignoring HTTP headers and
|
56
|
+
# converting the values to strings if they're not an Array
|
57
|
+
# or Hash.
|
58
|
+
def extract_rack_env(env)
|
59
|
+
env.inject({}) do |acc, data|
|
60
|
+
k, v = data
|
61
|
+
|
62
|
+
if !(k =~ /^HTTP_/)
|
63
|
+
v = v.to_s if !v.is_a?(Array) && !v.is_a?(Hash) && !v.is_a?(Integer)
|
64
|
+
acc[k] = v
|
65
|
+
end
|
66
|
+
|
67
|
+
acc
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
data/lib/radar/error.rb
CHANGED
@@ -35,10 +35,12 @@ module Radar
|
|
35
35
|
:occurred_at => occurred_at.to_i
|
36
36
|
}
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
application.config.data_extensions.values.each do |extension|
|
39
|
+
Support::Hash.deep_merge!(result, extension.new(self).to_hash || {})
|
40
|
+
end
|
41
|
+
|
42
|
+
application.config.filters.values.each do |filter|
|
43
|
+
result = filter.call(result)
|
42
44
|
end
|
43
45
|
|
44
46
|
result
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Radar
|
2
|
+
module Filters
|
3
|
+
# Filters the event data by filtering out a given key which
|
4
|
+
# can exist anywhere in the data hash. For example, given
|
5
|
+
# the following hash:
|
6
|
+
#
|
7
|
+
# { :request => { :password => "foo" },
|
8
|
+
# :rack_env => { :params => { :password => "foo" } } }
|
9
|
+
#
|
10
|
+
# If the KeyFilter was configured like so:
|
11
|
+
#
|
12
|
+
# app.filters.use :key, :key => :password
|
13
|
+
#
|
14
|
+
# Then the data hash would turn into:
|
15
|
+
#
|
16
|
+
# { :request => { :password => "[FILTERED]" },
|
17
|
+
# :rack_env => { :params => { :password => "[FILTERED]" } } }
|
18
|
+
#
|
19
|
+
# ## Options
|
20
|
+
#
|
21
|
+
# * `:key` - A single element or array of elements which represent the
|
22
|
+
# keys to filter out of the event hash.
|
23
|
+
# * `:filter_text` - The text which replaces keys which are caught by the
|
24
|
+
# filter. This defaults to "[FILTERED]"
|
25
|
+
#
|
26
|
+
class KeyFilter
|
27
|
+
attr_accessor :key
|
28
|
+
attr_accessor :filter_text
|
29
|
+
|
30
|
+
def initialize(opts=nil)
|
31
|
+
(opts || {}).each do |k,v|
|
32
|
+
send("#{k}=", v)
|
33
|
+
end
|
34
|
+
|
35
|
+
@filter_text ||= "[FILTERED]"
|
36
|
+
end
|
37
|
+
|
38
|
+
def filter(data)
|
39
|
+
# Convert the keys to strings, since we always compare against strings
|
40
|
+
filter_keys = [key].flatten.collect { |k| k.to_s }
|
41
|
+
|
42
|
+
data.each do |k,v|
|
43
|
+
if filter_keys.include?(k.to_s)
|
44
|
+
data[k] = filter_text
|
45
|
+
elsif v.is_a?(Hash)
|
46
|
+
filter(v)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
data
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Radar
|
2
|
+
module Integration
|
3
|
+
# Allows drop-in integration with Rack for Radar. This class
|
4
|
+
# should not ever actually be used with {Application#integrate}.
|
5
|
+
# Instead, use the middleware provided by {Rack::Radar}, passing
|
6
|
+
# in your application like so:
|
7
|
+
#
|
8
|
+
# use Rack::Radar, :application => radar_app
|
9
|
+
#
|
10
|
+
class Rack
|
11
|
+
def self.integrate!(app)
|
12
|
+
raise "To enable Rack integration, please do: `use Rack::Radar, :application => app` instead."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Rack
|
19
|
+
# A rack middleware which allows Radar to catch any exceptions
|
20
|
+
# thrown down a Rack app and report it to the given Radar application.
|
21
|
+
#
|
22
|
+
# use Rack::Radar, :application => radar_app
|
23
|
+
#
|
24
|
+
class Radar
|
25
|
+
def initialize(app, opts=nil)
|
26
|
+
@app = app
|
27
|
+
@opts = { :application => nil }.merge(opts || {})
|
28
|
+
raise ArgumentError.new("Must provide a radar application in `:application`") if !@opts[:application] || !@opts[:application].is_a?(::Radar::Application)
|
29
|
+
|
30
|
+
# Enable the rack data extension
|
31
|
+
@opts[:application].config.data_extensions.use :rack
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(env)
|
35
|
+
@app.call(env)
|
36
|
+
rescue Exception => e
|
37
|
+
@opts[:application].report(e, :rack_request => Rack::Request.new(env), :rack_env => env)
|
38
|
+
raise
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "rails"
|
2
|
+
|
3
|
+
module Radar
|
4
|
+
module Integration
|
5
|
+
# Allows drop-in integration with Rails 3 for Radar. This
|
6
|
+
# basically enables a middleware in your Rails 3 application
|
7
|
+
# which catches any exceptions and adds some additional
|
8
|
+
# information to the exception (such as the rack environment,
|
9
|
+
# request URL, etc.)
|
10
|
+
class Rails3
|
11
|
+
def self.integrate!(app)
|
12
|
+
raise ArgumentError.new("Rails integration requires a Rails application to be defined.") if !Rails.application
|
13
|
+
|
14
|
+
# For now just use the Rack::Radar
|
15
|
+
Rails.application.config.middleware.use "Rack::Radar", :application => app
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|