radar 0.1.0 → 0.2.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 CHANGED
@@ -1,2 +1,3 @@
1
+ -m markdown
1
2
  --files
2
3
  docs/user_guide.md
data/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ ## 0.2.0 (unreleased)
2
+
3
+ - Built in matcher: `:backtrace` (or `BacktraceMatcher`) which checks that
4
+ the backtrace includes the given text. [GH-18]
5
+ - Built in matcher: `:class` (or `ClassMatcher`) which checks against the
6
+ exception class. [GH-17]
7
+ - Add `config.match` to conditionally match exceptions before reporting
8
+ them so that exceptions can be better filtered per application. [GH-11]
9
+ - Changed the way reporters and data extensions are enabled. You must now
10
+ use methods `use`, `swap`, `insert`, `delete`, etc. See the user guide
11
+ for details. [GH-16]
12
+ - Added `prune_time` configuration to `FileReporter` which automatically prunes
13
+ files older than the specified age. [GH-15]
14
+ - Extra data in the form of a hash can be passed to `Application#report` now,
15
+ which is available in `ExceptionEvent#extra`, so that reporters and data
16
+ extensions can take advantage of this. [GH-14]
17
+ - Added LICENSE file. Oops!
18
+
19
+ ## 0.1.0
20
+
21
+ - Initial version.
data/Gemfile.lock CHANGED
@@ -7,7 +7,7 @@ GIT
7
7
  PATH
8
8
  remote: .
9
9
  specs:
10
- radar (0.1.0)
10
+ radar (0.2.0)
11
11
  json (>= 1.4.6)
12
12
 
13
13
  GEM
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2010 Mitchell Hashimoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -29,7 +29,7 @@ ask for any more information 90% of the time.
29
29
  Then just begin logging exceptions in your application:
30
30
 
31
31
  r = Radar::Application.new(:my_application)
32
- r.config.reporter Radar::Reporter::FileReporter
32
+ r.config.reporters.use Radar::Reporter::FileReporter
33
33
  r.report(exception)
34
34
 
35
35
  You can also tell Radar to attach itself to Ruby's `at_exit` hook
data/docs/user_guide.md CHANGED
@@ -32,7 +32,7 @@ remote server, etc.). Radar comes with some built-in reporters. Below, we config
32
32
  the application to log errors to a file (by default at `~/.radar/errors/my_application`):
33
33
 
34
34
  Radar::Application.new(:my_application) do |app|
35
- app.config.reporter Radar::Reporter::FileReporter
35
+ app.config.reporters.use Radar::Reporter::FileReporter
36
36
  end
37
37
 
38
38
  ### Reporting Errors
@@ -80,15 +80,15 @@ of what this means with a few examples:
80
80
  Reporters are enabled using the appilication configuration:
81
81
 
82
82
  Radar::Application.new(:my_application) do |app|
83
- app.config.reporter FileReporter
83
+ app.config.reporters.use FileReporter
84
84
  end
85
85
 
86
86
  And can be configured by passing a block to the reporter, which is yielded with
87
87
  the instance of that reporter:
88
88
 
89
89
  Radar::Application.new(:my_application) do |app|
90
- app.config.reporter FileReporter do |reporter|
91
- reporter.storage_directory = "~/.radar/exceptions"
90
+ app.config.reporters.use FileReporter do |reporter|
91
+ reporter.output_directory = "~/.radar/exceptions"
92
92
  end
93
93
  end
94
94
 
@@ -96,8 +96,8 @@ Radar also allows multiple reporters to be used, which are then called
96
96
  in the order they are defined when an exception occurs:
97
97
 
98
98
  Radar::Application.new(:my_application) do |app|
99
- app.config.reporter FileReporter
100
- app.config.reporter AnotherReporter
99
+ app.config.reporters.use FileReporter
100
+ app.config.reporters.use AnotherReporter
101
101
  end
102
102
 
103
103
  ### Built-in Reporters
@@ -112,7 +112,7 @@ where `timestamp` is the time that the exception occurred and `uniquehash` is th
112
112
  The directory where these files will be stored is configurable:
113
113
 
114
114
  Radar::Application.new(:my_application) do |app|
115
- app.config.reporter Radar::Reporter::FileReporter do |reporter|
115
+ app.config.reporters.use Radar::Reporter::FileReporter do |reporter|
116
116
  reporter.output_directory = "~/my_application_errors"
117
117
  end
118
118
  end
@@ -130,6 +130,9 @@ A few notes:
130
130
  JSON and pretty print it if you wish it to be easily human readable. There are
