radar 0.2.0 → 0.3.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.
Files changed (39) hide show
  1. data/.yardopts +1 -0
  2. data/CHANGELOG.md +14 -1
  3. data/Gemfile +10 -4
  4. data/Gemfile.lock +65 -1
  5. data/README.md +10 -1
  6. data/docs/user_guide.md +214 -18
  7. data/examples/README.md +5 -0
  8. data/examples/rack/README.md +15 -0
  9. data/examples/rack/config.ru +18 -0
  10. data/lib/radar.rb +20 -2
  11. data/lib/radar/application.rb +30 -1
  12. data/lib/radar/config.rb +54 -12
  13. data/lib/radar/data_extensions/rack.rb +72 -0
  14. data/lib/radar/error.rb +1 -0
  15. data/lib/radar/exception_event.rb +6 -4
  16. data/lib/radar/filters/key_filter.rb +54 -0
  17. data/lib/radar/integration/rack.rb +41 -0
  18. data/lib/radar/integration/rails3.rb +19 -0
  19. data/lib/radar/integration/rails3/generator.rb +19 -0
  20. data/lib/radar/integration/rails3/railtie.rb +12 -0
  21. data/lib/radar/integration/rails3/templates/README +17 -0
  22. data/lib/radar/integration/rails3/templates/radar.rb +15 -0
  23. data/lib/radar/logger.rb +37 -0
  24. data/lib/radar/reporter/file_reporter.rb +31 -12
  25. data/lib/radar/reporter/io_reporter.rb +35 -0
  26. data/lib/radar/reporter/logger_reporter.rb +31 -0
  27. data/lib/radar/version.rb +1 -1
  28. data/radar.gemspec +2 -4
  29. data/test/radar/application_test.rb +38 -0
  30. data/test/radar/config_test.rb +34 -0
  31. data/test/radar/data_extensions/rack_test.rb +51 -0
  32. data/test/radar/exception_event_test.rb +20 -0
  33. data/test/radar/filters/key_filter_test.rb +28 -0
  34. data/test/radar/integration/rack_test.rb +61 -0
  35. data/test/radar/integration/rails3_test.rb +29 -0
  36. data/test/radar/logger_test.rb +13 -0
  37. data/test/radar/reporter/io_reporter_test.rb +20 -0
  38. data/test/radar/reporter/logger_reporter_test.rb +21 -0
  39. metadata +25 -4
@@ -0,0 +1,19 @@
1
+ # Provides a generator to rails 3 to generate the initializer
2
+ # file in `config/initializers/radar.rb`. This class is not
3
+ # scoped since Rails generates the generator scope based on
4
+ # the Ruby scope (e.g. this allows the command to just be
5
+ # "rails g radar" instead of "rails g radar:integrations:rails3:radar"
6
+ # or some other crazy string).
7
+ class RadarGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ desc "Creates a Radar initializer"
11
+
12
+ def copy_initializer
13
+ template "radar.rb", "config/initializers/radar.rb"
14
+ end
15
+
16
+ def show_readme
17
+ readme "README"
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ require "rails"
2
+
3
+ module Radar
4
+ # The Radar Railtie allows Radar to integrate with Rails 3 by
5
+ # adding generators. **This file is only loaded automatically
6
+ # for Rails 3**.
7
+ class Railtie < Rails::Railtie
8
+ generators do
9
+ require File.expand_path("../generator", __FILE__)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+
2
+ ================================================================================
3
+
4
+ Radar has been setup for this Rails 3 project! BUT...
5
+
6
+ Some manual work you must complete before Radar is completely prepared:
7
+
8
+ * Open `config/initializers/radar.rb` and add at least one reporter to
9
+ the Radar application so Radar knows how you'd like your exceptions
10
+ handled. Example (this would go in the block in that file):
11
+
12
+ app.config.reporters.use :file
13
+
14
+ Once a reporter is added, Radar has already been setup to integrate with
15
+ Rails 3, so you may use your application like normal.
16
+
17
+ ================================================================================
@@ -0,0 +1,15 @@
1
+ # This creates a Radar application for your Rails app. Use the block
2
+ # to configure it. For detailed documentation, please see the user guide
3
+ # online at: http://radargem.com/file.user_guide.html
4
+ Radar::Application.new(:<%= Rails.application.class.to_s.underscore.tr('/', '_') %>) do |app|
5
+ # ==> Reporter Configuration
6
+ # Configure any reporters here. Reporters tell Radar how to report exceptions.
7
+ # This may be to a file, to a server, to a stream, etc. At least one reporter
8
+ # is required for Radar to do something with your exceptions. By default,
9
+ # Radar reports to the Rails logger. Change this if you want to report to
10
+ # a file, a server, etc.
11
+ app.reporters.use :logger, :log_object => Rails.logger, :log_level => :error
12
+
13
+ # Tell Radar to integrate this application with Rails 3.
14
+ app.integrate :rails3
15
+ end
@@ -0,0 +1,37 @@
1
+ require 'logger'
2
+ require 'fileutils'
3
+
4
+ module Radar
5
+ # A lightweight logger which logs what Radar does to a single
6
+ # configurable location. This logger is simply meant as a way
7
+ # you can verify that Radar is working as intended, and not meant
8
+ # as a logger of every exception's data; this is the job of {Reporter}s.
9
+ class Logger < ::Logger
10
+ attr_reader :application
11
+
12
+ def initialize(application)
13
+ @application = application
14
+ super(log_location)
15
+ end
16
+
17
+ def format_message(severity, timestamp, progname, message)
18
+ "[#{application.name}][#{severity[0,1].upcase}][#{timestamp}] -- #{message}\n"
19
+ end
20
+
21
+ # Returns the location of the logfile. This is configurable using
22
+ # {Config#log_location=}.
23
+ #
24
+ # @return [String]
25
+ def log_location
26
+ location = @application.config.log_location
27
+ location = location.is_a?(Proc) ? location.call(application) : location
28
+
29
+ if location.is_a?(String)
30
+ directory = File.dirname(location)
31
+ FileUtils.mkdir_p(directory) if !File.directory?(directory)
32
+ end
33
+
34
+ location
35
+ end
36
+ end
37
+ end
@@ -35,23 +35,34 @@ module Radar
35
35
  attr_accessor :output_directory
