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.
@@ -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
- class Base
5
- def rescue_action_with_exceptional(exception)
6
-
7
- params_to_send = (respond_to? :filter_parameters) ? filter_parameters(params) : params
8
-
9
- Exceptional.handle(exception, self, request, params_to_send)
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
- rescue_action_without_exceptional exception
12
- end
23
+ rescue_action_without_exceptional exception
24
+ end
13
25
 
14
- alias_method :rescue_action_without_exceptional, :rescue_action
15
- alias_method :rescue_action, :rescue_action_with_exceptional
16
- protected :rescue_action
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
@@ -2,7 +2,7 @@ module Exceptional #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
4
  MINOR = 0
5
- TINY = 1
5
+ TINY = 6
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -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