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