radar 0.2.0 → 0.3.0

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