36
36
  attr_accessor :prune_time
37
37
 
38
- def initialize
39
- @output_directory = lambda { |event| "~/.radar/errors/#{event.application.name}" }
40
- @prune_time = nil
38
+ def initialize(opts=nil)
39
+ (opts || {}).each do |k,v|
40
+ send("#{k}=", v)
41
+ end
42
+
43
+ @output_directory ||= lambda { |event| "~/.radar/errors/#{event.application.name}" }
41
44
  end
42
45
 
43
46
  def report(event)
47
+ @event = event
44
48
  output_file = File.join(File.expand_path(output_directory(event)), "#{event.occurred_at.to_i}-#{event.uniqueness_hash}.txt")
45
49
  directory = File.dirname(output_file)
46
50
 
47
- # Attempt to make the directory if it doesn't exist
48
- FileUtils.mkdir_p directory
51
+ begin
52
+ # Attempt to make the directory if it doesn't exist
53
+ FileUtils.mkdir_p(directory) if !File.directory?(directory)
49
54
 
50
- # Prune files if enabled
51
- prune(directory) if prune_time
55
+ # Prune files if enabled
56
+ prune(directory) if prune_time
52
57
 
53
- # Write out the JSON to the output file
54
- File.open(output_file, 'w') { |f| f.write(event.to_json) }
58
+ # Write out the JSON to the output file
59
+ log("#{self.class}: Reported to #{output_file}")
60
+ File.open(output_file, 'w') { |f| f.write(event.to_json) }
61
+ rescue Errno::EACCES
62
+ # Rebrand the exception so its easier to tell what exactly
63
+ # is going on.
64
+ raise ReporterError.new("#{self.class}: Failed to create directory or log to: #{output_file}")
65
+ end
55
66
  end
56
67
 
57
68
  # Prunes the files in the given directory according to the age limit
@@ -59,11 +70,19 @@ module Radar
59
70
  #
60
71
  # @param [String] directory Directory to prune
61
72
  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
73
+ count = Dir[File.join(directory, "*.txt")].inject(0) do |acc, file|
74
+ next acc unless File.file?(file)
75
+ next acc unless (Time.now.to_i - File.ctime(file).to_i) >= prune_time.to_i
65
76
  File.delete(file)
77
+ acc + 1
66
78
  end
79
+
80
+ log("Pruned #{count} file(s) in #{directory}.") if count > 0
81
+ end
82
+
83
+ # Convenience method for logging.
84
+ def log(message)
85
+ @event.application.logger.info("#{self.class}: #{message}") if @event
67
86
  end
68
87
 
69
88
  # Returns the currently configured output directory. If `event` is given
