contrast-exceptional 0.0.1 → 0.0.6
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/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
|