contrast-exceptional 0.0.1 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -0
- data/Manifest +11 -5
- data/README +3 -3
- data/Rakefile +5 -6
- data/exceptional.gemspec +32 -32
- data/init.rb +1 -19
- data/lib/exceptional.rb +12 -223
- data/lib/exceptional/api.rb +108 -0
- data/lib/exceptional/bootstrap.rb +23 -0
- data/lib/exceptional/config.rb +74 -0
- data/lib/exceptional/exception_data.rb +2 -4
- data/lib/exceptional/integration/rails.rb +25 -13
- data/lib/exceptional/log.rb +50 -0
- data/lib/exceptional/remote.rb +75 -0
- data/lib/exceptional/version.rb +1 -1
- data/spec/api_spec.rb +211 -0
- data/spec/bootstrap_spec.rb +58 -0
- data/spec/config_spec.rb +110 -0
- data/spec/exceptional_rescue_from_spec.rb +41 -0
- data/spec/exceptional_spec.rb +16 -58
- data/spec/log_spec.rb +28 -0
- data/spec/remote_spec.rb +137 -0
- data/spec/spec_helper.rb +11 -0
- metadata +43 -25
- data/lib/exceptional/agent/worker.rb +0 -56
- data/lib/exceptional/deployed_environment.rb +0 -86
- data/lib/exceptional/rails.rb +0 -53
- data/spec/deployed_environment_spec.rb +0 -168
- data/spec/worker_spec.rb +0 -21
@@ -0,0 +1,23 @@
|
|
1
|
+
module Exceptional
|
2
|
+
module Bootstrap
|
3
|
+
|
4
|
+
# called from init.rb
|
5
|
+
def bootstrap(environment, application_root)
|
6
|
+
begin
|
7
|
+
setup_config(environment, File.join(application_root,"config", "exceptional.yml"))
|
8
|
+
setup_log(File.join(application_root, "log"), log_level)
|
9
|
+
|
10
|
+
if enabled?
|
11
|
+
if authenticate
|
12
|
+
require File.join('exceptional', 'integration', 'rails')
|
13
|
+
else
|
14
|
+
STDERR.puts "Exceptional plugin not authenticated, check your API Key"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
rescue Exception => e
|
18
|
+
STDERR.puts e
|
19
|
+
STDERR.puts "Exceptional Plugin disabled."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Exceptional
|
4
|
+
module Config
|
5
|
+
|
6
|
+
# Defaults for configuration variables
|
7
|
+
REMOTE_HOST = "getexceptional.com"
|
8
|
+
REMOTE_PORT = 80
|
9
|
+
REMOTE_SSL_PORT = 443
|
10
|
+
SSL = false
|
11
|
+
LOG_LEVEL = 'info'
|
12
|
+
LOG_PATH = nil
|
13
|
+
|
14
|
+
class ConfigurationException < StandardError; end
|
15
|
+
|
16
|
+
attr_reader :api_key
|
17
|
+
attr_writer :ssl_enabled, :remote_host, :remote_port, :api_key
|
18
|
+
|
19
|
+
def setup_config(environment, config_file)
|
20
|
+
begin
|
21
|
+
config = YAML::load(File.open(config_file))[environment]
|
22
|
+
@api_key = config['api-key'] unless config['api-key'].nil?
|
23
|
+
@ssl_enabled = config['ssl'] unless config['ssl'].nil?
|
24
|
+
@log_level = config['log-level'] unless config['log-level'].nil?
|
25
|
+
@enabled = config['enabled'] unless config['enabled'].nil?
|
26
|
+
@remote_port = config['remote-port'].to_i unless config['remote-port'].nil?
|
27
|
+
@remote_host = config['remote-host'] unless config['remote-host'].nil?
|
28
|
+
@applicaton_root = application_root
|
29
|
+
|
30
|
+
log_config_info
|
31
|
+
rescue Exception => e
|
32
|
+
raise ConfigurationException.new("Unable to load configuration #{config_file} for environment #{environment} : #{e.message}")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def application_root
|
37
|
+
@applicaton_root || (File.dirname(__FILE__) + '/../..')
|
38
|
+
end
|
39
|
+
|
40
|
+
def remote_host
|
41
|
+
@remote_host || REMOTE_HOST
|
42
|
+
end
|
43
|
+
|
44
|
+
def remote_port
|
45
|
+
@remote_port || default_port
|
46
|
+
end
|
47
|
+
|
48
|
+
def log_level
|
49
|
+
@log_level || LOG_LEVEL
|
50
|
+
end
|
51
|
+
|
52
|
+
def default_port
|
53
|
+
ssl_enabled? ? REMOTE_SSL_PORT : REMOTE_PORT
|
54
|
+
end
|
55
|
+
|
56
|
+
def ssl_enabled?
|
57
|
+
@ssl_enabled || SSL
|
58
|
+
end
|
59
|
+
|
60
|
+
def enabled?
|
61
|
+
@enabled || false
|
62
|
+
end
|
63
|
+
|
64
|
+
def valid_api_key?
|
65
|
+
@api_key && @api_key.length == 40 ? true : false
|
66
|
+
end
|
67
|
+
|
68
|
+
def log_config_info
|
69
|
+
Exceptional.to_log('debug', "API Key: #{api_key}")
|
70
|
+
Exceptional.to_log('debug', "Remote Host: #{remote_host}:#{remote_port}")
|
71
|
+
Exceptional.to_log('debug', "Log level: #{log_level}")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -32,15 +32,13 @@ module Exceptional
|
|
32
32
|
hash = {}
|
33
33
|
::ATTRS.each do |attribute|
|
34
34
|
value = send(attribute)
|
35
|
-
hash[attribute] = value unless (value.nil? || value.empty?)
|
35
|
+
hash[attribute] = value unless (value.nil? || value.empty? || attribute.is_a?(TCPSocket) || attribute.is_a?(TCPServer))
|
36
36
|
end
|
37
37
|
hash
|
38
38
|
end
|
39
39
|
|
40
40
|
def to_json
|
41
41
|
self.to_hash.to_json
|
42
|
-
end
|
43
|
-
|
42
|
+
end
|
44
43
|
end
|
45
|
-
|
46
44
|
end
|
@@ -1,20 +1,32 @@
|
|
1
|
+
if defined? ActiveSupport
|
2
|
+
|
3
|
+
# Hack to force Rails version prior to 2.0 to use quoted JSON as per the JSON standard... (TODO: could be cleaner!)
|
4
|
+
if (defined?(ActiveSupport::JSON) && ActiveSupport::JSON.respond_to?(:unquote_hash_key_identifiers))
|
5
|
+
ActiveSupport::JSON.unquote_hash_key_identifiers = false
|
6
|
+
end
|
7
|
+
|
8
|
+
end
|
9
|
+
|
1
10
|
if defined? ActionController
|
2
11
|
|
3
|
-
module ActionController
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
12
|
+
module ActionController
|
13
|
+
class Base
|
14
|
+
|
15
|
+
def rescue_action_with_exceptional(exception)
|
16
|
+
# TODO potentially hook onto rescue_without_handler if it exists? would negate need to check handler_for_rescue every time.
|
17
|
+
# if there's handler defined with rescue_from() do not call Exceptional
|
18
|
+
if !(respond_to?(:handler_for_rescue) && handler_for_rescue(exception))
|
19
|
+
params_to_send = (respond_to? :filter_parameters) ? filter_parameters(params) : params
|
20
|
+
Exceptional.handle(exception, self, request, params_to_send)
|
21
|
+
end
|
10
22
|
|
11
|
-
|
12
|
-
|
23
|
+
rescue_action_without_exceptional exception
|
24
|
+
end
|
13
25
|
|
14
|
-
|
15
|
-
|
16
|
-
|
26
|
+
alias_method :rescue_action_without_exceptional, :rescue_action
|
27
|
+
alias_method :rescue_action, :rescue_action_with_exceptional
|
28
|
+
protected :rescue_action
|
29
|
+
end
|
17
30
|
end
|
18
|
-
end
|
19
31
|
|
20
32
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Exceptional
|
4
|
+
module Log
|
5
|
+
|
6
|
+
attr_reader :log
|
7
|
+
|
8
|
+
def setup_log(log_dir, log_level = Logger::INFO)
|
9
|
+
begin
|
10
|
+
Dir.mkdir(log_dir) unless File.directory?(log_dir)
|
11
|
+
|
12
|
+
|
13
|
+
log_path = File.join(log_dir, "/exceptional.log")
|
14
|
+
log = Logger.new log_path
|
15
|
+
|
16
|
+
log.level = log_level
|
17
|
+
|
18
|
+
allowed_log_levels = ['debug', 'info', 'warn', 'error', 'fatal']
|
19
|
+
if log_level && allowed_log_levels.include?(log_level)
|
20
|
+
log.level = eval("Logger::#{log_level.upcase}")
|
21
|
+
end
|
22
|
+
|
23
|
+
@log = log
|
24
|
+
rescue Exception => e
|
25
|
+
raise Exceptional::Config::ConfigurationException.new("Unable to create log file #{log_path} #{e.message}")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def log!(msg, level = 'info')
|
30
|
+
to_log level, msg
|
31
|
+
to_stderr msg
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_stderr(msg)
|
35
|
+
STDERR.puts format_log_message(msg)
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def to_log(level, msg)
|
41
|
+
@log.send level, msg if @log
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def format_log_message(msg)
|
47
|
+
"** [Exceptional] " + msg
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'cgi'
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module Exceptional
|
6
|
+
module Remote
|
7
|
+
|
8
|
+
class RemoteException < StandardError; end
|
9
|
+
|
10
|
+
::PROTOCOL_VERSION = 3
|
11
|
+
|
12
|
+
# authenticate with getexceptional.com
|
13
|
+
# returns true if the configured api_key is registered and can send data
|
14
|
+
# otherwise false
|
15
|
+
def authenticate
|
16
|
+
|
17
|
+
return @authenticated if @authenticated
|
18
|
+
|
19
|
+
if Exceptional.api_key.nil?
|
20
|
+
raise Exceptional::Config::ConfigurationException.new("API Key must be configured")
|
21
|
+
end
|
22
|
+
|
23
|
+
begin
|
24
|
+
# TODO No data required to authenticate, send a nil string? hacky
|
25
|
+
# TODO should retry if a http connection failed
|
26
|
+
authenticated = call_remote(:authenticate, "")
|
27
|
+
|
28
|
+
@authenticated = authenticated =~ /true/ ? true : false
|
29
|
+
rescue
|
30
|
+
@authenticated = false
|
31
|
+
ensure
|
32
|
+
return @authenticated
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def authenticated?
|
37
|
+
@authenticated || false
|
38
|
+
end
|
39
|
+
|
40
|
+
def post_exception(data)
|
41
|
+
if !authenticated?
|
42
|
+
authenticate
|
43
|
+
end
|
44
|
+
|
45
|
+
call_remote(:errors, data)
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def call_remote(method, data)
|
51
|
+
begin
|
52
|
+
http = Net::HTTP.new(Exceptional.remote_host, Exceptional.remote_port)
|
53
|
+
http.use_ssl = true if Exceptional.ssl_enabled?
|
54
|
+
uri = "/#{method.to_s}?&api_key=#{Exceptional.api_key}&protocol_version=#{::PROTOCOL_VERSION}"
|
55
|
+
headers = method.to_s == 'errors' ? { 'Content-Type' => 'application/x-gzip', 'Accept' => 'application/x-gzip' } : {}
|
56
|
+
|
57
|
+
compressed_data = CGI::escape(Zlib::Deflate.deflate(data, Zlib::BEST_SPEED))
|
58
|
+
response = http.start do |http|
|
59
|
+
http.post(uri, compressed_data, headers)
|
60
|
+
end
|
61
|
+
|
62
|
+
if response.kind_of? Net::HTTPSuccess
|
63
|
+
return response.body
|
64
|
+
else
|
65
|
+
raise RemoteException.new("#{response.code}: #{response.message}")
|
66
|
+
end
|
67
|
+
|
68
|
+
rescue Exception => e
|
69
|
+
Exceptional.log! "Error contacting Exceptional: #{e}", 'info'
|
70
|
+
Exceptional.log! e.backtrace.join("\n"), 'debug'
|
71
|
+
raise e
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/exceptional/version.rb
CHANGED
data/spec/api_spec.rb
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe Exceptional::Api do
|
5
|
+
|
6
|
+
|
7
|
+
describe "with no configuration" do
|
8
|
+
before(:each) do
|
9
|
+
Exceptional.stub!(:to_stderr) # Don't print error when testing
|
10
|
+
end
|
11
|
+
|
12
|
+
after(:each) do
|
13
|
+
Exceptional.api_key= nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should connect to getexceptional.com by default" do
|
17
|
+
Exceptional.remote_host.should == "getexceptional.com"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should connect to port 80 by default" do
|
21
|
+
Exceptional.remote_port.should == 80
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should parse exception into exception data object" do
|
25
|
+
exception = mock(Exception, :message => "Something bad has happened",
|
26
|
+
:backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"])
|
27
|
+
exception_data = Exceptional.parse(exception)
|
28
|
+
exception_data.kind_of?(Exceptional::ExceptionData).should be_true
|
29
|
+
exception_data.exception_message.should == exception.message
|
30
|
+
exception_data.exception_backtrace.should == exception.backtrace
|
31
|
+
exception_data.exception_class.should == exception.class.to_s
|
32
|
+
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should post exception" do
|
37
|
+
|
38
|
+
exception_data = mock(Exceptional::ExceptionData,
|
39
|
+
:message => "Something bad has happened",
|
40
|
+
:backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"],
|
41
|
+
:class => Exception, :to_hash => { :message => "Something bad has happened" })
|
42
|
+
Exceptional.api_key = "TEST_API_KEY"
|
43
|
+
Exceptional.should_receive(:authenticate).once.and_return(true)
|
44
|
+
Exceptional.should_receive(:call_remote, :with => [:errors, exception_data]).once
|
45
|
+
Exceptional.post(exception_data)
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should catch exception" do
|
50
|
+
exception = mock(Exception, :message => "Something bad has happened",
|
51
|
+
:backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"])
|
52
|
+
|
53
|
+
exception_data = mock(Exceptional::ExceptionData,
|
54
|
+
:message => "Something bad has happened",
|
55
|
+
:backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"],
|
56
|
+
:class => Exception, :to_hash => { :message => "Something bad has happened" })
|
57
|
+
exception_data.should_receive(:controller_name=).with(File.basename($0))
|
58
|
+
|
59
|
+
Exceptional.should_receive(:parse, :with => [exception]).and_return(exception_data)
|
60
|
+
Exceptional.should_receive(:post, :with => [exception_data])
|
61
|
+
|
62
|
+
Exceptional.catch(exception)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should raise a license exception if api key is not set" do
|
66
|
+
exception_data = mock(Exceptional::ExceptionData,
|
67
|
+
:message => "Something bad has happened",
|
68
|
+
:backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"],
|
69
|
+
:class => Exception,
|
70
|
+
:to_hash => { :message => "Something bad has happened" })
|
71
|
+
Exceptional.api_key.should == nil
|
72
|
+
lambda { Exceptional.post(exception_data) }.should raise_error(Exceptional::Config::ConfigurationException)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "rescue" do
|
77
|
+
|
78
|
+
it "should send exception data onto catch" do
|
79
|
+
Exceptional.should_receive(:catch)
|
80
|
+
lambda{ Exceptional.rescue do
|
81
|
+
raise IOError
|
82
|
+
end}.should raise_error(IOError)
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "handle" do
|
88
|
+
before(:each) do
|
89
|
+
Exceptional.stub!(:to_stderr) # Don't print error when testing
|
90
|
+
Exceptional.stub!(:log!) # Don't even attempt to log
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should send exception data onto post" do
|
94
|
+
exception = mock(Exception, :message => "Something bad has happened",
|
95
|
+
:backtrace => "/app/controllers/buggy_controller.rb:29:in `index'")
|
96
|
+
|
97
|
+
controller = mock("controller", :controller_name => "Test Controller Name", :action_name => "Test Action Name")
|
98
|
+
|
99
|
+
class SessionHelper
|
100
|
+
def initialize
|
101
|
+
@some_var = 1
|
102
|
+
@cgi_var = 2
|
103
|
+
@x_db = 3
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
request = mock("request", :env => {"key1" => "val1"}, :protocol => "http", :host => "getexceptional.com", :request_uri => "/path/to/resource", :session => SessionHelper.new)
|
108
|
+
|
109
|
+
Exceptional.should_receive(:post_exception).once
|
110
|
+
Exceptional.handle(exception, controller, request, {:clients => {:name => "bar"}})
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "with helper methods" do
|
115
|
+
|
116
|
+
it "safe_environment() should delete all rack related stuff from environment" do
|
117
|
+
request = mock(request, :env => { 'rack_var' => 'value', 'non_ack' => 'value2' })
|
118
|
+
Exceptional.send(:safe_environment, request).should == { 'non_ack' => 'value2' }
|
119
|
+
end
|
120
|
+
|
121
|
+
it "safe_environment() should handle array type parameters" do
|
122
|
+
|
123
|
+
request = mock(request, :env => {
|
124
|
+
'string_array_var' => ['value', 'another value'],
|
125
|
+
'bool_array_var' => [false, false, true],
|
126
|
+
'numb_array_var' => [3,2,1],
|
127
|
+
'nil_array_var' => [nil, nil],
|
128
|
+
'non_ack' => 'value2' }
|
129
|
+
)
|
130
|
+
Exceptional.send(:safe_environment, request).should == {
|
131
|
+
'string_array_var' => ['value', 'another value'],
|
132
|
+
'bool_array_var' => [false, false, true],
|
133
|
+
'numb_array_var' => [3,2,1],
|
134
|
+
'nil_array_var' => [nil, nil],
|
135
|
+
'non_ack' => 'value2' }
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
it "safe_session() should handle array type parameters" do
|
140
|
+
mock_session = mock("session")
|
141
|
+
mock_session.should_receive(:instance_variables).and_return(['var1', 'var2'])
|
142
|
+
mock_session.should_receive(:instance_variable_get).with('var1').and_return(['value', 'another value'])
|
143
|
+
mock_session.should_receive(:instance_variable_get).with('var2').and_return('value2')
|
144
|
+
Exceptional.send(:safe_session, mock_session).should == { 'var1' => ['value', 'another value'], 'var2' => 'value2' }
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
it "safe_session() should filter all /db/, /cgi/ variables and sub @ for blank" do
|
149
|
+
class SessionHelper
|
150
|
+
def initialize
|
151
|
+
@some_var = 1
|
152
|
+
@cgi_var = 2
|
153
|
+
@x_db = 3
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
session = SessionHelper.new
|
158
|
+
Exceptional.send(:safe_session, session).should == { 'some_var' => 1 }
|
159
|
+
end
|
160
|
+
|
161
|
+
it "sanitize_hash() should sanitize cyclic problem for to_json" do
|
162
|
+
class MyClass
|
163
|
+
def initialize
|
164
|
+
@test = self
|
165
|
+
end
|
166
|
+
|
167
|
+
def to_hash
|
168
|
+
{ :test => self }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
my_class = MyClass.new
|
172
|
+
|
173
|
+
lambda { my_class.to_json }.should raise_error(ActiveSupport::JSON::CircularReferenceError)
|
174
|
+
Exceptional.send(:sanitize_hash, my_class.to_hash).to_json.should == "{}"
|
175
|
+
end
|
176
|
+
|
177
|
+
it "sanitize_hash() should sanitize cyclic problem for to_json passing hash" do
|
178
|
+
class MyClass
|
179
|
+
def initialize
|
180
|
+
@test = self
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_hash
|
184
|
+
{ :test => self }
|
185
|
+
end
|
186
|
+
end
|
187
|
+
my_class = MyClass.new
|
188
|
+
|
189
|
+
|
190
|
+
lambda { my_class.to_json }.should raise_error(ActiveSupport::JSON::CircularReferenceError)
|
191
|
+
Exceptional.send(:sanitize_hash, {'hkey' => my_class}).to_json.should == "{}"
|
192
|
+
end
|
193
|
+
|
194
|
+
it "sanitize_hash() should sanitize cyclic problem for to_json passing hash mult params" do
|
195
|
+
class MyClass
|
196
|
+
def initialize
|
197
|
+
@test = self
|
198
|
+
end
|
199
|
+
|
200
|
+
def to_hash
|
201
|
+
{ :test => self }
|
202
|
+
end
|
203
|
+
end
|
204
|
+
my_class = MyClass.new
|
205
|
+
|
206
|
+
|
207
|
+
lambda { my_class.to_json }.should raise_error(ActiveSupport::JSON::CircularReferenceError)
|
208
|
+
Exceptional.send(:sanitize_hash, {'hkey' => my_class, 'ruby' => 'tuesday'}).to_json.should == "{\"ruby\": \"tuesday\"}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|