radar 0.2.0 → 0.3.0

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