131
131
  [services](http://jsonformatter.curiousconcept.com/) out there to do this.
132
132
 
133
+ For complete documentation on this reporter, please see the actual {Radar::Reporter::FileReporter}
134
+ page.
135
+
133
136
  ### Custom Reporters
134
137
 
135
138
  It is very easy to write custom reporters. A reporter is simply a class which
@@ -146,7 +149,7 @@ occurred:
146
149
  And then using that reporter is just as easy:
147
150
 
148
151
  Radar::Application.new(:my_application) do |app|
149
- app.config.reporter StdoutReporter
152
+ app.config.reporters.use StdoutReporter
150
153
  end
151
154
 
152
155
  ## Data Extensions
@@ -181,7 +184,7 @@ Data extensions are enabled via the application configuration like most other
181
184
  things:
182
185
 
183
186
  Radar::Application.new(:my_application) do |app|
184
- app.config.data_extension UnameExtension
187
+ app.config.data_extensions.use UnameExtension
185
188
  end
186
189
 
187
190
  ### Built-In Data Extensions
@@ -195,3 +198,96 @@ Some of these are enabled by default, which are designated by the `*` on the nam
195
198
  host such as Ruby version and operating system.
196
199
 
197
200
  `*`: Enabled by default on every application.
201
+
202
+ ## Matchers
203
+
204
+ Matchers allow Radar applications to conditionally match exceptions so that
205
+ a Radar application doesn't catch unwanted exceptions, such as exceptions which
206
+ may not be caused by the library in question, or perhaps exceptions which aren't
207
+ really exceptional.
208
+
209
+ ### Enabling a Matcher
210
+
211
+ Matchers are enabled in the application configuration:
212
+
213
+ Radar::Application.new(:app) do |app|
214
+ app.config.match :class, StandardError
215
+ app.config.match :backtrace, /file.rb$/
216
+ end
217
+
218
+ As you can see, multiple matchers may be enabled. In this case, as long as at
219
+ least one matches, then the exception will be reported. The first argument to
220
+ {Radar::Config#match match} is a symbol or class of a matcher. If it is a symbol,
221
+ the symbol is constantized and expects to exist under the `Radar::Matchers` namespace.
222
+ If it is a class, that class will be used as the matcher. Any additional arguments
223
+ are passed directly into the initializer of the matcher. For more information
224
+ on writing a custom matcher, see the section below.
225
+
226
+ If no matchers are specified (the default), then all exceptions are caught.
227
+
228
+ ### Built-in Matchers
229
+
230
+ #### `:backtrace`
231
+
232
+ A matcher which matches against the backtrace of the exception. It allows:
233
+
234
+ * Match that a string is a substring of a line in the backtrace
235
+ * Match that a regexp matches a line in the backtrace
236
+ * Match one of the above up to a maximum depth in the backtrace
237
+
238
+ Examples of each are shown below (respective to the above order):
239
+
240
+ app.config.match :backtrace, "my_file.rb"
241
+ app.config.match :backtrace, /.+_test.rb/
242
+ app.config.match :backtrace, /.+_test.rb/, :depth => 5
243
+
244
+ If an exception doesn't have a backtrace (can happen if you don't actually
245
+ `raise` an exception, but instantiate one) then the matcher always returns
246
+ `false`.
247
+
248
+ #### `:class`
249
+
250
+ A matcher which matches against the class of the exception. It is configurable
251
+ so it can check against:
252
+
253
+ * An exact match
254
+ * Match class or any subclasses
255
+ * Match a regexp name of a class
256
+
257
+ Examples of each are shown below (in the above order):
258
+
259
+ app.config.match :class, StandardError
260
+ app.config.match :class, StandardError, :include_subclasses => true
261
+ app.config.match :class, /.*Error/
262
+
263
+ ### Custom Matchers
264
+
265
+ Matchers are simply classes which respond to `matches?` which returns a boolean
266
+ noting if the given {Radar::ExceptionEvent} matches. If true, then the exception
267
+ is reported, otherwise other matchers are tried, or if there are no other matchers,
268
+ the exception is ignored.
269
+
270
+ Below is a simple custom matcher which only matches exceptions with the
271
+ configured message:
272
+
273
+ class ErrorMessageMatcher
274
+ def initialize(message)
275
+ @message = message
276
+ end
277
+
278
+ def matches?(event)
279
+ event.exception.message == @message
280
+ end
281
+ end
282
+
283
+ And the usage is shown below:
284
+
285
+ Radar::Application.new(:app) do |app|
286
+ app.config.match ErrorMessageMatcher, "sample message"
287
+ end
288
+
289
+ And this results in the following behavior:
290
+
291
+ raise "Hello, World" # not reported
292
+ raise "sample message" # reported since it matches the message
293
+
data/lib/radar.rb CHANGED
@@ -15,4 +15,9 @@ module Radar
15
15
  class Reporter
16
16
  autoload :FileReporter, 'radar/reporter/file_reporter'
17
17
  end
18
+
19
+ module Matchers
20
+ autoload :BacktraceMatcher, 'radar/matchers/backtrace_matcher'
21
+ autoload :ClassMatcher, 'radar/matchers/class_matcher'
22
+ end
18
23
  end
@@ -56,9 +56,13 @@ module Radar
56
56
  #
57
57
  # $app = Radar::Application.new
58
58
  # $app.config do |config|
59
- # config.storage_directory = "foo"
59
+ # config.reporters.use Radar::Reporter::FileReporter
60
60
  # end
61
61
  #
62
+ # You can also just use it without a block:
63
+ #
64
+ # $app.config.reporters.use Radar::Reporter::FileReporter
65
+ #
62
66
  # @yield [Config] Configuration object, only if block is given.
63
67
  # @return [Config]
64
68
  def config
@@ -68,14 +72,20 @@ module Radar
68
72
  end
69
73
 
70
74
  # Reports an exception. This will send the exception on to the
71
- # various reporters configured for this application.
75
+ # various reporters configured for this application. If any
76
+ # matchers are defined, using {Config#match}, then at least one
77
+ # must match for the report to go forward to reporters.
72
78
  #
73
79
  # @param [Exception] exception
74
- def report(exception)
75
- data = ExceptionEvent.new(self, exception)
80
+ def report(exception, extra=nil)
81
+ data = ExceptionEvent.new(self, exception, extra)
82
+
83
+ # If there are matchers, then verify that at least one matches
84
+ # before continuing
85
+ return if !config.matchers.empty? && !config.matchers.values.find { |m| m.matches?(data) }
76
86
 
77
87
  # Report the exception to each of the reporters
78
- config.reporters.each do |reporter|
88
+ config.reporters.values.each do |reporter|
79
89
  reporter.report(data)
80
90
  end
81
91
  end
data/lib/radar/config.rb CHANGED
@@ -1,35 +1,129 @@
1
+ require 'forwardable'
2
+
1
3
  module Radar
4
+ # The configuration class used for applications. To configure your application
5
+ # see {Application#config}. This is also where all the examples are.
2
6
  class Config
3
7
  attr_reader :reporters
4
8
  attr_reader :data_extensions
9
+ attr_reader :matchers
5
10
 
6
11
  def initialize
7
- @reporters = []
8
- @data_extensions = [DataExtensions::HostEnvironment]
12
+ @reporters = UseArray.new do |klass, &block|
13
+ instance = klass.new
14
+ block.call(instance) if block
15
+ [klass, instance]
16
+ end
17
+
18
+ @data_extensions = UseArray.new
19
+ @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
9
25
  end
10
26
 
11
- # Add a reporter to an application. If a block is given, it
12
- # will be yielded later (since reporters are initialized lazily)
13
- # with the instance of the reporter.
27
+ # Adds a matcher rule to the application. An application will only
28
+ # report an exception if the event agrees with at least one of the
29
+ # matchers.
30
+ #
31
+ # To use a matcher, there are two options. The first is to use a
32
+ # symbol for the name:
33
+ #
34
+ # config.match :class, StandardError
14
35
  #
15
- # @param [Class] klass A {Reporter} class.
16
- def reporter(klass)
17
- instance = klass.new
18
- yield instance if block_given?
19
- @reporters << instance
36
+ # This will cause Radar to search for a class named "ClassMatcher"
37
+ # under the namespace {Radar::Matchers}.
38
+ #
39
+ # A second option is to use a class itself:
40
+ #
41
+ # config.match Radar::Matchers::ClassMatcher, StandardError
42
+ #
43
+ # Radar will then use the specified class as the matcher.
44
+ #
45
+ def match(matcher, *args)
46
+ @matchers.use(matcher, *args)
20
47
  end
48
+ end
21
49
 
22
- # Adds a data extension to an application. Data extensions allow
23
- # extra data to be included into an {ExceptionEvent} (they appear
24
- # in the {ExceptionEvent#to_hash} output). For more information,
25
- # please read the Radar user guide.
50
+ class Config
51
+ # A subclass of Array which allows for slightly different usage, based
52
+ # on `ActionDispatch::MiddlewareStack` in Rails 3. The main methods are
53
+ # enumerated below:
26
54
  #
27
- # This method takes a class. This class is expected to be initialized
28
- # with an {ExceptionEvent}, and must implement the `to_hash` method.
55
+ # - {#use}
56
+ # - {#insert}
57
+ # - {#insert_before}
58
+ # - {#insert_after}
59
+ # - {#swap}
60
+ # - {#delete}
29
61
  #
30
- # @param [Class] klass
31
- def data_extension(klass)
32
- @data_extensions << klass
62
+ class UseArray
63
+ extend Forwardable
64
+ def_delegators :@_array, :empty?, :length, :clear
65
+
66
+ # Initializes the UseArray with the given block used to generate
67
+ # the value created for the {#use} method. The block given determines
68
+ # how the {#use} method stores the key/value.
69
+ def initialize(*args, &block)
70
+ @_array = []
71
+ @_use_block = block || Proc.new { |key, *args| [key, key] }
72
+ end
73
+
74
+ # Use the given key. It is up to the configured use block (given by
75
+ # the initializer) to generate the actual key/value stored in the array.
76
+ def use(*args, &block)
77
+ insert(length, *args, &block)
78
+ end
79
+
80
+ # Insert the given key at the given index or directly before the
81
+ # given object (by key).
82
+ def insert(key, *args, &block)
83
+ @_array.insert(index(key), @_use_block.call(*args, &block))
84
+ end
85
+ alias_method :insert_before, :insert
86
+
87
+ # Insert after the given key.
88
+ def insert_after(key, *args, &block)
89
+ i = index(key)
90
+ raise ArgumentError.new("No such key found: #{key}") if !i
91
+ insert(i + 1, *args, &block)
92
+ end
93
+
94
+ # Swaps out the given object at the given index or key with a new
95
+ # object.
96
+ def swap(key, *args, &block)
97
+ i = index(key)
98
+ raise ArgumentError.new("No such key found: #{key}") if !i
99
+ delete(i)
100
+ insert(i, *args, &block)
101
+ end
102
+
103
+ # Delete the object with the given key or index.
104
+ def delete(key)
105
+ @_array.delete_at(index(key))
106
+ end
107
+
108
+ # Returns the value for the given key. If the key is an integer,
109
+ # it is returned as-is. Otherwise, do a lookup on the array for the
110
+ # the given key and return the index of it.
111
+ def index(key)
112
+ return key if key.is_a?(Integer)
113
+ @_array.each_with_index do |data, i|
114
+ return i if data[0] == key
115
+ end
116
+
117
+ nil
118
+ end
119
+
120
+ # Returns the values of this array.
121
+ def values
122
+ @_array.inject([]) do |acc, data|
123
+ acc << data[1]
124
+ acc
125
+ end
126
+ end
33
127
  end
34
128
  end
35
129
  end
@@ -9,16 +9,18 @@ module Radar
9
9
  attr_reader :application
10
10
  attr_reader :exception
11
11
  attr_reader :occurred_at
12
+ attr_reader :extra
12
13
 
13
- def initialize(application, exception)
14
+ def initialize(application, exception, extra=nil)
14
15
  @application = application
15
16
  @exception = exception
17
+ @extra = extra || {}
16
18
  @occurred_at = Time.now
17
19
  end
18
20
 
19
21
  # A hash of information about this exception event. This includes
20
22
  # {Application#to_hash} as well as information about the exception.
21
- # This also includes any {Config#data_extension data_extensions} if
23
+ # This also includes any {Config#data_extensions data_extensions} if
22
24
  # specified.
23
25
  #
24
26
  # @return [Hash]
@@ -34,7 +36,7 @@ module Radar
34
36
  }
