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.
- 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
|
|