@@ -0,0 +1,35 @@
1
+ module Radar
2
+ class Reporter
3
+ # A reporter which simply dumps the event JSON out to some IO
4
+ # object. If you're outputting to a file, you should look into
5
+ # {FileReporter} instead, which will automatically create unique
6
+ # filenames per exception.
7
+ #
8
+ # Some uses for this reporter:
9
+ #
10
+ # - Output to `stderr`, since a process's `stderr` may be redirected
11
+ # to a log file already.
12
+ # - Output to `stdout`, just for testing.
13
+ # - Output to some network IO stream to talk to a server across
14
+ # a network.
15
+ #
16
+ class IoReporter
17
+ attr_accessor :io_object
18
+
19
+ def initialize(opts=nil)
20
+ (opts || {}).each do |k,v|
21
+ send("#{k}=", v)
22
+ end
23
+ end
24
+
25
+ def report(event)
26
+ return if !io_object
27
+ raise ArgumentError.new("IoReporter `io_object` must be an IO object.") if !io_object.is_a?(IO)
28
+
29
+ # Straight push the object to the object and flush immediately
30
+ io_object.puts(event.to_json)
31
+ io_object.flush
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ module Radar
2
+ class Reporter
3
+ # A reporter which logs to a Ruby `Logger`-like object (any object
4
+ # which responds to the various log levels). This reporter is useful
5
+ # if you wish to integrate Radar into your already existing logging
6
+ # systems.
7
+ #
8
+ # app.config.reporters.use :logger, :log_object => Logger.new(STDOUT)
9
+ # app.config.reporters.use :logger, :log_object => Logger.new(STDOUT), :log_level => :warn
10
+ #
11
+ class LoggerReporter
12
+ attr_accessor :log_object
13
+ attr_accessor :log_level
14
+
15
+ def initialize(opts=nil)
16
+ (opts || {}).each do |k,v|
17
+ send("#{k}=", v)
18
+ end
19
+
20
+ @log_level ||= :error
21
+ end
22
+
23
+ def report(event)
24
+ raise ArgumentError.new("#{self.class} `log_object` must be set to a valid logger.") if !log_object.is_a?(Logger)
25
+ raise ArgumentError.new("#{self.class} `log_object` must respond to specified `log_level`.") if !log_object.respond_to?(log_level)
26
+
27
+ log_object.send(log_level, event.to_json)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,3 +1,3 @@
1
1
  module Radar
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,6 +1,4 @@
1
- # -*- encoding: utf-8 -*-
2
- $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
3
- require 'radar/version'
1
+ require File.expand_path("../lib/radar/version", __FILE__)
4
2
 
5
3
  Gem::Specification.new do |s|
6
4
  s.name = "radar"
@@ -23,6 +21,6 @@ Gem::Specification.new do |s|
23
21
  s.add_development_dependency "rake"
24
22
 
25
23
  s.files = `git ls-files`.split("\n")
26
- s.executables = `git ls-files`.split("\n").select{|f| f =~ /^bin/}
24
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
27
25
  s.require_path = 'lib'
28
26
  end
@@ -62,6 +62,14 @@ class ApplicationTest < Test::Unit::TestCase
62
62
  end
63
63
  end
64
64
 
65
+ context "logger" do
66
+ should "provide a logger which is initialized on access" do
67
+ Radar::Logger.expects(:new).with(@instance).once.returns("foo")
68
+ @instance.logger
69
+ @instance.logger
70
+ end
71
+ end
72
+
65
73
  context "reporting" do
66
74
  should "call report on each registered reporter" do
67
75
  reporter = Class.new do
@@ -117,6 +125,18 @@ class ApplicationTest < Test::Unit::TestCase
117
125
  end
118
126
  end
119
127
 
128
+ context "integrations" do
129
+ should "integrate with built-in integrators" do
130
+ Radar::Integration::Rack.expects(:integrate!).with(@instance)
131
+ @instance.integrate(:rack)
132
+ end
133
+
134
+ should "integrate with specified classes" do
135
+ Radar::Integration::Rack.expects(:integrate!).with(@instance)
136
+ @instance.integrate(Radar::Integration::Rack)
137
+ end
138
+ end
139
+
120
140
  context "to_hash" do
121
141
  setup do
122
142
  @hash = @instance.to_hash
@@ -127,6 +147,24 @@ class ApplicationTest < Test::Unit::TestCase
127
147
  end
128
148
  end
129
149
 
150
+ context "delegation to config" do
151
+ should "delegate reporters" do
152
+ assert_equal @instance.config.reporters, @instance.reporters
153
+ end
154
+
155
+ should "delegate data extensions" do
156
+ assert_equal @instance.config.data_extensions, @instance.data_extensions
157
+ end
158
+
159
+ should "delegate matchers" do
160
+ assert_equal @instance.config.matchers, @instance.matchers
161
+ end
162
+
163
+ should "delegate filters" do
164
+ assert_equal @instance.config.filters, @instance.filters
165
+ end
166
+ end
167
+
130
168
  # Untested: Application#rescue_at_exit! since I'm not aware of an
131
169
  # [easy] way of testing it without spawning out a separate process.
132
170
  end
@@ -26,6 +26,12 @@ class ConfigTest < Test::Unit::TestCase
26
26
  assert @instance.reporters.values.first.is_a?(@reporter_klass)