35
37
 
36
38
  if !application.config.data_extensions.empty?
37
- application.config.data_extensions.each do |extension|
39
+ application.config.data_extensions.values.each do |extension|
38
40
  Support::Hash.deep_merge!(result, extension.new(self).to_hash)
39
41
  end
40
42
  end
@@ -0,0 +1,31 @@
1
+ module Radar
2
+ module Matchers
3
+ # A matcher which matches exceptions which contain a certain
4
+ # file in their backtrace.
5
+ #
6
+ # app.config.match :backtrace, "my_file"
7
+ # app.config.match :backtrace, %r{lib/my_application}
8
+ # app.config.match :backtrace, %r{lib/my_application}, :depth => 5
9
+ #
10
+ # By default this will search the entire backtrace, unless a depth
11
+ # is specified.
12
+ class BacktraceMatcher
13
+ def initialize(file, opts=nil)
14
+ @file = file
15
+ @opts = { :depth => nil }.merge(opts || {})
16
+ end
17
+
18
+ def matches?(event)
19
+ return false if !event.exception.backtrace
20
+
21
+ event.exception.backtrace.each_with_index do |line, depth|
22
+ return true if @file.is_a?(Regexp) && line =~ @file
23
+ return true if @file.is_a?(String) && line.include?(@file)
24
+ return false if @opts[:depth] && @opts[:depth].to_i <= (depth + 1)
25
+ end
26
+
27
+ false
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ module Radar
2
+ module Matchers
3
+ # A matcher which matches exceptions with a specific class.
4
+ #
5
+ # app.config.match :class, StandardError
6
+ # app.config.match :class, StandardError, :include_subclasses => true
7
+ # app.config.match :class, /.*Error/
8
+ #
9
+ class ClassMatcher
10
+ def initialize(klass, opts=nil)
11
+ @klass = klass
12
+ @opts = { :include_subclasses => false }.merge(opts || {})
13
+ end
14
+
15
+ def matches?(event)
16
+ return event.exception.class.to_s =~ @klass if @klass.is_a?(Regexp)
17
+ return event.exception.class == @klass if !@opts[:include_subclasses]
18
+
19
+ # Check for subclass matches
20
+ current = event.exception.class
21
+ while current
22
+ return true if current == @klass
23
+ current = current.superclass
24
+ end
25
+
26
+ false
27
+ end
28
+ end
29
+ end
30
+ end
@@ -5,25 +5,67 @@ module Radar
5
5
  # Reports exceptions by dumping the JSON data out to a file on the
