radar 0.1.0 → 0.2.0

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