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,6 @@
1
+ 0.0.6 - Added a better 'safe' to_json method which works more effectively for rails integration avoiding cyclic reference and un-jsonable data
2
+ 0.0.5 - Fixed a bug in the plugin where request.env was being modified directly. dup to solve issues.
3
+ 0.0.4 - Restructured the plugin to extract all different concerns into self contained modules, will give a better base to extend. Also plays nicer with Rdoc + singleton classes
4
+ 0.0.3 - Dropped background queing of exceptions, send them all directly. Fixed issues with session/environment data. Working with rails 2.3 (some hacks needed to get there, need to refactor.)
5
+ 0.0.2 - Fix a bug introduced with nil sessions
6
+ 0.0.1 - First Version
data/Manifest CHANGED
@@ -3,18 +3,24 @@ exceptional.yml
3
3
  History.txt
4
4
  init.rb
5
5
  install.rb
6
- lib/exceptional/agent/worker.rb
7
- lib/exceptional/deployed_environment.rb
6
+ lib/exceptional/api.rb
7
+ lib/exceptional/bootstrap.rb
8
+ lib/exceptional/config.rb
8
9
  lib/exceptional/exception_data.rb
9
10
  lib/exceptional/integration/rails.rb
10
- lib/exceptional/rails.rb
11
+ lib/exceptional/log.rb
12
+ lib/exceptional/remote.rb
11
13
  lib/exceptional/version.rb
12
14
  lib/exceptional.rb
13
15
  Manifest
14
16
  Rakefile
15
17
  README
16
- spec/deployed_environment_spec.rb
18
+ spec/api_spec.rb
19
+ spec/bootstrap_spec.rb
20
+ spec/config_spec.rb
17
21
  spec/exception_data_spec.rb
22
+ spec/exceptional_rescue_from_spec.rb
18
23
  spec/exceptional_spec.rb
24
+ spec/log_spec.rb
25
+ spec/remote_spec.rb
19
26
  spec/spec_helper.rb
