radar 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +1 -0
- data/CHANGELOG.md +14 -1
- data/Gemfile +10 -4
- data/Gemfile.lock +65 -1
- data/README.md +10 -1
- data/docs/user_guide.md +214 -18
- data/examples/README.md +5 -0
- data/examples/rack/README.md +15 -0
- data/examples/rack/config.ru +18 -0
- data/lib/radar.rb +20 -2
- data/lib/radar/application.rb +30 -1
- data/lib/radar/config.rb +54 -12
- data/lib/radar/data_extensions/rack.rb +72 -0
- data/lib/radar/error.rb +1 -0
- data/lib/radar/exception_event.rb +6 -4
- data/lib/radar/filters/key_filter.rb +54 -0
- data/lib/radar/integration/rack.rb +41 -0
- data/lib/radar/integration/rails3.rb +19 -0
- data/lib/radar/integration/rails3/generator.rb +19 -0
- data/lib/radar/integration/rails3/railtie.rb +12 -0
- data/lib/radar/integration/rails3/templates/README +17 -0
- data/lib/radar/integration/rails3/templates/radar.rb +15 -0
- data/lib/radar/logger.rb +37 -0
- data/lib/radar/reporter/file_reporter.rb +31 -12
- data/lib/radar/reporter/io_reporter.rb +35 -0
- data/lib/radar/reporter/logger_reporter.rb +31 -0
- data/lib/radar/version.rb +1 -1
- data/radar.gemspec +2 -4
- data/test/radar/application_test.rb +38 -0
- data/test/radar/config_test.rb +34 -0
- data/test/radar/data_extensions/rack_test.rb +51 -0
- data/test/radar/exception_event_test.rb +20 -0
- data/test/radar/filters/key_filter_test.rb +28 -0
- data/test/radar/integration/rack_test.rb +61 -0
- data/test/radar/integration/rails3_test.rb +29 -0
- data/test/radar/logger_test.rb +13 -0
- data/test/radar/reporter/io_reporter_test.rb +20 -0
- data/test/radar/reporter/logger_reporter_test.rb +21 -0
- 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
|
data/lib/radar/logger.rb
ADDED
@@ -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
|
-
|
40
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
55
|
+
# Prune files if enabled
|
56
|
+
prune(directory) if prune_time
|
52
57
|
|
53
|
-
|
54
|
-
|
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")].
|
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
|
data/lib/radar/version.rb
CHANGED
data/radar.gemspec
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
|
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").
|
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
|
data/test/radar/config_test.rb
CHANGED
@@ -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
|
|