radar 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle/*
4
+ doc/*
5
+ .yardoc/*
6
+ test.rb
@@ -0,0 +1,2 @@
1
+ --files
2
+ docs/user_guide.md
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source :gemcutter
2
+
3
+ # Specify your gem's dependencies in radar.gemspec
4
+ gemspec
5
+
6
+ # Additional gems which I don't really want in the gemspec but
7
+ # are useful for development
8
+ group :development do
9
+ gem "yard", :git => "http://github.com/lsegal/yard.git"
10
+ gem "bluecloth"
11
+ end
@@ -0,0 +1,34 @@
1
+ GIT
2
+ remote: http://github.com/lsegal/yard.git
3
+ revision: bf84275
4
+ specs:
5
+ yard (0.6.0.rc1)
6
+
7
+ PATH
8
+ remote: .
9
+ specs:
10
+ radar (0.1.0)
11
+ json (>= 1.4.6)
12
+
13
+ GEM
14
+ remote: http://rubygems.org/
15
+ specs:
16
+ bluecloth (2.0.7)
17
+ json (1.4.6)
18
+ mocha (0.9.8)
19
+ rake
20
+ rake (0.8.7)
21
+ shoulda (2.11.3)
22
+
23
+ PLATFORMS
24
+ ruby
25
+
26
+ DEPENDENCIES
27
+ bluecloth
28
+ bundler (>= 1.0.0.rc.5)
29
+ json (>= 1.4.6)
30
+ mocha
31
+ radar!
32
+ rake
33
+ shoulda
34
+ yard!
@@ -0,0 +1,107 @@
1
+ # Radar
2
+
3
+ * Source: [http://github.com/mitchellh/radar](http://github.com/mitchellh/radar)
4
+ * IRC: `#vagrant` on Freenode
5
+ * User Guide: [http://mitchellh.github.com/radar/file.user_guide.html](http://mitchellh.github.com/radar/file.user_guide.html)
6
+ * Documentation: [http://mitchellh.github.com/radar](http://mitchellh.github.com/radar)
7
+
8
+ Radar is a tool which provides a drop-in solution to catching and reporting
9
+ errors in your Ruby applications in customizable ways.
10
+
11
+ Radar is not a typical exception notifier such as Hoptoad and Exceptional
12
+ since the former are built with Rails apps in mind, logging to a central
13
+ server. Instead, Radar was initially built for [Vagrant](http://vagrantup.com),
14
+ a command line tool. And instead of solely logging to a central server,
15
+ Radar supports logging in configurable ways (to a file, to a server, to
16
+ a growl notification, etc.)
17
+
18
+ Radar was built out of the need for an easy automated solution to catching
19
+ exceptions and gathering information, since I noticed that every time a stack
20
+ trace was outputted for [Vagrant](http://vagrantup.com), I was asking the
21
+ user all the same questions which could've easily have been gathered automatically.
22
+ Now, Vagrant users can simply send me the radar output and I don't have to
23
+ ask for any more information 90% of the time.
24
+
25
+ ## Quick Start
26
+
27
+ gem install radar
28
+
29
+ Then just begin logging exceptions in your application:
30
+
31
+ r = Radar::Application.new(:my_application)
32
+ r.config.reporter Radar::Reporter::FileReporter
33
+ r.report(exception)
34
+
35
+ You can also tell Radar to attach itself to Ruby's `at_exit` hook
36
+ so it can catch application-crashing exceptions automatically:
37
+
38
+ r.rescue_at_exit!
39
+
40
+ Both of the above methods can be used together, of course.
41
+
42
+ Since the above enabled the `FileReporter` for the application, reported
43
+ exceptions will be stored in a text file on the local filesystem, by default
44
+ at `~/.radar/errors/my_application`. Sample contents of the exception
45
+ data generated by default is shown below. Note that you can add your own
46
+ custom data to this output by using data extensions (covered in the user
47
+ guide).
48
+
49
+ {
50
+ "application":{
51
+ "name":"my_application"
52
+ },
53
+ "exception":{
54
+ "klass":"RuntimeError",
55
+ "message":"This was an example exception",
56
+ "backtrace":[
57
+ "test.rb:28:in `<main>'"
58
+ ],
59
+ "uniqueness_hash":"296d169e7928c4433ccfcf091b4d737aabe83dcb"
60
+ },
61
+ "occurred_at":1281894743,
62
+ "host_environment":{
63
+ "ruby_version":"1.9.2",
64
+ "ruby_pl":-1,
65
+ "ruby_release_date":"2010-07-11",
66
+ "ruby_platform":"x86_64-darwin10.4.0"
67
+ }
68
+ }
69
+
70
+ Also, instead of assigning the application to a variable, you may also
71
+ look it up later anywhere in your application:
72
+
73
+ Radar::Application.find(:my_application).report(exception)
74
+
75
+ # Or the shorthand:
76
+ Radar[:my_application].report(exception)
77
+
78
+ ## Documentation and User Guide
79
+
80
+ For more details on configuring Radar, please view the
81
+ [user guide](http://mitchellh.github.com/radar/file.user_guide.html), which
82
+ is a detailed overview of Radar and all of its features.
83
+
84
+ ## Reporting Bugs and Requesting Features
85
+
86
+ Please use the [issues section](http://github.com/mitchellh/radar/issues) to report
87
+ bugs and request features. This section is also used as the TODO area for the
88
+ Radar gem.
89
+
90
+ ## Contributing
91
+
92
+ To hack on Radar, you'll need [bundler](http://github.com/carlhuda/bundler) which
93
+ can be installed with a simple `gem install bundler`. Then, do the following:
94
+
95
+ bundle install
96
+ rake
97
+
98
+ This will run the test suite, which should come back all green! Then you're
99
+ good to go.
100
+
101
+ The general steps to contributing a new change are:
102
+
103
+ 1. Fork the repository
104
+ 2. Make your changes, writing tests if necessary
105
+ 3. Open an [issue](http://github.com/mitchellh/radar/issues) with the feature and
106
+ a link to your fork or a gist with a patch.
107
+ 4. Wait patiently, for I am but one man.
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rake/testtask'
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ task :default => :test
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.libs << "test"
10
+ t.pattern = 'test/**/*_test.rb'
11
+ end
12
+
13
+ begin
14
+ require 'yard'
15
+ YARD::Rake::YardocTask.new
16
+ rescue LoadError
17
+ end
@@ -0,0 +1,197 @@
1
+ # Radar User's Guide
2
+
3
+ ## Overview
4
+
5
+ Radar is a tool which provides a drop-in solution to catching and reporting
6
+ errors in your Ruby application via customizable mediums.
7
+
8
+ ## Installation
9
+
10
+ Install via RubyGems:
11
+
12
+ gem install radar
13
+
14
+ Or specify in your own library or application's `Gemfile` or `gemspec` so
15
+ that the gem is included properly.
16
+
17
+ ## Basic Usage
18
+
19
+ ### Setup
20
+
21
+ First, as early as possible in your application so that Radar can begin
22
+ catching exceptions right away, create a new {Radar::Application}
23
+ instance for your own app, replacing `my_application` with a unique name for your
24
+ application.
25
+
26
+ Radar::Application.new(:my_application)
27
+
28
+ But this alone won't do anything, since you haven't configured any reporters
29
+ for the application. Reporters are what handle taking an {Radar::ExceptionEvent ExceptionEvent}
30
+ and doing something with it (such as storing it in a file, reporting it to a
31
+ remote server, etc.). Radar comes with some built-in reporters. Below, we configure
32
+ the application to log errors to a file (by default at `~/.radar/errors/my_application`):
33
+
34
+ Radar::Application.new(:my_application) do |app|
35
+ app.config.reporter Radar::Reporter::FileReporter
36
+ end
37
+
38
+ ### Reporting Errors
39
+
40
+ Once the application is setup, there are two methods to report errors:
41
+
42
+ 1. Manually call the {Radar::Application#report report} method on the application.
43
+ 2. Tell the application to {Radar::Application#rescue_at_exit! rescue at exit} so
44
+ Radar automatically catches any exceptions before your application crashes.
45
+
46
+ Calling the report method manually:
47
+
48
+ app = Radar::Application.new(:my_application)
49
+ app.report(exception)
50
+
51
+ The use case for this is in a `rescue` block somewhere, and forces Radar
52
+ to report the given exception.
53
+
54
+ Telling Radar to catch exceptions on exit is equally simple, and can be
55
+ used in conjunction with the above method as well:
56
+
57
+ app = Radar::Application.new(:my_application)
58
+ app.rescue_at_exit!
59
+
60
+ Now, whenever your application is about to crash (an exception not caught by
61
+ a `rescue`), Radar will catch your exception and report it just prior to
62
+ crashing.
63
+
64
+ # Features
65
+
66
+ ## Reporters
67
+
68
+ On its own, Radar does nothing but catch and shuttle exceptions to reporters. Without
69
+ reporters, Radar is basically useless! A reporter is a class which takes an
70
+ {Radar::ExceptionEvent} and does something with it. Its easier to get a better idea
71
+ of what this means with a few examples:
72
+
73
+ * `FileReporter` - Stores the data from an exception on the filesystem for each
74
+ exception.
75
+ * `ServerReporter` - Transfers information about an exception to some remote
76
+ server.
77
+
78
+ ### Enabling and Configuring a Reporter
79
+
80
+ Reporters are enabled using the appilication configuration:
81
+
82
+ Radar::Application.new(:my_application) do |app|
83
+ app.config.reporter FileReporter
84
+ end
85
+
86
+ And can be configured by passing a block to the reporter, which is yielded with
87
+ the instance of that reporter:
88
+
89
+ Radar::Application.new(:my_application) do |app|
90
+ app.config.reporter FileReporter do |reporter|
91
+ reporter.storage_directory = "~/.radar/exceptions"
92
+ end
93
+ end
94
+
95
+ Radar also allows multiple reporters to be used, which are then called
96
+ in the order they are defined when an exception occurs:
97
+
98
+ Radar::Application.new(:my_application) do |app|
99
+ app.config.reporter FileReporter
100
+ app.config.reporter AnotherReporter
101
+ end
102
+
103
+ ### Built-in Reporters
104
+
105
+ #### FileReporter
106
+
107
+ {Radar::Reporter::FileReporter FileReporter} outputs exception information as JSON to a file
108
+ on the local filesystem. The filename is in the format of `timestamp-uniquehash.txt`,
109
+ where `timestamp` is the time that the exception occurred and `uniquehash` is the
110
+ {Radar::ExceptionEvent#uniqueness_hash}.
111
+
112
+ The directory where these files will be stored is configurable:
113
+
114
+ Radar::Application.new(:my_application) do |app|
115
+ app.config.reporter Radar::Reporter::FileReporter do |reporter|
116
+ reporter.output_directory = "~/my_application_errors"
117
+ end
118
+ end
119
+
120
+ You may also use a lambda. Below is also the default value for the FileReporter:
121
+
122
+ reporter.output_directory = lambda { |event| "~/.radar/#{event.application.name}" }
123
+
124
+ A few notes:
125
+
126
+ * The FileReporter does not automatically handle cleaning up old exception files or reporting
127
+ these files to any remote server. It is up to you, if you wish, to clean up old
128
+ exception information.
129
+ * The JSON output is compressed JSON, and is not pretty printed. It is up to you to take the
130
+ JSON and pretty print it if you wish it to be easily human readable. There are
131
+ [services](http://jsonformatter.curiousconcept.com/) out there to do this.
132
+
133
+ ### Custom Reporters
134
+
135
+ It is very easy to write custom reporters. A reporter is simply a class which
136
+ responds to `report` and takes a single {Radar::ExceptionEvent} as a parameter.
137
+ Below is an example of a reporter which simply prints out that an error
138
+ occurred:
139
+
140
+ class StdoutReporter
141
+ def report(event)
142
+ puts "An exception occurred! Message: #{event.exception.message}"
143
+ end
144
+ end
145
+
146
+ And then using that reporter is just as easy:
147
+
148
+ Radar::Application.new(:my_application) do |app|
149
+ app.config.reporter StdoutReporter
150
+ end
151
+
152
+ ## Data Extensions
153
+
154
+ Data extensions allow you to easily extend the data represented by {Radar::ExceptionEvent#to_hash}.
155
+ By default, Radar only sends a small amount of information about the host environment and the
156
+ exception when an exception occurs. Often its more helpful to get more information about the
157
+ environment to more easily track down a bug. Some examples:
158
+
159
+ * HTTP request headers for a web application
160
+ * Configuration settings for a desktop application
161
+
162
+ ### Defining Data Extensions
163
+
164
+ Data extensions are defined with classes which are expected to be initialized
165
+ with a reference to a {Radar::ExceptionEvent} and must implement the `to_hash`
166
+ method. Below is a data extension to add the output of `uname -a` to the event:
167
+
168
+ class UnameExtension
169
+ def initialize(event)
170
+ @event = event
171
+ end
172
+
173
+ def to_hash
174
+ { :uname => `uname -a` }
175
+ end
176
+ end
177
+
178
+ ### Enabling Data Extensions
179
+
180
+ Data extensions are enabled via the application configuration like most other
181
+ things:
182
+
183
+ Radar::Application.new(:my_application) do |app|
184
+ app.config.data_extension UnameExtension
185
+ end
186
+
187
+ ### Built-In Data Extensions
188
+
189
+ By default, {Radar::ExceptionEvent#to_hash} (view the source) returns very little
190
+ information on its own. To be as general and extensible as possible, even the data
191
+ such as information about the host are created using built-in data extensions.
192
+ Some of these are enabled by default, which are designated by the `*` on the name.
193
+
194
+ * {Radar::DataExtensions::HostEnvironment HostEnvironment}* - Adds information about the
195
+ host such as Ruby version and operating system.
196
+
197
+ `*`: Enabled by default on every application.
@@ -0,0 +1,18 @@
1
+ require 'radar/version'
2
+ require 'radar/error'
3
+
4
+ module Radar
5
+ autoload :Application, 'radar/application'
6
+ autoload :Config, 'radar/config'
7
+ autoload :ExceptionEvent, 'radar/exception_event'
8
+ autoload :Reporter, 'radar/reporter'
9
+ autoload :Support, 'radar/support'
10
+
11
+ module DataExtensions
12
+ autoload :HostEnvironment, 'radar/data_extensions/host_environment'
13
+ end
14
+
15
+ class Reporter
16
+ autoload :FileReporter, 'radar/reporter/file_reporter'
17
+ end
18
+ end
@@ -0,0 +1,96 @@
1
+ require 'thread'
2
+
3
+ module Radar
4
+ # A shortcut for {Application.find}.
5
+ def [](*args)
6
+ Application.find(*args)
7
+ end
8
+ module_function :[]
9
+
10
+ # Represents an instance of Radar for a given application. Every
11
+ # application which uses Radar must instantiate an {Application}.
12
+ class Application
13
+ @@registered = {}
14
+ @@mutex = Mutex.new
15
+
16
+ attr_reader :name
17
+ attr_reader :creation_location
18
+
19
+ # Looks up an application which was registered with the given name.
20
+ #
21
+ # @param [String] name Application name.
22
+ # @return [Application]
23
+ def self.find(name)
24
+ @@registered[name]
25
+ end
26
+
27
+ # Removes all registered applications. **This is only exposed for testing
28
+ # purposes.**
29
+ def self.clear!
30
+ @@registered.clear
31
+ end
32
+
33
+ # Creates a new application with the given name and registers
34
+ # it for lookup later. If a block is given, it will be yielded with
35
+ # the new instantiated {Application} so you can also {#config} it
36
+ # all in one go.
37
+ #
38
+ # @param [String] name Application name. This must be unique for
39
+ # any given application or an exception will be raised.
40
+ # @return [Application]
41
+ def initialize(name, register=true)
42
+ @@mutex.synchronize do
43
+ raise ApplicationAlreadyExists.new("'#{name}' already defined at '#{self.class.find(name).creation_location}'") if self.class.find(name)
44
+
45
+ @name = name
46
+ @creation_location = caller.first
47
+ yield self if block_given?
48
+ @@registered[name] = self if register
49
+ end
50
+ end
51
+
52
+ # Configures the application by returning the configuration
53
+ # object. If a block is given, the configuration object will be
54
+ # passed into it, allowing for a cleaner way of configuring your
55
+ # applications.
56
+ #
57
+ # $app = Radar::Application.new
58
+ # $app.config do |config|
59
+ # config.storage_directory = "foo"
60
+ # end
61
+ #
62
+ # @yield [Config] Configuration object, only if block is given.
63
+ # @return [Config]
64
+ def config
65
+ @_config ||= Config.new
66
+ yield @_config if block_given?
67
+ @_config
68
+ end
69
+
70
+ # Reports an exception. This will send the exception on to the
71
+ # various reporters configured for this application.
72
+ #
73
+ # @param [Exception] exception
74
+ def report(exception)
75
+ data = ExceptionEvent.new(self, exception)
76
+
77
+ # Report the exception to each of the reporters
78
+ config.reporters.each do |reporter|
79
+ reporter.report(data)
80
+ end
81
+ end
82
+
83
+ # Hooks this application into the `at_exit` handler so that
84
+ # application crashing exceptions are properly reported.
85
+ def rescue_at_exit!
86
+ at_exit { report($!) if $! }
87
+ end
88
+
89
+ # Converts application to a serialization-friendly hash.
90
+ #
91
+ # @return [Hash]
92
+ def to_hash
93
+ { :name => name }
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,35 @@
1
+ module Radar
2
+ class Config
3
+ attr_reader :reporters
4
+ attr_reader :data_extensions
5
+
6
+ def initialize
7
+ @reporters = []
8
+ @data_extensions = [DataExtensions::HostEnvironment]
9
+ end
10
+
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.
14
+ #
15
+ # @param [Class] klass A {Reporter} class.
16
+ def reporter(klass)
17
+ instance = klass.new
18
+ yield instance if block_given?
19
+ @reporters << instance
20
+ end
21
+
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.
26
+ #
27
+ # This method takes a class. This class is expected to be initialized
28
+ # with an {ExceptionEvent}, and must implement the `to_hash` method.
29
+ #
30
+ # @param [Class] klass
31
+ def data_extension(klass)
32
+ @data_extensions << klass
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ module Radar
2
+ module DataExtensions
3
+ # Data extension which adds information about the host environment:
4
+ #
5
+ # * Operating system
6
+ # * Ruby version
7
+ #
8
+ class HostEnvironment
9
+ def initialize(event)
10
+ @event = event
11
+ end
12
+
13
+ def to_hash
14
+ { :host_environment => {
15
+ :ruby_version => (RUBY_VERSION rescue '?'),
16
+ :ruby_pl => (RUBY_PATCHLEVEL rescue '?'),
17
+ :ruby_release_date => (RUBY_RELEASE_DATE rescue '?'),
18
+ :ruby_platform => (RUBY_PLATFORM rescue '?')
19
+ }
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,6 @@
1
+ module Radar
2
+ # Represents an internal radar error. This class is made so its easy
3
+ # for users of radar to catch all radar related errors.
4
+ class Error < StandardError; end
5
+ class ApplicationAlreadyExists < Error; end
6
+ end
@@ -0,0 +1,61 @@
1
+ require 'digest/sha1'
2
+ require 'json'
3
+
4
+ module Radar
5
+ # Represents the event of an exception being captured. This class
6
+ # contains references to the {Application} and exception which is
7
+ # raised.
8
+ class ExceptionEvent
9
+ attr_reader :application
10
+ attr_reader :exception
11
+ attr_reader :occurred_at
12
+
13
+ def initialize(application, exception)
14
+ @application = application
15
+ @exception = exception
16
+ @occurred_at = Time.now
17
+ end
18
+
19
+ # A hash of information about this exception event. This includes
20
+ # {Application#to_hash} as well as information about the exception.
21
+ # This also includes any {Config#data_extension data_extensions} if
22
+ # specified.
23
+ #
24
+ # @return [Hash]
25
+ def to_hash
26
+ result = { :application => application.to_hash,
27
+ :exception => {
28
+ :klass => exception.class.to_s,
29
+ :message => exception.message,
30
+ :backtrace => exception.backtrace,
31
+ :uniqueness_hash => uniqueness_hash
32
+ },
33
+ :occurred_at => occurred_at.to_i
34
+ }
35
+
36
+ if !application.config.data_extensions.empty?
37
+ application.config.data_extensions.each do |extension|
38
+ Support::Hash.deep_merge!(result, extension.new(self).to_hash)
39
+ end
40
+ end
41
+
42
+ result
43
+ end
44
+
45
+ # JSONified {#to_hash} output.
46
+ #
47
+ # @return [String]
48
+ def to_json
49
+ to_hash.to_json
50
+ end
51
+
52
+ # Returns uniqueness hash to test if one event is roughly equivalent to
53
+ # another. The uniqueness hash is generated by taking the exception
54
+ # backtrace and class and generating the SHA1 hash of those concatenated.
55
+ #
56
+ # @return [String]
57
+ def uniqueness_hash
58
+ Digest::SHA1.hexdigest("#{exception.class}-#{exception.backtrace rescue 'blank'}")
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,29 @@
1
+ module Radar
2
+ # This class defines the interface for a {Reporter}. A reporter
3
+ # is a class which takes exception information and "reports" it
4
+ # in some way. Examples of reporters (note that not all of these
5
+ # may be implemented; they are ideas):
6
+ #
7
+ # - FileReporter - Logs the exception out to a timestamped
8
+ # file in a specified directory.
9
+ # - ServerReporter - Logs the exception to a server somewhere for
10
+ # later retrieval.
11
+ # - GrowlReporter - Causes a Growl notification to occur, noting
12
+ # that an exception occurred.
13
+ #
14
+ # From the examples above it is clear to see that the reporter doesn't
15
+ # necessarilly need to store the exception information anywhere, it
16
+ # is simply required to take some action in the event that an exception
17
+ # occurred.
18
+ #
19
+ # Radar allows for multiple reporters to be used together, so a reporter
20
+ # doesn't need to do everything. They're meant to be small units of
21
+ # functionality.
22
+ #
23
+ class Reporter
24
+ # Report the environment.
25
+ def report(environment)
26
+ raise "Implement the `report` method in #{self.class}"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,37 @@
1
+ require 'fileutils'
2
+
3
+ module Radar
4
+ class Reporter
5
+ # Reports exceptions by dumping the JSON data out to a file on the
6
+ # local filesystem. The reporter is configurable:
7
+ #
8
+ # * {#output_directory=}
9
+ #
10
+ class FileReporter
11
+ attr_accessor :output_directory
12
+
13
+ def initialize
14
+ @output_directory = lambda { |event| "~/.radar/errors/#{event.application.name}" }
15
+ end
16
+
17
+ def report(event)
18
+ output_file = File.join(File.expand_path(output_directory(event)), "#{event.occurred_at.to_i}-#{event.uniqueness_hash}.txt")
19
+
20
+ # Attempt to make the directory if it doesn't exist
21
+ FileUtils.mkdir_p File.dirname(output_file)
22
+
23
+ # Write out the JSON to the output file
24
+ File.open(output_file, 'w') { |f| f.write(event.to_json) }
25
+ end
26
+
27
+ # Returns the currently configured output directory. If `event` is given
28
+ # as a parameter and the currently set directory is a lambda, then the
29
+ # lambda will be evaluated then returned. If no event is given, the lambda
30
+ # is returned as-is.
31
+ def output_directory(event=nil)
32
+ return @output_directory.call(event) if event && @output_directory.is_a?(Proc)
33
+ @output_directory
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ module Radar
2
+ class Support
3
+ class Hash
4
+ #----------------------------------------------------------------------
5
+ # Deep Merging - Taken from Ruby on Rails ActiveSupport
6
+ #----------------------------------------------------------------------
7
+
8
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
9
+ def self.deep_merge(source, other)
10
+ deep_merge!(source.dup, other)
11
+ end
12
+
13
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
14
+ # Modifies the receiver in place.
15
+ def self.deep_merge!(source, other)
16
+ other.each_pair do |k,v|
17
+ tv = source[k]
18
+ source[k] = tv.is_a?(::Hash) && v.is_a?(::Hash) ? deep_merge(tv, v) : v
19
+ end
20
+
21
+ source
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module Radar
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
+ require 'radar/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "radar"
7
+ s.version = Radar::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Mitchell Hashimoto"]
10
+ s.email = ["mitchell.hashimoto@gmail.com"]
11
+ s.homepage = "http://rubygems.org/gems/radar"
12
+ s.summary = "Easily catch and report errors in your Ruby libraries and applications any way you want!"
13
+ s.description = "Radar provides a drop-in solution to catching and reporting errors in your libraries and applications."
14
+
15
+ s.required_rubygems_version = ">= 1.3.6"
16
+ s.rubyforge_project = "radar"
17
+
18
+ s.add_dependency "json", ">= 1.4.6"
19
+
20
+ s.add_development_dependency "bundler", ">= 1.0.0.rc.5"
21
+ s.add_development_dependency "shoulda"
22
+ s.add_development_dependency "mocha"
23
+ s.add_development_dependency "rake"
24
+
25
+ s.files = `git ls-files`.split("\n")
26
+ s.executables = `git ls-files`.split("\n").select{|f| f =~ /^bin/}
27
+ s.require_path = 'lib'
28
+ end
@@ -0,0 +1,105 @@
1
+ require 'test_helper'
2
+
3
+ class ApplicationTest < Test::Unit::TestCase
4
+ context "application class" do
5
+ setup do
6
+ @klass = Radar::Application
7
+ @instance = @klass.new("bar", false)
8
+ end
9
+
10
+ context "initializing" do
11
+ teardown do
12
+ @klass.clear!
13
+ end
14
+
15
+ should "be able to create for a name" do
16
+ instance = @klass.new("foo")
17
+ assert_equal "foo", instance.name
18
+ end
19
+
20
+ should "be able to lookup after created" do
21
+ instance = @klass.new("foo")
22
+ assert_equal instance, @klass.find("foo")
23
+ end
24
+
25
+ should "allow creation of unregistered applications" do
26
+ instance = @klass.new("foo", false)
27
+ assert_nil @klass.find("foo")
28
+ end
29
+
30
+ should "raise an exception if duplicate name is used" do
31
+ assert_raises(Radar::ApplicationAlreadyExists) {
32
+ @klass.new("foo")
33
+ @klass.new("foo")
34
+ }
35
+ end
36
+
37
+ should "yield with the instance if a block is given" do
38
+ @klass.new("foo") do |instance|
39
+ assert instance.is_a?(@klass)
40
+ end
41
+ end
42
+ end
43
+
44
+ context "configuration" do
45
+ setup do
46
+ @instance.config.reporters.clear
47
+
48
+ @reporter = Class.new
49
+ end
50
+
51
+ should "be able to configure an application" do
52
+ @instance.config.reporter(@reporter)
53
+ assert !@instance.config.reporters.empty?
54
+ end
55
+
56
+ should "be able to configure using a block" do
57
+ @instance.config do |config|
58
+ config.reporter(@reporter)
59
+ end
60
+
61
+ assert !@instance.config.reporters.empty?
62
+ end
63
+ end
64
+
65
+ context "reporting" do
66
+ setup do
67
+ # The fake reporter class
68
+ reporter = Class.new do
69
+ def report(environment)
70
+ raise "success"
71
+ end
72
+ end
73
+
74
+ # Setup the application to use the fake reporter
75
+ @instance.config do |config|
76
+ config.reporter reporter
77
+ end
78
+ end
79
+
80
+ should "call report on each registered reporter" do
81
+ assert_raises(RuntimeError) do
82
+ begin
83
+ @instance.report(Exception.new)
84
+ rescue => e
85
+ assert_equal "success", e.message
86
+ raise
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ context "to_hash" do
93
+ setup do
94
+ @hash = @instance.to_hash
95
+ end
96
+
97
+ should "contain name" do
98
+ assert_equal @instance.name, @hash[:name]
99
+ end
100
+ end
101
+
102
+ # Untested: Application#rescue_at_exit! since I'm not aware of an
103
+ # [easy] way of testing it without spawning out a separate process.
104
+ end
105
+ end
@@ -0,0 +1,59 @@
1
+ require 'test_helper'
2
+
3
+ class ConfigTest < Test::Unit::TestCase
4
+ context "configuration" do
5
+ setup do
6
+ @klass = Radar::Config
7
+ @instance = @klass.new
8
+ end
9
+
10
+ context "reporters" do
11
+ setup do
12
+ @reporter_klass = Class.new
13
+ end
14
+
15
+ teardown do
16
+ @instance.reporters.clear
17
+ end
18
+
19
+ should "initially have no reporters" do
20
+ assert @instance.reporters.empty?
21
+ end
22
+
23
+ should "be able to add reporters" do
24
+ @instance.reporter @reporter_klass
25
+ assert !@instance.reporters.empty?
26
+ assert @instance.reporters.first.is_a?(@reporter_klass)
27
+ end
28
+
29
+ should "yield the reporter instance if a block is given" do
30
+ @reporter_klass.any_instance.expects(:some_method).once
31
+ @instance.reporter @reporter_klass do |reporter|
32
+ reporter.some_method
33
+ end
34
+ end
35
+ end
36
+
37
+ context "data extensions" do
38
+ setup do
39
+ @extension = Class.new do
40
+ def initialize(event)
41
+ end
42
+ end
43
+ end
44
+
45
+ teardown do
46
+ @instance.data_extensions.clear
47
+ end
48
+
49
+ should "initially have some data extensions" do
50
+ assert_equal [Radar::DataExtensions::HostEnvironment], @instance.data_extensions
51
+ end
52
+
53
+ should "be able to add data extensions" do
54
+ @instance.data_extension @extension
55
+ assert !@instance.data_extensions.empty?
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+
3
+ class HostEnvironmentDataTest < Test::Unit::TestCase
4
+ context "host environment class" do
5
+ setup do
6
+ @klass = Radar::DataExtensions::HostEnvironment
7
+ @instance = @klass.new(create_exception_event)
8
+ end
9
+
10
+ should "be able to convert to a hash" do
11
+ result = @instance.to_hash
12
+ assert result.is_a?(Hash)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,48 @@
1
+ require 'test_helper'
2
+
3
+ class ExceptionEventTest < Test::Unit::TestCase
4
+ context "ExceptionEvent class" do
5
+ setup do
6
+ @klass = Radar::ExceptionEvent
7
+ @instance = create_exception_event
8
+ end
9
+
10
+ context "to_hash" do
11
+ context "data extensions" do
12
+ setup do
13
+ @extension = Class.new do
14
+ def initialize(event); @event = event; end
15
+ def to_hash; { :exception => { :foo => :bar } }; end
16
+ end
17
+
18
+ @instance.application.config.data_extension @extension
19
+ @result = @instance.to_hash
20
+ end
21
+
22
+ should "include data extensions if defined" do
23
+ assert @result[:exception].has_key?(:foo), "instance should have key: foo"
24
+ assert_equal :bar, @result[:exception][:foo]
25
+ end
26
+
27
+ should "deep merge information" do
28
+ assert @result[:exception].has_key?(:klass)
29
+ end
30
+ end
31
+ end
32
+
33
+ context "to_json" do
34
+ should "just jsonify hash output" do
35
+ assert_equal @instance.to_hash.to_json, @instance.to_json
36
+ end
37
+ 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
+ end
48
+ end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ class FileReporterTest < Test::Unit::TestCase
4
+ context "file reporter class" do
5
+ setup do
6
+ @klass = Radar::Reporter::FileReporter
7
+ @instance = @klass.new
8
+ end
9
+
10
+ should "allow output directory to be a lambda" do
11
+ @instance.output_directory = lambda { |event| event.application.name }
12
+ event = create_exception_event
13
+ assert_equal event.application.name, @instance.output_directory(event)
14
+ end
15
+
16
+ should "allow output directory to be a string" do
17
+ value = "value"
18
+ @instance.output_directory = value
19
+ assert_equal value, @instance.output_directory
20
+ end
21
+
22
+ should "just return the lambda if no event is given to read" do
23
+ @instance.output_directory = lambda {}
24
+ assert @instance.output_directory.is_a?(Proc)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,14 @@
1
+ require 'test_helper'
2
+
3
+ class ReporterTest < Test::Unit::TestCase
4
+ context "reporter class" do
5
+ setup do
6
+ @klass = Radar::Reporter
7
+ @instance = @klass.new
8
+ end
9
+
10
+ should "raise an exception for an unimplemented reporter" do
11
+ assert_raises(RuntimeError) { @instance.report(nil) }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ class SupportTest < Test::Unit::TestCase
4
+ context "Support class" do
5
+ setup do
6
+ @klass = Radar::Support
7
+ end
8
+
9
+ context "hash" do
10
+ should "deep merge simple hashes" do
11
+ result = @klass::Hash.deep_merge({ :a => 1 }, { :b => 2 })
12
+ assert_equal 1, result[:a]
13
+ assert_equal 2, result[:b]
14
+ end
15
+
16
+ should "support deep merging" do
17
+ a = { :a => { :a => 1 } }
18
+ b = { :a => { :b => 2 } }
19
+
20
+ result = @klass::Hash.deep_merge(a, b)
21
+ assert_equal 1, result[:a][:a]
22
+ assert_equal 2, result[:a][:b]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ $:.unshift File.expand_path('../../lib', __FILE__)
2
+
3
+ require "rubygems"
4
+ require "bundler/setup"
5
+ require "test/unit"
6
+ require "shoulda"
7
+ require "mocha"
8
+ require "radar"
9
+
10
+ class Test::Unit::TestCase
11
+ # Returns a real {Radar::ExceptionEvent} object with a newly created
12
+ # {Radar::Application} and a valid (has a backtrace) exception.
13
+ def create_exception_event
14
+ application = Radar::Application.new(:foo, false)
15
+ exception = nil
16
+
17
+ begin
18
+ raise "Something bad happened!"
19
+ rescue => e
20
+ exception = e
21
+ end
22
+
23
+ Radar::ExceptionEvent.new(application, exception)
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: radar
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Mitchell Hashimoto
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-08-15 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: json
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 1
29
+ - 4
30
+ - 6
31
+ version: 1.4.6
32
+ type: :runtime
33
+ prerelease: false
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: bundler
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 1
44
+ - 0
45
+ - 0
46
+ - rc
47
+ - 5
48
+ version: 1.0.0.rc.5
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: shoulda
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: *id003
65
+ - !ruby/object:Gem::Dependency
66
+ name: mocha
67
+ requirement: &id004 !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: *id004
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *id005
91
+ description: Radar provides a drop-in solution to catching and reporting errors in your libraries and applications.
92
+ email:
93
+ - mitchell.hashimoto@gmail.com
94
+ executables: []
95
+
96
+ extensions: []
97
+
98
+ extra_rdoc_files: []
99
+
100
+ files:
101
+ - .gitignore
102
+ - .yardopts
103
+ - Gemfile
104
+ - Gemfile.lock
105
+ - README.md
106
+ - Rakefile
107
+ - docs/user_guide.md
108
+ - lib/radar.rb
109
+ - lib/radar/application.rb
110
+ - lib/radar/config.rb
111
+ - lib/radar/data_extensions/host_environment.rb
112
+ - lib/radar/error.rb
113
+ - lib/radar/exception_event.rb
114
+ - lib/radar/reporter.rb
115
+ - lib/radar/reporter/file_reporter.rb
116
+ - lib/radar/support.rb
117
+ - lib/radar/version.rb
118
+ - radar.gemspec
119
+ - test/radar/application_test.rb
120
+ - test/radar/config_test.rb
121
+ - test/radar/data_extensions/host_environment_test.rb
122
+ - test/radar/exception_event_test.rb
123
+ - test/radar/reporter/file_reporter_test.rb
124
+ - test/radar/reporter_test.rb
125
+ - test/radar/support_test.rb
126
+ - test/test_helper.rb
127
+ has_rdoc: true
128
+ homepage: http://rubygems.org/gems/radar
129
+ licenses: []
130
+
131
+ post_install_message:
132
+ rdoc_options: []
133
+
134
+ require_paths:
135
+ - lib
136
+ required_ruby_version: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ hash: 4451322989948183573
142
+ segments:
143
+ - 0
144
+ version: "0"
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ none: false
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ segments:
151
+ - 1
152
+ - 3
153
+ - 6
154
+ version: 1.3.6
155
+ requirements: []
156
+
157
+ rubyforge_project: radar
158
+ rubygems_version: 1.3.7
159
+ signing_key:
160
+ specification_version: 3
161
+ summary: Easily catch and report errors in your Ruby libraries and applications any way you want!
162
+ test_files: []
163
+