6
6
  # local filesystem. The reporter is configurable:
7
7
  #
8
- # * {#output_directory=}
8
+ # ## Configurable Values
9
+ #
10
+ # ### `output_directory`
11
+ #
12
+ # Specifies the directory where the outputted files are stored. This value
13
+ # can be either a string or a lambda which takes an {ExceptionEvent} as its
14
+ # only parameter. The reporter will automatically attempt to make the configured
15
+ # directory if it doesn't already exist. Examples of both methods of specifying
16
+ # the directory are shown below:
17
+ #
18
+ # reporter.output_directory = "~/hard/coded/path"
19
+ #
20
+ # Or:
21
+ #
22
+ # reporter.output_directory = lambda { |event| "~/.radar/errors/#{event.application.name}" }
23
+ #
24
+ # ### `prune_time`
25
+ #
26
+ # Specifies the maximum age (in seconds) that a previously outputted file
27
+ # is allowed to reach before being pruned. When an exception is raised, the
28
+ # FileReporter will automatically prune existing files which are older than
29
+ # the specified amount. By default this is `nil` (no pruning occurs).
30
+ #
31
+ # # One week:
32
+ # reporter.prune_time = 60 * 60 * 24 * 7
9
33
  #
10
34
  class FileReporter
11
35
  attr_accessor :output_directory