27
27
  end
28
28
 
29
+ should "be able to add reporters via built-in symbols" do
30
+ @instance.reporters.use :file
31
+ assert !@instance.reporters.empty?
32
+ assert @instance.reporters.values.first.is_a?(Radar::Reporter::FileReporter)
33
+ end
34
+
29
35
  should "yield the reporter instance if a block is given" do
30
36
  @reporter_klass.any_instance.expects(:some_method).once
31
37
  @instance.reporters.use @reporter_klass do |reporter|
@@ -54,6 +60,11 @@ class ConfigTest < Test::Unit::TestCase
54
60
  @instance.data_extensions.use @extension
55
61
  assert !@instance.data_extensions.empty?
56
62
  end
63
+
64
+ should "be able to add built-in extensions via symbols" do
65
+ @instance.data_extensions.use :rack
66
+ assert_equal Radar::DataExtensions::Rack, @instance.data_extensions.values.last
67
+ end
57
68
  end
58
69
 
59
70
  context "matchers" do
@@ -75,6 +86,29 @@ class ConfigTest < Test::Unit::TestCase
75
86
  @instance.match @matcher
76
87
  assert !@instance.matchers.empty?
77
88
  end
89
+
90
+ should "be able to use built-in matchers as symbols" do
91
+ @instance.match :class, Object
92
+ assert @instance.matchers.values.first.is_a?(Radar::Matchers::ClassMatcher)
93
+ end
94
+ end
95
+
96
+ context "filters" do
97
+ teardown do
98
+ @instance.filters.clear
99
+ end
100
+
101
+ should "raise an ArgumentError if no class or lambda is given" do
102
+ assert_raises(ArgumentError) { @instance.filters.use }
103
+ end
104
+
105
+ should "be able to use just a block" do
106
+ assert_nothing_raised {
107
+ @instance.filters.use do |foo|
108
+ end
109
+ }
110
+ assert_equal 1, @instance.filters.length
111
+ end
78
112
  end
79
113
  end
80
114
 
@@ -0,0 +1,51 @@
1
+ require 'test_helper'
2
+
3
+ class RackDataTest < Test::Unit::TestCase
4
+ context "rack data extension class" do
5
+ setup do
6
+ @klass = Radar::DataExtensions::Rack
7
+ @event = create_exception_event
8
+ @instance = @klass.new(@event)
9
+ end
10
+
11
+ should "be able to convert to a hash" do
12
+ assert @instance.respond_to?(:to_hash)
13
+ assert @instance.to_hash.is_a?(Hash)
14
+ end
15
+
16
+ should "merge in only HTTP headers" do
17
+ @event.extra[:rack_env] = {
18
+ "HTTP_CONTENT_TYPE" => "text/html",
19
+ "other" => "baz"
20
+ }
21
+
22
+ result = @instance.to_hash
23
+ assert result[:request][:headers].has_key?("Content-Type")
24
+ assert !result[:request][:headers].has_key?("other")
25
+ end
26
+
27
+ context "merging in rack environment properly" do
28
+ setup do
29
+ @event.extra[:rack_env] = {
30
+ "HTTP_CONTENT_TYPE" => "text/html",
31
+ "other" => 7,
32
+ "else" => Class.new.new
33
+ }
34
+
35
+ @result = @instance.to_hash[:request][:rack_env]
36
+ end
37
+
38
+ should "not merge in the HTTP headers" do
39
+ assert !@result.has_key?("HTTP_CONTENT_TYPE")
40
+ end
41
+
42
+ should "merge in the numeric value as-is" do
43
+ assert_equal 7, @result["other"]
44
+ end
45
+
46
+ should "merge in the foreign class as a string" do
47
+ assert_equal @event.extra[:rack_env]["else"].to_s, @result["else"]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -45,6 +45,26 @@ class ExceptionEventTest < Test::Unit::TestCase
45
45
  should "deep merge information" do
46
46
  assert @result[:exception].has_key?(:klass)
47
47
  end
48
+
49
+ should "deep merge properly even if to_hash returns nil" do
50
+ @extension.any_instance.stubs(:to_hash).returns(nil)
51
+ assert_nothing_raised { @instance.to_hash }
52
+ end
53
+ end
54
+
55
+ context "filters" do
56
+ should "have an application key by default" do
57
+ assert @instance.to_hash.has_key?(:application)
58
+ end
59
+
60
+ should "not filter out the application key with filter" do
61
+ @instance.application.config.filters.use do |data|
62
+ data.delete(:application)
63
+ data
64
+ end
65
+
66
+ assert !@instance.to_hash.has_key?(:application)
67
+ end
48
68
  end
49
69
  end
50
70