radar 0.2.0 → 0.3.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/.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
|