radar 0.3.0 → 0.4.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/.gitignore +2 -1
- data/CHANGELOG.md +21 -0
- data/Gemfile +3 -2
- data/Gemfile.lock +47 -48
- data/README.md +14 -25
- data/docs/user_guide.md +155 -30
- data/examples/rack/config.ru +1 -1
- data/examples/sinatra/README.md +15 -0
- data/examples/sinatra/example.rb +18 -0
- data/lib/radar.rb +11 -5
- data/lib/radar/application.rb +14 -3
- data/lib/radar/backtrace.rb +42 -0
- data/lib/radar/config.rb +112 -20
- data/lib/radar/data_extensions/rack.rb +4 -21
- data/lib/radar/data_extensions/rails2.rb +31 -0
- data/lib/radar/data_extensions/request_helper.rb +28 -0
- data/lib/radar/exception_event.rb +10 -4
- data/lib/radar/integration/rails2.rb +31 -0
- data/lib/radar/integration/rails2/action_controller_rescue.rb +30 -0
- data/lib/radar/integration/rails3.rb +0 -2
- data/lib/radar/integration/rails3/railtie.rb +0 -2
- data/lib/radar/integration/sinatra.rb +21 -0
- data/lib/radar/matchers/local_request_matcher.rb +43 -0
- data/lib/radar/reporter/hoptoad_reporter.rb +204 -0
- data/lib/radar/reporter/logger_reporter.rb +2 -3
- data/lib/radar/version.rb +1 -1
- data/radar.gemspec +1 -0
- data/test/radar/application_test.rb +39 -18
- data/test/radar/backtrace_test.rb +42 -0
- data/test/radar/config_test.rb +49 -4
- data/test/radar/data_extensions/rails2_test.rb +14 -0
- data/test/radar/exception_event_test.rb +9 -0
- data/test/radar/integration/rack_test.rb +1 -1
- data/test/radar/integration/sinatra_test.rb +13 -0
- data/test/radar/matchers/local_request_matcher_test.rb +26 -0
- data/test/radar/reporter/hoptoad_reporter_test.rb +20 -0
- data/test/radar/reporter/logger_reporter_test.rb +0 -4
- metadata +41 -12
@@ -8,13 +8,15 @@ module Radar
|
|
8
8
|
class ExceptionEvent
|
9
9
|
attr_reader :application
|
10
10
|
attr_reader :exception
|
11
|
-
attr_reader :
|
11
|
+
attr_reader :backtrace
|
12
12
|
attr_reader :extra
|
13
|
+
attr_reader :occurred_at
|
13
14
|
|
14
15
|
def initialize(application, exception, extra=nil)
|
15
16
|
@application = application
|
16
|
-
@exception
|
17
|
-
@
|
17
|
+
@exception = exception
|
18
|
+
@backtrace = Backtrace.new(exception.backtrace)
|
19
|
+
@extra = extra || {}
|
18
20
|
@occurred_at = Time.now
|
19
21
|
end
|
20
22
|
|
@@ -25,11 +27,13 @@ module Radar
|
|
25
27
|
#
|
26
28
|
# @return [Hash]
|
27
29
|
def to_hash
|
30
|
+
return @_to_hash_result if @_to_hash_result
|
31
|
+
|
28
32
|
result = { :application => application.to_hash,
|
29
33
|
:exception => {
|
30
34
|
:klass => exception.class.to_s,
|
31
35
|
:message => exception.message,
|
32
|
-
:backtrace =>
|
36
|
+
:backtrace => backtrace,
|
33
37
|
:uniqueness_hash => uniqueness_hash
|
34
38
|
},
|
35
39
|
:occurred_at => occurred_at.to_i
|
@@ -43,6 +47,8 @@ module Radar
|
|
43
47
|
result = filter.call(result)
|
44
48
|
end
|
45
49
|
|
50
|
+
# Cache the resulting hash to it is only generated once.
|
51
|
+
@_to_hash_result = result
|
46
52
|
result
|
47
53
|
end
|
48
54
|
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'radar/integration/rails2/action_controller_rescue'
|
2
|
+
|
3
|
+
module Radar
|
4
|
+
module Integration
|
5
|
+
# Allows drop-in integration with Rails 2 for Radar. This monkeypatches
|
6
|
+
# `ActionController::Base` to capture errors and also enables a data
|
7
|
+
# extension to extract the Rails 2 request information into the exception
|
8
|
+
# event.
|
9
|
+
class Rails2
|
10
|
+
@@integrated_apps = []
|
11
|
+
|
12
|
+
def self.integrate!(app)
|
13
|
+
# Only monkeypatch ActionController::Base once
|
14
|
+
ActionController::Base.send(:include, ActionControllerRescue) if @@integrated_apps.empty?
|
15
|
+
|
16
|
+
# Only integrate each application once
|
17
|
+
if !@@integrated_apps.include?(app)
|
18
|
+
app.data_extensions.use :rails2
|
19
|
+
@@integrated_apps << app
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns all the Radar applications which have been integrated with
|
24
|
+
# Rails 2, in the order that they were integrated. This Array is `dup`ed
|
25
|
+
# so that no modifications can be made to it.
|
26
|
+
def self.integrated_apps
|
27
|
+
@@integrated_apps.dup
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Radar
|
2
|
+
module Integration
|
3
|
+
class Rails2
|
4
|
+
module ActionControllerRescue
|
5
|
+
def self.included(base)
|
6
|
+
# Unfortunate alias method chain... solution: upgrade to rails 3 ;)
|
7
|
+
base.send(:alias_method, :rescue_action_without_radar, :rescue_action)
|
8
|
+
base.send(:alias_method, :rescue_action, :rescue_action_with_radar)
|
9
|
+
base.send(:protected, :rescue_action)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def rescue_action_with_radar(exception)
|
15
|
+
report_exception_to_radar(exception)
|
16
|
+
rescue_action_without_radar(exception)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Report an exception to all Radar applications which chose to integrate
|
20
|
+
# with Rails 2.
|
21
|
+
def report_exception_to_radar(exception)
|
22
|
+
Radar::Integration::Rails2.integrated_apps.each do |app|
|
23
|
+
app.report(exception, :rails2_request => request)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Radar
|
2
|
+
module Integration
|
3
|
+
# Allows drop-in integration with Sinatra for Radar. This class
|
4
|
+
# should not ever actually be used {Application#integrate}. Instead,
|
5
|
+
# use the middlware provided by {Rack::Radar}.
|
6
|
+
#
|
7
|
+
# Radar::Application.new(:app) do |app|
|
8
|
+
# # configure...
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# class MyApp < Sinatra::Base
|
12
|
+
# use Rack::Radar, :application => Radar[:app]
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
class Sinatra
|
16
|
+
def self.integrate!(app)
|
17
|
+
raise "To enable Sinatra integration, please use `Rack::Radar` middleware instead. View the user guide for more information."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Radar
|
2
|
+
module Matchers
|
3
|
+
# A matcher which matches exceptions if the request is from
|
4
|
+
# a local IP. The IPs which constitute a "local request" are
|
5
|
+
# those which match any of the following:
|
6
|
+
#
|
7
|
+
# [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/]
|
8
|
+
#
|
9
|
+
# If there is no request information found in the exception event
|
10
|
+
# data, then it will not match.
|
11
|
+
#
|
12
|
+
# This matcher expects the IP to be accessible at `event.to_hash[:request][:remote_ip]`,
|
13
|
+
# though this is configurable. Examples of usage are shown below:
|
14
|
+
#
|
15
|
+
# app.match :local_request
|
16
|
+
#
|
17
|
+
# With a custom IP field:
|
18
|
+
#
|
19
|
+
# app.match :local_request, :remote_ip_getter => lamdba { |event| event.to_hash[:remote_ip] }
|
20
|
+
#
|
21
|
+
class LocalRequestMatcher
|
22
|
+
attr_accessor :localhost
|
23
|
+
attr_accessor :remote_ip_getter
|
24
|
+
|
25
|
+
def initialize(opts=nil)
|
26
|
+
(opts || {}).each do |k,v|
|
27
|
+
send("#{k}=", v)
|
28
|
+
end
|
29
|
+
|
30
|
+
@localhost ||= [/^127\.0\.0\.\d{1,3}$/, "::1", /^0:0:0:0:0:0:0:1(%.*)?$/]
|
31
|
+
@remote_ip_getter ||= Proc.new { |event| event.to_hash[:request][:remote_ip] }
|
32
|
+
end
|
33
|
+
|
34
|
+
def matches?(event)
|
35
|
+
remote_ip = remote_ip_getter.call(event)
|
36
|
+
localhost.any? { |local_ip| local_ip === remote_ip }
|
37
|
+
rescue Exception
|
38
|
+
# Any exceptions assume that we didn't match.
|
39
|
+
false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
require 'builder'
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/https'
|
4
|
+
|
5
|
+
module Radar
|
6
|
+
class Reporter
|
7
|
+
# Thanks to the Hoptoad Notifier library for the format of the XML and
|
8
|
+
# also the `net/http` code. Writing this reporter would have been much
|
9
|
+
# more trying if Thoughtbot's code wasn't open.
|
10
|
+
|
11
|
+
# Reports exceptions to the Hoptoad server (http://hoptoadapp.com). Enabling
|
12
|
+
# this in your Radar application is easy:
|
13
|
+
#
|
14
|
+
# app.reporters.use :hoptoad, :api_key => "hoptoad-api-key"
|
15
|
+
#
|
16
|
+
# The API key is your project's API key which can be found on the Hoptoad
|
17
|
+
# website. There are many additional options which can be set, but the most
|
18
|
+
# useful are probably `project_root` and `environment_name`. These will be
|
19
|
+
# auto-detected in a Rails application but for all others, its helpful
|
20
|
+
# to set them:
|
21
|
+
#
|
22
|
+
# app.reporters.use :hoptoad do |r|
|
23
|
+
# r.api_key = "api-key"
|
24
|
+
# r.project_root = File.expand_path("../../", __FILE__)
|
25
|
+
# r.environment_name = "development"
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
class HoptoadReporter
|
29
|
+
API_VERSION = "2.0"
|
30
|
+
NOTICES_URL = "/notifier_api/v2/notices/"
|
31
|
+
|
32
|
+
# Options which should be set:
|
33
|
+
attr_accessor :api_key
|
34
|
+
attr_accessor :project_root
|
35
|
+
attr_accessor :environment_name
|
36
|
+
|
37
|
+
# The rest can probably be left alone:
|
38
|
+
# HTTP settings
|
39
|
+
attr_accessor :host
|
40
|
+
attr_accessor :headers
|
41
|
+
attr_accessor :secure
|
42
|
+
attr_accessor :http_open_timeout
|
43
|
+
attr_accessor :http_read_timeout
|
44
|
+
|
45
|
+
# Proxy settings
|
46
|
+
attr_accessor :proxy_host
|
47
|
+
attr_accessor :proxy_port
|
48
|
+
attr_accessor :proxy_user
|
49
|
+
attr_accessor :proxy_pass
|
50
|
+
|
51
|
+
# Notifier information. This defaults to Radar information.
|
52
|
+
attr_accessor :notifier_name
|
53
|
+
attr_accessor :notifier_version
|
54
|
+
attr_accessor :notifier_url
|
55
|
+
|
56
|
+
def initialize(opts=nil)
|
57
|
+
(opts || {}).each do |k,v|
|
58
|
+
send("#{k}=", v)
|
59
|
+
end
|
60
|
+
|
61
|
+
@project_root ||= defined?(Rails) ? Rails.root.to_s : '/not/set'
|
62
|
+
@environment_name ||= defined?(Rails) ? Rails.env.to_s : 'radar'
|
63
|
+
|
64
|
+
@host ||= 'hoptoadapp.com'
|
65
|
+
@headers ||= { 'Content-type' => 'text/xml', 'Accept' => 'text/xml, application/xml' }
|
66
|
+
@secure ||= false
|
67
|
+
@http_open_timeout ||= 2
|
68
|
+
@http_read_timeout ||= 5
|
69
|
+
|
70
|
+
@notifier_name ||= "Radar"
|
71
|
+
@notifier_version ||= Radar::VERSION
|
72
|
+
@notifier_url ||= "http://radargem.com"
|
73
|
+
end
|
74
|
+
|
75
|
+
def report(event)
|
76
|
+
raise ArgumentError.new("`api_key` is required.") if !api_key
|
77
|
+
|
78
|
+
http = Net::HTTP.Proxy(proxy_host, proxy_port, proxy_user, proxy_pass).new(url.host, url.port)
|
79
|
+
http.read_timeout = http_read_timeout
|
80
|
+
http.open_timeout = http_open_timeout
|
81
|
+
http.use_ssl = secure
|
82
|
+
|
83
|
+
response = begin
|
84
|
+
http.post(url.path, event_xml(event), headers)
|
85
|
+
rescue TimeoutError => e
|
86
|
+
event.application.logger.error("#{self.class}: POST timeout.")
|
87
|
+
nil
|
88
|
+
end
|
89
|
+
|
90
|
+
if !response.is_a?(Net::HTTPSuccess)
|
91
|
+
event.application.logger.error("#{self.class}: Failed to send: #{response.body}")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Converts an event to the properly formatted XML for transmission
|
96
|
+
# to Hoptoad.
|
97
|
+
def event_xml(event)
|
98
|
+
request_data = request_info(event)
|
99
|
+
|
100
|
+
builder = Builder::XmlMarkup.new
|
101
|
+
builder.instruct!
|
102
|
+
xml = builder.notice(:version => API_VERSION) do |notice|
|
103
|
+
notice.tag!("api-key", api_key)
|
104
|
+
|
105
|
+
notice.notifier do |notifier|
|
106
|
+
notifier.name(notifier_name)
|
107
|
+
notifier.version(notifier_version)
|
108
|
+
notifier.url(notifier_url)
|
109
|
+
end
|
110
|
+
|
111
|
+
notice.error do |error|
|
112
|
+
error.tag!("class", event.exception.class.to_s)
|
113
|
+
error.message(event.exception.message)
|
114
|
+
error.backtrace do |backtrace|
|
115
|
+
event.backtrace.each do |entry|
|
116
|
+
backtrace.line(:number => entry.line, :file => entry.file, :method => entry.method)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
if !request_data.empty?
|
122
|
+
notice.request do |request|
|
123
|
+
request.url(request_data[:url])
|
124
|
+
request.component(request_data[:controller])
|
125
|
+
request.action(request_data[:action])
|
126
|
+
|
127
|
+
if !request_data[:parameters].empty?
|
128
|
+
request.params do |params|
|
129
|
+
xml_vars_for_hash(params, request_data[:parameters])
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
if !request_data[:session].empty?
|
134
|
+
request.session do |session|
|
135
|
+
xml_vars_for_hash(session, request_data[:session])
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
if !request_data[:cgi_data].empty?
|
140
|
+
request.tag!("cgi-data") do |cgi|
|
141
|
+
xml_vars_for_hash(cgi, request_data[:cgi_data])
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
notice.tag!("server-environment") do |env|
|
148
|
+
env.tag!("project-root", project_root)
|
149
|
+
env.tag!("environment-name", environment_name)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
xml.to_s
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns a URI object pointed to the proper endpoint for the
|
157
|
+
# Hoptoad API.
|
158
|
+
def url
|
159
|
+
protocol = secure ? 'https' : 'http'
|
160
|
+
port = secure ? 443 : 80
|
161
|
+
|
162
|
+
URI.parse("#{protocol}://#{host}:#{port}").merge(NOTICES_URL)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Returns information about the request based on the event, such
|
166
|
+
# as URL, controller, action, parameters, etc.
|
167
|
+
def request_info(event)
|
168
|
+
return @_request_info if @_request_info
|
169
|
+
|
170
|
+
# Use the hash of the event so that if any filters deleted data, it is properly
|
171
|
+
# removed.
|
172
|
+
hash = event.to_hash.dup
|
173
|
+
hash[:request] ||= {}
|
174
|
+
hash[:request][:rack_env] ||= {}
|
175
|
+
|
176
|
+
@_request_info = {}
|
177
|
+
|
178
|
+
if hash[:request]
|
179
|
+
@_request_info[:url] = hash[:request][:url]
|
180
|
+
@_request_info[:parameters] = hash[:request][:rack_env]['action_dispatch.request.parameters'] ||
|
181
|
+
hash[:request][:parameters] ||
|
182
|
+
{}
|
183
|
+
@_request_info[:controller] = @_request_info[:parameters]['controller']
|
184
|
+
@_request_info[:action] = @_request_info[:parameters]['action']
|
185
|
+
@_request_info[:cgi_data] = hash[:request][:rack_env] || hash[:request][:headers] || {}
|
186
|
+
@_request_info[:session] = hash[:request][:rack_env]['rack.session'] || {}
|
187
|
+
end
|
188
|
+
|
189
|
+
@_request_info
|
190
|
+
end
|
191
|
+
|
192
|
+
# Turns a hash into the proper XML vars
|
193
|
+
def xml_vars_for_hash(builder, hash)
|
194
|
+
hash.each do |k,v|
|
195
|
+
if v.is_a?(Hash)
|
196
|
+
builder.var(:key => k.to_s) { |b| xml_vars_for_hash(b, v) }
|
197
|
+
else
|
198
|
+
builder.var(v.to_s, :key => k.to_s)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
@@ -5,8 +5,8 @@ module Radar
|
|
5
5
|
# if you wish to integrate Radar into your already existing logging
|
6
6
|
# systems.
|
7
7
|
#
|
8
|
-
# app.
|
9
|
-
# app.
|
8
|
+
# app.reporters.use :logger, :log_object => Logger.new(STDOUT)
|
9
|
+
# app.reporters.use :logger, :log_object => Logger.new(STDOUT), :log_level => :warn
|
10
10
|
#
|
11
11
|
class LoggerReporter
|
12
12
|
attr_accessor :log_object
|
@@ -21,7 +21,6 @@ module Radar
|
|
21
21
|
end
|
22
22
|
|
23
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
24
|
raise ArgumentError.new("#{self.class} `log_object` must respond to specified `log_level`.") if !log_object.respond_to?(log_level)
|
26
25
|
|
27
26
|
log_object.send(log_level, event.to_json)
|
data/lib/radar/version.rb
CHANGED
data/radar.gemspec
CHANGED
@@ -45,7 +45,9 @@ class ApplicationTest < Test::Unit::TestCase
|
|
45
45
|
setup do
|
46
46
|
@instance.config.reporters.clear
|
47
47
|
|
48
|
-
@reporter = Class.new
|
48
|
+
@reporter = Class.new do
|
49
|
+
def report(event); end
|
50
|
+
end
|
49
51
|
end
|
50
52
|
|
51
53
|
should "be able to configure an application" do
|
@@ -108,19 +110,39 @@ class ApplicationTest < Test::Unit::TestCase
|
|
108
110
|
def matches?(event); event.extra[:foo] == :bar; end
|
109
111
|
end
|
110
112
|
|
111
|
-
@reporter = Class.new
|
113
|
+
@reporter = Class.new do
|
114
|
+
def report(event); raise "Reported"; end
|
115
|
+
end
|
116
|
+
|
112
117
|
@instance.config.reporters.use @reporter
|
113
118
|
@instance.config.match @matcher
|
114
119
|
end
|
115
120
|
|
116
121
|
should "not report if a matcher is specified and doesn't match" do
|
117
|
-
@
|
118
|
-
@instance.report(Exception.new, :foo => :wrong)
|
122
|
+
assert_nothing_raised { @instance.report(Exception.new, :foo => :wrong) }
|
119
123
|
end
|
120
124
|
|
121
125
|
should "report if a matcher matches" do
|
122
|
-
@
|
123
|
-
|
126
|
+
assert_raises(RuntimeError) { @instance.report(Exception.new, :foo => :bar) }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context "with a rejecter" do
|
131
|
+
setup do
|
132
|
+
@rejecter = Class.new do
|
133
|
+
def matches?(event); event.extra[:foo] == :bar; end
|
134
|
+
end
|
135
|
+
|
136
|
+
@instance.reject @rejecter
|
137
|
+
@instance.reporter { |event| raise "Reported" }
|
138
|
+
end
|
139
|
+
|
140
|
+
should "not report if a rejecter is specified and matches" do
|
141
|
+
assert_nothing_raised { @instance.report(Exception.new, :foo => :bar) }
|
142
|
+
end
|
143
|
+
|
144
|
+
should "report if the rejecter doesn't match" do
|
145
|
+
assert_raises(RuntimeError) { @instance.report(Exception.new) }
|
124
146
|
end
|
125
147
|
end
|
126
148
|
end
|
@@ -148,20 +170,19 @@ class ApplicationTest < Test::Unit::TestCase
|
|
148
170
|
end
|
149
171
|
|
150
172
|
context "delegation to config" do
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
173
|
+
# Test delegating of accessors
|
174
|
+
[:reporters, :data_extensions, :matchers, :filters, :rejecters].each do |attr|
|
175
|
+
should "delegate #{attr}" do
|
176
|
+
assert_equal @instance.config.send(attr), @instance.send(attr)
|
177
|
+
end
|
161
178
|
end
|
162
179
|
|
163
|
-
|
164
|
-
|
180
|
+
# Test delegating of methods.
|
181
|
+
[:reporter, :data_extension, :match, :filter, :reject].each do |method|
|
182
|
+
should "delegate `#{method}` method" do
|
183
|
+
@instance.config.expects(method).once
|
184
|
+
@instance.send(method)
|
185
|
+
end
|
165
186
|
end
|
166
187
|
end
|
167
188
|
|