radar 0.1.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.
@@ -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
+