20
- spec/worker_spec.rb
data/README CHANGED
@@ -4,10 +4,10 @@ This plugin posts exception data to Exceptional (http://getexceptional.com). Dat
4
4
 
5
5
  = Exceptional
6
6
 
7
- Exceptional (http://getexceptional.com) is a web-based exception tracking and managing system for Ruby on Rails folks and their apps. It's currently in early, private beta.
7
+ Exceptional (http://getexceptional.com) is a web-based exception tracking and managing system for Ruby on Rails folks and their apps.
8
8
 
9
9
  = Installation
10
10
 
11
- For installation instructions, follow the steps displayed after adding a new app to your account. Please send any questions or comments to hello@getexceptional.com.
11
+ For installation instructions, follow the steps displayed after adding a new app to your account. Please send any questions or comments to feedback@getexceptional.com.
12
12
 
13
- Copyright © 2008 Contrast.
13
+ Copyright © 2008, 2009 Contrast.
data/Rakefile CHANGED
@@ -1,13 +1,13 @@
1
1
  begin
2
2
  require 'echoe'
3
3
 
4
- Echoe.new('exceptional', '0.0.1') do |p|
4
+ Echoe.new('exceptional', '0.0.6') do |p|
5
5
  p.rubyforge_name = 'exceptional'
6
6
  p.summary = "Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)"
7
7
  p.description = "Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)"
8
8
  p.url = "http://getexceptional.com/"
9
- p.author = ['David Rice']
10
- p.email = "david@contrast.ie"
9
+ p.author = ["Contrast"]
10
+ p.email = "hello@contrast.ie"
11
11
  p.dependencies = ["json"]
12
12
  end
13
13
 
@@ -24,17 +24,16 @@ begin
24
24
  t.spec_files = FileList['spec/**/*_spec.rb']
25
25
  t.spec_opts = ['--color']
26
26
  end
27
-
27
+
28
28
  task :test do
29
29
  Rake::Task['spec'].invoke
30
30
  end
31
-
31
+
32
32
  Spec::Rake::SpecTask.new("coverage") do |t|
33
33
  t.spec_files = FileList['spec/**/*_spec.rb']
34
34
  t.spec_opts = ['--color']
35
35
  t.rcov = true
36
36
  t.rcov_opts = ['--exclude', '^spec,/gems/']
37
37
  end
38
-
39
38
 
40
39
  end
@@ -1,34 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  Gem::Specification.new do |s|
2
- s.name = "exceptional"
3
- s.version = "0.0.1"
4
- s.date = "2008-10-13"
5
- s.summary = "Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)"
6
- s.email = "david@getexceptional.com"
7
- s.homepage = "http://github.com/contrast/exceptional"
8
- s.description = "Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)"
4
+ s.name = %q{exceptional}
5
+ s.version = "0.0.6"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Contrast"]
9
+ s.date = %q{2009-07-09}
10
+ s.description = %q{Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)}
11
+ s.email = %q{hello@contrast.ie}
12
+ s.extra_rdoc_files = ["lib/exceptional/api.rb", "lib/exceptional/bootstrap.rb", "lib/exceptional/config.rb", "lib/exceptional/exception_data.rb", "lib/exceptional/integration/rails.rb", "lib/exceptional/log.rb", "lib/exceptional/remote.rb", "lib/exceptional/version.rb", "lib/exceptional.rb", "README"]
13
+ s.files = ["exceptional.gemspec", "exceptional.yml", "History.txt", "init.rb", "install.rb", "lib/exceptional/api.rb", "lib/exceptional/bootstrap.rb", "lib/exceptional/config.rb", "lib/exceptional/exception_data.rb", "lib/exceptional/integration/rails.rb", "lib/exceptional/log.rb", "lib/exceptional/remote.rb", "lib/exceptional/version.rb", "lib/exceptional.rb", "Manifest", "Rakefile", "README", "spec/api_spec.rb", "spec/bootstrap_spec.rb", "spec/config_spec.rb", "spec/exception_data_spec.rb", "spec/exceptional_rescue_from_spec.rb", "spec/exceptional_spec.rb", "spec/log_spec.rb", "spec/remote_spec.rb", "spec/spec_helper.rb"]
9
14
  s.has_rdoc = true
10
- s.authors = ["David Rice", "Paul Campbell"]
11
- s.files = ["History.txt",
12
- "Manifest",
13
- "README",
14
- "Rakefile",
15
- "exceptional.gemspec",
16
- "exceptional.yml",
17
- "init.rb",
18
- "install.rb",
19
- "lib/exceptional/agent/worker.rb",
20
- "lib/exceptional/deployed_environment.rb",
21
- "lib/exceptional/exception_data.rb",
22
- "lib/exceptional/integration/rails.rb",
23
- "lib/exceptional/rails.rb",
24
- "lib/exceptional/version.rb",
25
- "lib/exceptional.rb"]
26
- s.test_files = ["spec/deployed_environment_spec.rb",
27
- "spec/exception_data_spec.rb",
28
- "spec/exceptional_spec.rb",
29
- "spec/spec_helper.rb",
30
- "spec/worker_spec.rb"]
31
- s.rdoc_options = ["--main", "README"]
32
- s.extra_rdoc_files = ["History.txt", "Manifest", "README"]
33
- s.add_dependency("json", ["> 0.0.0"])
34
- end
15
+ s.homepage = %q{http://getexceptional.com/}
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Exceptional", "--main", "README"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{exceptional}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)}
21
+
22
+ if s.respond_to? :specification_version then
23
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
24
+ s.specification_version = 2
25
+
26
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
27
+ s.add_runtime_dependency(%q<json>, [">= 0"])
28
+ else
29
+ s.add_dependency(%q<json>, [">= 0"])
30
+ end
31
+ else
32
+ s.add_dependency(%q<json>, [">= 0"])
33
+ end
34
+ end
data/init.rb CHANGED
@@ -1,20 +1,2 @@
1
1
  require 'exceptional'
2
-
3
- def to_stderr(s)
4
- STDERR.puts "** [Exceptional] " + s
5
- end
6
-
7
- config_file = File.join(RAILS_ROOT,"/config/exceptional.yml")
8
-
9
- begin
10
- Exceptional.application_root = RAILS_ROOT
11
- Exceptional.environment = RAILS_ENV
12
-
13
- Exceptional.load_config(config_file)
14
- if Exceptional.enabled?
15
- Exceptional::Rails.init
16
- end
17
- rescue Exception => e
18
- to_stderr e
19
- to_stderr "Plugin disabled."
20
- end
2
+ Exceptional.bootstrap(RAILS_ENV, RAILS_ROOT)
@@ -1,231 +1,20 @@
1
1
  $:.unshift File.dirname(__FILE__)
2
- require 'exceptional/rails'
3
- require 'exceptional/deployed_environment'
4
- require 'exceptional/agent/worker'
2
+
5
3
  require 'exceptional/exception_data'
6
4
  require 'exceptional/version'
7
- require 'rubygems'
8
-
9
- require 'zlib'
10
- require 'cgi'
11
- require 'net/http'
12
- require 'logger'
13
- require 'yaml'
14
- require 'json' unless defined? Rails
15
- # Hack to force Rails version prior to 2.0 to use quoted JSON as per the JSON standard... (TODO: could be cleaner!)
16
- ActiveSupport::JSON.unquote_hash_key_identifiers = false if (defined?(ActiveSupport::JSON) && ActiveSupport::JSON.respond_to?(:unquote_hash_key_identifiers))
5
+ require 'exceptional/log'
6
+ require 'exceptional/config'
7
+ require 'exceptional/remote'
8
+ require 'exceptional/api'
9
+ require 'exceptional/bootstrap'
17
10
 
18
11
  module Exceptional
19
- class LicenseException < StandardError; end
20
- class ConfigurationException < StandardError; end
21
-
22
- ::PROTOCOL_VERSION = 3
23
- # Defaults for configuration variables
24
- ::REMOTE_HOST = "getexceptional.com"
25
- ::REMOTE_PORT = 80
26
- ::REMOTE_SSL_PORT = 443
27
- ::SSL = false
28
- ::LOG_LEVEL = 'info'
29
- ::LOG_PATH = nil
30
- ::WORKER_TIMEOUT = 10 # seconds
31
- ::MODE = :direct
32
-
33
- class << self
34
- attr_accessor :api_key, :log, :deployed_environment, :log_path, :worker,
35
- :worker_thread, :environment, :application_root
36
- attr_writer :remote_host, :remote_port, :ssl_enabled, :log_level
37
-
38
- # rescue any exceptions within the given block,
39
- # send it to exceptional,
40
- # then raise
41
- def rescue(&block)
42
- begin
43
- block.call
44
- rescue Exception => e
45
- self.catch(e)
46
- raise(e)
47
- end
48
- end
49
-
50
- # parse an exception into an ExceptionData object
51
- def parse(exception)
52
- exception_data = ExceptionData.new
53
- exception_data.exception_backtrace = exception.backtrace
54
- exception_data.exception_message = exception.message
55
- exception_data.exception_class = exception.class.to_s
56
- exception_data
57
- end
58
-
59
- # authenticate with getexceptional.com
60
- # returns true if the configured api_key is registered and can send data
61
- # otherwise false
62
- def authenticate
63
- begin
64
- # TODO No data required to authenticate, send a nil string? hacky
65
- # TODO should retry if a http connection failed
66
- return @authenticated if @authenticated
67
- authenticated = call_remote(:authenticate, "")
68
- @authenticated = authenticated =~ /true/
69
- rescue
70
- @authenticated = false
71
- ensure
72
- return @authenticated
73
- end
74
- end
75
-
76
- # post the given exception data to getexceptional.com
77
- def post(exception_data)
78
- call_remote(:errors, exception_data.to_json)
79
- end
80
-
81
- # given a regular ruby Exception class, will parse into an ExceptionData
82
- # object and post to getexceptional.com
83
- def catch(exception)
84
- exception_data = parse(exception)
85
- exception_data.controller_name = File.basename($0)
86
- post(exception_data)
87
- end
88
-
89
- # used with Rails, takes an exception, controller, request and parameters
90
- # creates an ExceptionData object
91
- # if Exceptional is running in :direct mode, will post to getexceptional.com
92
- # if Exceptional is running in :queue mode, the data will be queued and posted later
93
- def handle(exception, controller, request, params)
94
- log! "Handling #{exception.message}", 'info'
95
- e = parse(exception)
96
- # Additional data for Rails Exceptions
97
- e.framework = "rails"
98
- e.controller_name = controller.controller_name
99
- e.action_name = controller.action_name
100
- e.application_root = self.application_root
101
- e.occurred_at = Time.now.strftime("%Y%m%d %H:%M:%S %Z")
102
- e.environment = request.env.to_hash
103
- e.url = "#{request.protocol}#{request.host}#{request.request_uri}"
104
- # Need to remove rack data from environment hash
105
- safe_environment = request.env.to_hash
106
- safe_environment.delete_if { |k,v| k =~ /rack/ }
107
- e.environment = safe_environment
108
-
109
- safe_session = {}
110
- request.session.instance_variables.each do |v|
111
- next if v =~ /cgi/
112
- next if v =~ /db/
113
- # remove prepended @'s
114
- var = v.sub("@","")
115
- safe_session[var] = request.session.instance_variable_get(v)
116
- end
117
-
118
- e.session = safe_session
119
- e.parameters = params.to_hash
120
12
 
121
- if mode == :queue
122
- worker.add_exception(e)
123
- else # :direct mode
124
- begin
125
- post e
126
- rescue Exception => exception
127
- log! "Error posting data to Exceptional."
128
- log! exception.message
129
- log! exception.backtace.join("\n"), 'debug'
130
- end
131
- end
132
- end
133
-
134
- # TODO these configuration methods & defaults should have their own class
135
- def remote_host
136
- @remote_host || ::REMOTE_HOST
137
- end
138
-
139
- def remote_port
140
- @remote_port || default_port
141
- end
142
-
143
- def log_level
144
- @log_level || ::LOG_LEVEL
145
- end
146
-
147
- def mode
148
- deployed_environment ? deployed_environment.determine_mode : ::MODE
149
- end
150
-
151
- def default_port
152
- ssl_enabled? ? ::REMOTE_SSL_PORT : ::REMOTE_PORT
153
- end
154
-
155
- def ssl_enabled?
156
- @ssl_enabled || ::SSL
157
- end
158
-
159
- def enabled?
160
- @enabled
161
- end
162
-
163
- def log!(msg, level = 'info')
164
- to_stderr msg
165
- log.send level, msg if log
166
- end
167
-
168
- def to_stderr(msg)
169
- if deployed_environment && deployed_environment.server != :unknown
170
- STDERR.puts "** [Exceptional] " + msg
171
- end
172
- end
173
-
174
- def log_config_info
175
- log! "API Key: #{api_key}", 'debug'
176
- log! "Deployed Environment: #{deployed_environment.to_s}", 'debug'
177
- log! "Remote Host: #{remote_host}:#{remote_port}", 'debug'
178
- log! "Mode: #{mode}", 'debug'
179
- log! "Log level: #{log_level}", 'debug'
180
- log! "Log path: #{log_path}", 'debug'
181
- end
182
-
183
- def load_config(file)
184
- begin
185
- config = YAML::load(File.open(file))[self.environment]
186
- @api_key = config['api-key'] unless config['api-key'].nil?
187
- @ssl_enabled = config['ssl'] unless config['ssl'].nil?
188
- @log_level = config['log-level'] unless config['log-level'].nil?
189
- @enabled = config['enabled'] unless config['enabled'].nil?
190
- @remote_port = config['remote-port'].to_i unless config['remote-port'].nil?
191
- @remote_host = config['remote-host'] unless config['remote-host'].nil?
192
- rescue Exception => e
193
- raise ConfigurationException.new("Unable to load configuration file:#{file} for environment:#{environment}")
194
- end
195
- end
196
-
197
- protected
198
-
199
- def valid_api_key?
200
- @api_key && @api_key.length == 40
201
- end
202
-
203
-
204
- def call_remote(method, data)
205
- if @api_key.nil?
206
- raise LicenseException.new("API Key must be configured")
207
- end
208
-
209
- http = Net::HTTP.new(remote_host, remote_port)
210
- uri = "/#{method.to_s}?&api_key=#{@api_key}&protocol_version=#{::PROTOCOL_VERSION}"
211
- headers = { 'Content-Type' => 'application/x-gzip', 'Accept' => 'application/x-gzip' }
212
- compressed_data = CGI::escape(Zlib::Deflate.deflate(data, Zlib::BEST_SPEED))
213
- response = http.start do |http|
214
- http.post(uri, compressed_data, headers)
215
- end
216
-
217
- if response.kind_of? Net::HTTPSuccess
218
- return response.body
219
- else
220
- raise Exception.new("#{response.code}: #{response.message}")
221
- end
222
-
223
- rescue Exception => e
224
- log! "Error contacting Exceptional: #{e}", 'info'
225
- log! e.backtrace.join("\n"), 'debug'
226
- raise e
227
- end
228
-
13
+ class << self
14
+ include Exceptional::Config
15
+ include Exceptional::Api
16
+ include Exceptional::Remote
17
+ include Exceptional::Log
18
+ include Exceptional::Bootstrap
229
19
  end
230
-
231
20
  end
@@ -0,0 +1,108 @@
1
+ require 'json' unless defined? Rails
2
+
3
+
4
+ module Exceptional
5
+
6
+ module Api
7
+ # parse an exception into an ExceptionData object
8
+ def parse(exception)
9
+ exception_data = ExceptionData.new
10
+ exception_data.exception_backtrace = exception.backtrace
11
+ exception_data.exception_message = exception.message
12
+ exception_data.exception_class = exception.class.to_s
13
+ exception_data
14
+ end
15
+
16
+ # post the given exception data to getexceptional.com
17
+ def post(exception_data)
18
+ hash = exception_data.to_hash
19
+ if hash[:session]
20
+ hash[:session].delete("initialization_options")
21
+ hash[:session].delete("request")
22
+ end
23
+
24
+ Exceptional.post_exception(hash.to_json)
25
+ end
26
+
27
+ # used with Rails, takes an exception, controller, request and parameters
28
+ # creates an ExceptionData object
29
+ def handle(exception, controller, request, params)
30
+ Exceptional.log! "Handling #{exception.message}", 'info'
31
+ begin
32
+ e = parse(exception)
33
+ # Additional data for Rails Exceptions
34
+ e.framework = "rails"
35
+ e.controller_name = controller.controller_name
36
+ e.action_name = controller.action_name
37
+ e.application_root = Exceptional.application_root
38
+ e.occurred_at = Time.now.strftime("%Y%m%d %H:%M:%S %Z")
39
+ e.environment = request.env.to_hash
40
+ e.url = "#{request.protocol}#{request.host}#{request.request_uri}"
41
+ e.environment = safe_environment(request)
42
+ e.session = safe_session(request.session)
43
+ e.parameters = sanitize_hash(params.to_hash)
44
+
45
+ post(e)
46
+ rescue Exception => exception
47
+ Exceptional.log! "Error preparing exception data."
48
+ Exceptional.log! exception.message
49
+ Exceptional.log! exception.backtrace.join("\n"), 'debug'
50
+ end
51
+ end
52
+
53
+ # rescue any exceptions within the given block,
54
+ # send it to exceptional,
55
+ # then raise
56
+ def rescue(&block)
57
+ begin
58
+ block.call
59
+ rescue Exception => e
60
+ self.catch(e)
61
+ raise(e)
62
+ end
63
+ end
64
+
65
+ def catch(exception)
66
+ exception_data = parse(exception)
67
+ exception_data.controller_name = File.basename($0)
68
+ post(exception_data)
69
+ end
70
+
71
+ protected
72
+
73
+ def safe_environment(request)
74
+ safe_environment = request.env.dup.to_hash
75
+ # From Rails 2.3 these objects that cause a circular reference error on .to_json need removed
76
+ # TODO potentially remove this case, should be covered by sanitize_hash
77
+ safe_environment.delete_if { |k,v| k =~ /rack/ || k =~ /action_controller/ || k == "_"}
78
+ # needed to add a filter for the hash for "_", causing invalid xml.
79
+ sanitize_hash(safe_environment)
80
+ end
81
+
82
+ def safe_session(session)
83
+ result = {}
84
+ session.instance_variables.each do |v|
85
+ next if v =~ /cgi/ || v =~ /db/ || v =~ /env/
86
+ var = v.sub("@","") # remove prepended @'s
87
+ result[var] = session.instance_variable_get(v)
88
+ end
89
+ sanitize_hash(result)
90
+ end
91
+
92
+ private
93
+
94
+ def sanitize_hash(hash)
95
+ return {} if hash.nil?
96
+ hash.reject { |key, val| !ensure_json_able(val) }
97
+ end
98
+
99
+ def ensure_json_able(value)
100
+ begin
101
+ value.to_json
102
+ true && value.instance_values.all? { |e| ensure_json_able(e)}
103
+ rescue Exception => e
104
+ false
105
+ end
106
+ end
107
+ end
108
+ end