36
+ attr_accessor :prune_time
12
37
 
13
38
  def initialize
14
39
  @output_directory = lambda { |event| "~/.radar/errors/#{event.application.name}" }
40
+ @prune_time = nil
15
41
  end
16
42
 
17
43
  def report(event)
18
44
  output_file = File.join(File.expand_path(output_directory(event)), "#{event.occurred_at.to_i}-#{event.uniqueness_hash}.txt")
45
+ directory = File.dirname(output_file)
19
46
 
20
47
  # Attempt to make the directory if it doesn't exist
21
- FileUtils.mkdir_p File.dirname(output_file)
48
+ FileUtils.mkdir_p directory
49
+
50
+ # Prune files if enabled
51
+ prune(directory) if prune_time
22
52
 
23
53
  # Write out the JSON to the output file
24
54
  File.open(output_file, 'w') { |f| f.write(event.to_json) }
25
55
  end
26
56
 
57
+ # Prunes the files in the given directory according to the age limit
58
+ # set by the {#prune_time} variable.
59
+ #
60
+ # @param [String] directory Directory to prune
61
+ def prune(directory)
62
+ Dir[File.join(directory, "*.txt")].each do |file|
63
+ next unless File.file?(file)
64
+ next unless (Time.now.to_i - File.ctime(file).to_i) >= prune_time.to_i
65
+ File.delete(file)
66
+ end
67
+ end
68
+
27
69
  # Returns the currently configured output directory. If `event` is given
28
70
  # as a parameter and the currently set directory is a lambda, then the
29
71
  # lambda will be evaluated then returned. If no event is given, the lambda
data/lib/radar/support.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  module Radar
2
2
  class Support
3
+ # Hash support methods:
4
+ #
5
+ # * {#deep_merge} and {#deep_merge!} - Does what it says: deep merges a
6
+ # hash with another hash. Taken from ActiveSupport in Rails 3.
7
+ #
3
8
  class Hash
4
- #----------------------------------------------------------------------
5
- # Deep Merging - Taken from Ruby on Rails ActiveSupport
6
- #----------------------------------------------------------------------
7
-
8
9
  # Returns a new hash with +self+ and +other_hash+ merged recursively.
9
10
  def self.deep_merge(source, other)
10
11
  deep_merge!(source.dup, other)
@@ -21,5 +22,26 @@ module Radar
21
22
  source
22
23
  end
23
24
  end
25
+
26
+ # Inflector methods:
27
+ #
28
+ # * {#camelize} - Convert a string or symbol to UpperCamelCase.
29
+ # * {#constantize} - Convert a string to a constant.
30
+ #
31
+ # Both of these inflector methods are taken directly from ActiveSupport
32
+ # in Rails 3.
33
+ class Inflector
34
+ def self.camelize(string)
35
+ string.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
36
+ end
37
+
38
+ def self.constantize(camel_cased_word)
39
+ names = camel_cased_word.split('::')
40
+ names.shift if names.empty? || names.first.empty?
41
+ names.inject(Object) do |acc, name|
42
+ acc.const_defined?(name) ? acc.const_get(name) : acc.const_missing(name)
43
+ end
44
+ end
45
+ end
24
46
  end
25
47
  end
data/lib/radar/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Radar
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -49,13 +49,13 @@ class ApplicationTest < Test::Unit::TestCase
49
49
  end
50
50
 
51
51
  should "be able to configure an application" do
52
- @instance.config.reporter(@reporter)
52
+ @instance.config.reporters.use @reporter
53
53
  assert !@instance.config.reporters.empty?
54
54
  end
55
55
 
56
56
  should "be able to configure using a block" do
57
57
  @instance.config do |config|
