radar 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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