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.
Files changed (39) hide show
  1. data/.yardopts +1 -0
  2. data/CHANGELOG.md +14 -1
  3. data/Gemfile +10 -4
  4. data/Gemfile.lock +65 -1
  5. data/README.md +10 -1
  6. data/docs/user_guide.md +214 -18
  7. data/examples/README.md +5 -0
  8. data/examples/rack/README.md +15 -0
  9. data/examples/rack/config.ru +18 -0
  10. data/lib/radar.rb +20 -2
  11. data/lib/radar/application.rb +30 -1
  12. data/lib/radar/config.rb +54 -12
  13. data/lib/radar/data_extensions/rack.rb +72 -0
  14. data/lib/radar/error.rb +1 -0
  15. data/lib/radar/exception_event.rb +6 -4
  16. data/lib/radar/filters/key_filter.rb +54 -0
  17. data/lib/radar/integration/rack.rb +41 -0
  18. data/lib/radar/integration/rails3.rb +19 -0
  19. data/lib/radar/integration/rails3/generator.rb +19 -0
  20. data/lib/radar/integration/rails3/railtie.rb +12 -0
  21. data/lib/radar/integration/rails3/templates/README +17 -0
  22. data/lib/radar/integration/rails3/templates/radar.rb +15 -0
  23. data/lib/radar/logger.rb +37 -0
  24. data/lib/radar/reporter/file_reporter.rb +31 -12
  25. data/lib/radar/reporter/io_reporter.rb +35 -0
  26. data/lib/radar/reporter/logger_reporter.rb +31 -0
  27. data/lib/radar/version.rb +1 -1
  28. data/radar.gemspec +2 -4
  29. data/test/radar/application_test.rb +38 -0
  30. data/test/radar/config_test.rb +34 -0
  31. data/test/radar/data_extensions/rack_test.rb +51 -0
  32. data/test/radar/exception_event_test.rb +20 -0
  33. data/test/radar/filters/key_filter_test.rb +28 -0
  34. data/test/radar/integration/rack_test.rb +61 -0
  35. data/test/radar/integration/rails3_test.rb +29 -0
  36. data/test/radar/logger_test.rb +13 -0
  37. data/test/radar/reporter/io_reporter_test.rb +20 -0
  38. data/test/radar/reporter/logger_reporter_test.rb +21 -0
  39. metadata +25 -4
@@ -0,0 +1,5 @@
1
+ # Radar Examples
2
+
3
+ This directory contains various examples of using Radar in various
4
+ ways. Each subdirectory is an example with its own `README` file explaining
5
+ the purpose and usage of the example.
@@ -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
+ }
@@ -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
- class Reporter
16
- autoload :FileReporter, 'radar/reporter/file_reporter'
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
@@ -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
- return if !config.matchers.empty? && !config.matchers.values.find { |m| m.matches?(data) }
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]
@@ -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 = UseArray.new do |klass, &block|
13
- instance = klass.new
14
- block.call(instance) if block
15
- [klass, instance]
16
- end
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
- @_array.insert(index(key), @_use_block.call(*args, &block))
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
@@ -3,4 +3,5 @@ module Radar
3
3
  # for users of radar to catch all radar related errors.
4
4
  class Error < StandardError; end
5
5
  class ApplicationAlreadyExists < Error; end
6
+ class ReporterError < Error; end
6
7
  end
@@ -35,10 +35,12 @@ module Radar
35
35
  :occurred_at => occurred_at.to_i
36
36
  }
37
37
 
38
- if !application.config.data_extensions.empty?
39
- application.config.data_extensions.values.each do |extension|
40
- Support::Hash.deep_merge!(result, extension.new(self).to_hash)
41
- end
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