58
- config.reporter(@reporter)
58
+ config.reporters.use @reporter
59
59
  end
60
60
 
61
61
  assert !@instance.config.reporters.empty?
@@ -63,21 +63,13 @@ class ApplicationTest < Test::Unit::TestCase
63
63
  end
64
64
 
65
65
  context "reporting" do
66
- setup do
67
- # The fake reporter class
66
+ should "call report on each registered reporter" do
68
67
  reporter = Class.new do
69
- def report(environment)
70
- raise "success"
71
- end
68
+ def report(environment); raise "success"; end
72
69
  end
73
70
 
74
- # Setup the application to use the fake reporter
75
- @instance.config do |config|
76
- config.reporter reporter
77
- end
78
- end
71
+ @instance.config.reporters.use reporter
79
72
 
80
- should "call report on each registered reporter" do
81
73
  assert_raises(RuntimeError) do
82
74
  begin
83
75
  @instance.report(Exception.new)
@@ -87,6 +79,42 @@ class ApplicationTest < Test::Unit::TestCase
87
79
  end
88
80
  end
89
81
  end
82
+
83
+ should "add extra data to the event if given" do
84
+ reporter = Class.new do
85
+ def report(event); raise event.extra[:foo]; end
86
+ end
87
+
88
+ @instance.config.reporters.use reporter
89
+
90
+ begin
91
+ @instance.report(Exception.new, :foo => "BAR")
92
+ rescue => e
93
+ assert_equal "BAR", e.message
94
+ end
95
+ end
96
+
97
+ context "with a matcher" do
98
+ setup do
99
+ @matcher = Class.new do
100
+ def matches?(event); event.extra[:foo] == :bar; end
101
+ end
102
+
103
+ @reporter = Class.new
104
+ @instance.config.reporters.use @reporter
105
+ @instance.config.match @matcher
106
+ end
107
+
108
+ should "not report if a matcher is specified and doesn't match" do
109
+ @reporter.any_instance.expects(:report).never
110
+ @instance.report(Exception.new, :foo => :wrong)
111
+ end
112
+
113
+ should "report if a matcher matches" do
114
+ @reporter.any_instance.expects(:report).once
115
+ @instance.report(Exception.new, :foo => :bar)
116
+ end
117
+ end
90
118
  end
91
119
 
92
120
  context "to_hash" do
@@ -21,14 +21,14 @@ class ConfigTest < Test::Unit::TestCase
21
21
  end
22
22
 
23
23
  should "be able to add reporters" do
24
- @instance.reporter @reporter_klass
24
+ @instance.reporters.use @reporter_klass
25
25
  assert !@instance.reporters.empty?
26
- assert @instance.reporters.first.is_a?(@reporter_klass)
26
+ assert @instance.reporters.values.first.is_a?(@reporter_klass)
27
27
  end
28
28
 
29
29
  should "yield the reporter instance if a block is given" do
30
30
  @reporter_klass.any_instance.expects(:some_method).once
31
- @instance.reporter @reporter_klass do |reporter|
31
+ @instance.reporters.use @reporter_klass do |reporter|
32
32
  reporter.some_method
33
33
  end
34
34
  end
@@ -47,13 +47,108 @@ class ConfigTest < Test::Unit::TestCase
47
47
  end
48
48
 
49
49
  should "initially have some data extensions" do
50
- assert_equal [Radar::DataExtensions::HostEnvironment], @instance.data_extensions
50
+ assert_equal [Radar::DataExtensions::HostEnvironment], @instance.data_extensions.values
51
51
  end
52
52
 
53
53
  should "be able to add data extensions" do
54
- @instance.data_extension @extension
54
+ @instance.data_extensions.use @extension
55
55
  assert !@instance.data_extensions.empty?
56
56
  end
57
57
  end
