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.
Files changed (38) hide show
  1. data/.gitignore +2 -1
  2. data/CHANGELOG.md +21 -0
  3. data/Gemfile +3 -2
  4. data/Gemfile.lock +47 -48
  5. data/README.md +14 -25
  6. data/docs/user_guide.md +155 -30
  7. data/examples/rack/config.ru +1 -1
  8. data/examples/sinatra/README.md +15 -0
  9. data/examples/sinatra/example.rb +18 -0
  10. data/lib/radar.rb +11 -5
  11. data/lib/radar/application.rb +14 -3
  12. data/lib/radar/backtrace.rb +42 -0
  13. data/lib/radar/config.rb +112 -20
  14. data/lib/radar/data_extensions/rack.rb +4 -21
  15. data/lib/radar/data_extensions/rails2.rb +31 -0
  16. data/lib/radar/data_extensions/request_helper.rb +28 -0
  17. data/lib/radar/exception_event.rb +10 -4
  18. data/lib/radar/integration/rails2.rb +31 -0
  19. data/lib/radar/integration/rails2/action_controller_rescue.rb +30 -0
  20. data/lib/radar/integration/rails3.rb +0 -2
  21. data/lib/radar/integration/rails3/railtie.rb +0 -2
  22. data/lib/radar/integration/sinatra.rb +21 -0
  23. data/lib/radar/matchers/local_request_matcher.rb +43 -0
  24. data/lib/radar/reporter/hoptoad_reporter.rb +204 -0
  25. data/lib/radar/reporter/logger_reporter.rb +2 -3
  26. data/lib/radar/version.rb +1 -1
  27. data/radar.gemspec +1 -0
  28. data/test/radar/application_test.rb +39 -18
  29. data/test/radar/backtrace_test.rb +42 -0
  30. data/test/radar/config_test.rb +49 -4
  31. data/test/radar/data_extensions/rails2_test.rb +14 -0
  32. data/test/radar/exception_event_test.rb +9 -0
  33. data/test/radar/integration/rack_test.rb +1 -1
  34. data/test/radar/integration/sinatra_test.rb +13 -0
  35. data/test/radar/matchers/local_request_matcher_test.rb +26 -0
  36. data/test/radar/reporter/hoptoad_reporter_test.rb +20 -0
  37. data/test/radar/reporter/logger_reporter_test.rb +0 -4
  38. metadata +41 -12
@@ -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.config.reporters.use :io, :io_object => STDERR
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!
@@ -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, 'radar/data_extensions/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, 'radar/integration/rack'
24
- autoload :Rails3, 'radar/integration/rails3'
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, 'radar/matchers/backtrace_matcher'
29
- autoload :ClassMatcher, 'radar/matchers/class_matcher'
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
@@ -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.matches?(data) && logger.info("Reporting exception. Matches: #{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.report(data)
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
@@ -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
- def match(matcher, *args)
42
- @matchers.use(matcher, *args)
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(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]
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(matcher, *args)
69
- matcher = Support::Inflector.constantize("Radar::Matchers::#{Support::Inflector.camelize(matcher)}Matcher") if !matcher.is_a?(Class)
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
- 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
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
- klass = Support::Inflector.constantize("Radar::Filters::#{Support::Inflector.camelize(klass)}Filter") if !klass.is_a?(Class)
84
- block = klass.new.method(:filter)
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
- [block, block]
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