58
+
59
+ context "matchers" do
60
+ setup do
61
+ @matcher = Class.new do
62
+ def matches?(event); false; end
63
+ end
64
+ end
65
+
66
+ teardown do
67
+ @instance.matchers.clear
68
+ end
69
+
70
+ should "initially have no matchers" do
71
+ assert @instance.matchers.empty?
72
+ end
73
+
74
+ should "be able to add matchers" do
75
+ @instance.match @matcher
76
+ assert !@instance.matchers.empty?
77
+ end
78
+ end
79
+ end
80
+
81
+ context "UseArray class" do
82
+ setup do
83
+ @klass = Radar::Config::UseArray
84
+ @instance = @klass.new
85
+ end
86
+
87
+ should "allow inserting objects via use" do
88
+ assert @instance.empty?
89
+ @instance.use(:foo)
90
+ assert !@instance.empty?
91
+ end
92
+
93
+ should "store the length" do
94
+ assert_equal 0, @instance.length
95
+ @instance.use(:foo)
96
+ @instance.use(:bar)
97
+ assert_equal 2, @instance.length
98
+ end
99
+
100
+ should "allow inserting objects at specific indexes" do
101
+ @instance.use(:foo)
102
+ @instance.insert(0, :bar)
103
+ assert_equal [:bar, :foo], @instance.values
104
+ end
105
+
106
+ should "allow inserting objects at specified key" do
107
+ @instance.use(:foo)
108
+ @instance.insert_before(:foo, :bar)
109
+ assert_equal [:bar, :foo], @instance.values
110
+ end
111
+
112
+ should "allow inserting objects after specified key" do
113
+ @instance.use(:foo)
114
+ @instance.insert_after(:foo, :bar)
115
+ assert_equal [:foo, :bar], @instance.values
116
+ end
117
+
118
+ should "raise an exception if inserting after a nonexistent key" do
119
+ assert_raises(ArgumentError) {
120
+ @instance.insert_after(:foo, :bar)
121
+ }
122
+ end
123
+
124
+ should "allow swapping objects" do
125
+ @instance.use(:foo)
126
+ @instance.swap(:foo, :bar)
127
+ assert_equal :bar, @instance.values.first
128
+ end
129
+
130
+ should "allow deleting objects" do
131
+ @instance.use(:foo)
132
+ @instance.delete(:foo)
133
+ assert @instance.empty?
134
+ end
135
+
136
+ should "allow querying for the values in the array" do
137
+ @instance.use(:foo)
138
+ @instance.use(:bar)
139
+ assert_equal [:foo, :bar], @instance.values
140
+ end
141
+
142
+ should "return the index of the given items" do
143
+ @instance.use(:foo)
144
+ @instance.use(:bar)
145
+
146
+ assert_equal 0, @instance.index(:foo)
147
+ assert_equal 1, @instance.index(:bar)
148
+ end
149
+
150
+ should "return the numeric index untouched if given" do
151
+ assert_equal 12, @instance.index(12)
152
+ end
58
153
  end
59
154
  end
@@ -7,6 +7,24 @@ class ExceptionEventTest < Test::Unit::TestCase
7
7
  @instance = create_exception_event
8
8
  end
9
9
 
10
+ should "generate a uniqueness hash" do
11
+ assert @instance.uniqueness_hash, "should have generated a uniqueness hash"
12
+ end
13
+
14
+ should "have a timestamp of when the exception occurred" do
15
+ assert @instance.occurred_at
16
+ assert @instance.occurred_at.is_a?(Time)
17
+ end
18
+
19
+ should "not have extra data by default" do
20
+ assert @instance.extra.empty?
21
+ end
22
+
23
+ should "allow for extra data to be present" do
24
+ @instance = create_exception_event(:foo => :bar)
25
+ assert_equal :bar, @instance.extra[:foo]
26
+ end
27
+
10
28
  context "to_hash" do
11
29
  context "data extensions" do
12
30
  setup do
@@ -15,7 +33,7 @@ class ExceptionEventTest < Test::Unit::TestCase
15
33
  def to_hash; { :exception => { :foo => :bar } }; end
16
34
  end
17
35
 
18
- @instance.application.config.data_extension @extension
36
+ @instance.application.config.data_extensions.use @extension
19
37
  @result = @instance.to_hash
20
38
  end
21
39
 
@@ -35,14 +53,5 @@ class ExceptionEventTest < Test::Unit::TestCase
35
53
  assert_equal @instance.to_hash.to_json, @instance.to_json
36
54
  end
37
55
  end
38
-
39
- should "generate a uniqueness hash" do
40
- assert @instance.uniqueness_hash, "should have generated a uniqueness hash"
41
- end
42
-
43
- should "have a timestamp of when the exception occurred" do
44
- assert @instance.occurred_at
45
- assert @instance.occurred_at.is_a?(Time)
46
- end
47
56
  end
48
57
  end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ class BacktraceMatcherTest < Test::Unit::TestCase
4
+ context "backtrace matcher class" do
5
+ setup do
6
+ @klass = Radar::Matchers::BacktraceMatcher
7
+ end
8
+
9
+ should "match if the backtrace matches a regexp" do
10
+ standard_event = create_exception_event { raise StandardError.new("An error") }
11
+ assert @klass.new(%r{/matchers/(.+)_test.rb}).matches?(standard_event)
12
+ assert !@klass.new(%r{/not_matchers/(.+)_test.rb}).matches?(standard_event)
13
+ end
14
+
15
+ should "match if the backtrace includes a substring" do
16
+ standard_event = create_exception_event { raise StandardError.new("An error") }
17
+ assert @klass.new("backtrace_matcher_test.rb").matches?(standard_event)
18
+ assert !@klass.new("not_real_matcher_test.rb").matches?(standard_event)
19
+ end
20
+
21
+ should "match only up to the specified depth" do
22
+ standard_event = create_exception_event { raise StandardError.new("An error") }
23
+ assert @klass.new("test_helper.rb", :depth => 5).matches?(standard_event)
24
+ assert !@klass.new("test_helper.rb", :depth => 1).matches?(standard_event)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ class ClassMatcherTest < Test::Unit::TestCase
4
+ context "class matcher class" do
5
+ setup do
6
+ @klass = Radar::Matchers::ClassMatcher
7
+ end
8
+
9
+ should "match if the class match exactly" do
10
+ standard_event = create_exception_event { raise StandardError.new("An error") }
11
+ assert @klass.new(RuntimeError).matches?(create_exception_event)
12
+ assert !@klass.new(RuntimeError).matches?(standard_event)
13
+ end
14
+
15
+ should "match regular expressions properly" do
16
+ standard_event = create_exception_event { raise StandardError.new("An error") }
17
+ assert @klass.new(/.*Error/).matches?(create_exception_event)
18
+ assert @klass.new(/.*Error/).matches?(standard_event)
19
+ end
20
+
21
+ should "match subclasses if specified" do
22
+ assert @klass.new(Exception, :include_subclasses => true).matches?(create_exception_event)
23
+ assert !@klass.new(Exception).matches?(create_exception_event)
24
+ end
25
+ end
26
+ end
@@ -7,6 +7,10 @@ class FileReporterTest < Test::Unit::TestCase
7
7
  @instance = @klass.new
8
8
  end
9
9
 
10
+ should "default prune time to nil" do
11
+ assert @instance.prune_time.nil?
12
+ end
13
+
10
14
  should "allow output directory to be a lambda" do
11
15
  @instance.output_directory = lambda { |event| event.application.name }
12
16
  event = create_exception_event
@@ -22,5 +22,21 @@ class SupportTest < Test::Unit::TestCase
22
22
  assert_equal 2, result[:a][:b]
23
23
  end
24
24
  end
25
+
26
+ context "inflector" do
27
+ setup do
28
+ @klass = @klass::Inflector
29
+ end
30
+
31
+ should "camelize a string" do
32
+ assert_equal "ActiveRecord", @klass.camelize("active_record")
33
+ assert_equal "ActiveRecord::Errors", @klass.camelize("active_record/errors")
34
+ end
35
+
36
+ should "constantize a string" do
37
+ assert_equal Radar::Application, @klass.constantize("Radar::Application")
38
+ assert_equal Radar::Reporter::FileReporter, @klass.constantize("Radar::Reporter::FileReporter")
39
+ end
40
+ end
25
41
  end
26
42
  end
data/test/test_helper.rb CHANGED
@@ -10,16 +10,17 @@ require "radar"
10
10
  class Test::Unit::TestCase
11
11
  # Returns a real {Radar::ExceptionEvent} object with a newly created
12
12
  # {Radar::Application} and a valid (has a backtrace) exception.
13
- def create_exception_event
13
+ def create_exception_event(extra=nil)
14
14
  application = Radar::Application.new(:foo, false)
15
15
  exception = nil
16
16
 
17
17
  begin
18
+ yield if block_given?
18
19
  raise "Something bad happened!"
19
20
  rescue => e
20
21
  exception = e
21
22
  end
22
23
 
23
- Radar::ExceptionEvent.new(application, exception)
24
+ Radar::ExceptionEvent.new(application, exception, extra)
24
25
  end
25
26
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 1
7
+ - 2
8
8
  - 0
9
- version: 0.1.0
9
+ version: 0.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Mitchell Hashimoto
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-08-15 00:00:00 -07:00
17
+ date: 2010-08-17 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -100,8 +100,10 @@ extra_rdoc_files: []
100
100
  files:
101
101
  - .gitignore
102
102
  - .yardopts
103
+ - CHANGELOG.md
103
104
  - Gemfile
104
105
  - Gemfile.lock
106
+ - LICENSE
105
107
  - README.md
106
108
  - Rakefile
107
109
  - docs/user_guide.md
@@ -111,6 +113,8 @@ files:
111
113
  - lib/radar/data_extensions/host_environment.rb
112
114
  - lib/radar/error.rb
113
115
  - lib/radar/exception_event.rb
116
+ - lib/radar/matchers/backtrace_matcher.rb
117
+ - lib/radar/matchers/class_matcher.rb
114
118
  - lib/radar/reporter.rb
115
119
  - lib/radar/reporter/file_reporter.rb
116
120
  - lib/radar/support.rb
@@ -120,6 +124,8 @@ files:
120
124
  - test/radar/config_test.rb
121
125
  - test/radar/data_extensions/host_environment_test.rb
122
126
  - test/radar/exception_event_test.rb
127
+ - test/radar/matchers/backtrace_matcher_test.rb
128
+ - test/radar/matchers/class_matcher_test.rb
123
129
  - test/radar/reporter/file_reporter_test.rb
124
130
  - test/radar/reporter_test.rb
125
131
  - test/radar/support_test.rb
@@ -138,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
138
144
  requirements:
139
145
  - - ">="
140
146
  - !ruby/object:Gem::Version
141
- hash: 4451322989948183573
147
+ hash: -4581519538816896341
142
148
  segments:
143
149
  - 0
144
150
  version: "0"