davidjrice-exceptional 0.0.1
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 +0 -0
- data/Manifest +20 -0
- data/README +13 -0
- data/Rakefile +40 -0
- data/exceptional.gemspec +34 -0
- data/exceptional.yml +34 -0
- data/init.rb +20 -0
- data/install.rb +18 -0
- data/lib/exceptional/agent/worker.rb +56 -0
- data/lib/exceptional/deployed_environment.rb +84 -0
- data/lib/exceptional/exception_data.rb +46 -0
- data/lib/exceptional/integration/rails.rb +20 -0
- data/lib/exceptional/rails.rb +53 -0
- data/lib/exceptional/version.rb +9 -0
- data/lib/exceptional.rb +227 -0
- data/spec/deployed_environment_spec.rb +168 -0
- data/spec/exception_data_spec.rb +34 -0
- data/spec/exceptional_spec.rb +67 -0
- data/spec/spec_helper.rb +2 -0
- metadata +83 -0
data/History.txt
ADDED
File without changes
|
data/Manifest
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
exceptional.gemspec
|
2
|
+
exceptional.yml
|
3
|
+
History.txt
|
4
|
+
init.rb
|
5
|
+
install.rb
|
6
|
+
lib/exceptional/agent/worker.rb
|
7
|
+
lib/exceptional/deployed_environment.rb
|
8
|
+
lib/exceptional/exception_data.rb
|
9
|
+
lib/exceptional/integration/rails.rb
|
10
|
+
lib/exceptional/rails.rb
|
11
|
+
lib/exceptional/version.rb
|
12
|
+
lib/exceptional.rb
|
13
|
+
Manifest
|
14
|
+
Rakefile
|
15
|
+
README
|
16
|
+
spec/deployed_environment_spec.rb
|
17
|
+
spec/exception_data_spec.rb
|
18
|
+
spec/exceptional_spec.rb
|
19
|
+
spec/spec_helper.rb
|
20
|
+
spec/worker_spec.rb
|
data/README
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
= Exceptional plugin for Ruby on Rails
|
2
|
+
|
3
|
+
This plugin posts exception data to Exceptional (http://getexceptional.com). Data about the request, session, environment and a backtrace of the exception is transmitted.
|
4
|
+
|
5
|
+
= Exceptional
|
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.
|
8
|
+
|
9
|
+
= Installation
|
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.
|
12
|
+
|
13
|
+
Copyright © 2008 Contrast.
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
begin
|
2
|
+
require 'echoe'
|
3
|
+
|
4
|
+
Echoe.new('exceptional', '0.0.1') do |p|
|
5
|
+
p.rubyforge_name = 'exceptional'
|
6
|
+
p.summary = "Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)"
|
7
|
+
p.description = "Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)"
|
8
|
+
p.url = "http://getexceptional.com/"
|
9
|
+
p.author = ['David Rice']
|
10
|
+
p.email = "david@contrast.ie"
|
11
|
+
p.dependencies = ["json"]
|
12
|
+
end
|
13
|
+
|
14
|
+
rescue LoadError => e
|
15
|
+
puts "You are missing a dependency required for meta-operations on this gem."
|
16
|
+
puts "#{e.to_s.capitalize}."
|
17
|
+
end
|
18
|
+
|
19
|
+
# add spec tasks, if you have rspec installed
|
20
|
+
begin
|
21
|
+
require 'spec/rake/spectask'
|
22
|
+
|
23
|
+
Spec::Rake::SpecTask.new("spec") do |t|
|
24
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
25
|
+
t.spec_opts = ['--color']
|
26
|
+
end
|
27
|
+
|
28
|
+
task :test do
|
29
|
+
Rake::Task['spec'].invoke
|
30
|
+
end
|
31
|
+
|
32
|
+
Spec::Rake::SpecTask.new("coverage") do |t|
|
33
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
34
|
+
t.spec_opts = ['--color']
|
35
|
+
t.rcov = true
|
36
|
+
t.rcov_opts = ['--exclude', '^spec,/gems/']
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
end
|
data/exceptional.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
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)"
|
9
|
+
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.rbrb"]
|
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
|
data/exceptional.yml
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# here are the settings that are common to all environments
|
2
|
+
common: &default_settings
|
3
|
+
# You must specify your Exceptional API key here.
|
4
|
+
api-key: PASTE_YOUR_API_KEY_HERE
|
5
|
+
# Exceptional creates a separate log file from your application's logs
|
6
|
+
# available levels are debug, info, warn, error, fatal
|
7
|
+
log-level: info
|
8
|
+
# The exceptional agent sends data via regular http by default
|
9
|
+
# Setting this value to true will send data over SSL, increasing security
|
10
|
+
# There will be an additional CPU overhead in encrypting the data, however
|
11
|
+
# as long as your deployment environment is not Passenger (mod_rails), this
|
12
|
+
# happens in the background so as not to incur a page wait for your users.
|
13
|
+
ssl: false
|
14
|
+
|
15
|
+
development:
|
16
|
+
<<: *default_settings
|
17
|
+
# Normally no reason to collect exceptions in development
|
18
|
+
# NOTE: for trial purposes you may want to enable exceptional in development
|
19
|
+
enabled: false
|
20
|
+
|
21
|
+
test:
|
22
|
+
<<: *default_settings
|
23
|
+
# No reason to collect exceptions when running tests by default
|
24
|
+
enabled: false
|
25
|
+
|
26
|
+
production:
|
27
|
+
<<: *default_settings
|
28
|
+
enabled: true
|
29
|
+
|
30
|
+
staging:
|
31
|
+
# It's common development practice to have a staging environment that closely
|
32
|
+
# mirrors production, by default catch errors in this environment too.
|
33
|
+
<<: *default_settings
|
34
|
+
enabled: true
|
data/init.rb
ADDED
@@ -0,0 +1,20 @@
|
|
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
|
data/install.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# This is the post install hook for when Exceptional is installed as a plugin.
|
2
|
+
require 'ftools'
|
3
|
+
|
4
|
+
# puts IO.read(File.join(File.dirname(__FILE__), 'README'))
|
5
|
+
|
6
|
+
config_file = File.expand_path("#{File.dirname(__FILE__)}/../../../config/exceptional.yml")
|
7
|
+
example_config_file = "#{File.dirname(__FILE__)}/exceptional.yml"
|
8
|
+
|
9
|
+
if File::exists? config_file
|
10
|
+
puts "Exceptional config file already exists. Please ensure it is up-to-date with the current format."
|
11
|
+
puts "See #{example_config_file}"
|
12
|
+
else
|
13
|
+
puts "Installing default Exceptional config"
|
14
|
+
puts " From #{example_config_file}"
|
15
|
+
puts "For exceptional to work you need to configure your API Key"
|
16
|
+
puts " See #{example_config_file}"
|
17
|
+
puts "If you don't have an API Key, get one at http://getexceptional.com/"
|
18
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# The Worker is used by Exceptional when started in :queue mode
|
2
|
+
# It queues and sends exception data asynchronously.
|
3
|
+
|
4
|
+
module Exceptional::Agent
|
5
|
+
|
6
|
+
class Worker
|
7
|
+
|
8
|
+
attr_reader :log, :timeout, :exceptions
|
9
|
+
|
10
|
+
def initialize(log = Logger.new(STDERR))
|
11
|
+
@exceptions = []
|
12
|
+
@timeout = ::WORKER_TIMEOUT
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@log = log
|
15
|
+
@log.info "Started Exceptional Worker."
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_exception(data)
|
19
|
+
@mutex.synchronize do
|
20
|
+
@exceptions << data
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
while(true) do
|
26
|
+
send_any_exception_data
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def exception_to_send
|
33
|
+
@mutex.synchronize do
|
34
|
+
return @exceptions.shift
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
def send_any_exception_data
|
40
|
+
if @exceptions.empty?
|
41
|
+
sleep @timeout
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
data = exception_to_send
|
46
|
+
|
47
|
+
begin
|
48
|
+
Exceptional.post(data)
|
49
|
+
rescue Exception => e
|
50
|
+
@log.error "Error sending exception data: #{e}"
|
51
|
+
@log.debug e.backtrace.join("\n")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# This class is used to determine the server environment
|
2
|
+
module Exceptional
|
3
|
+
class DeployedEnvironment
|
4
|
+
|
5
|
+
attr_reader :server, :identifier, :hostname#, :environment
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@server = :unknown
|
9
|
+
@identifier = nil
|
10
|
+
|
11
|
+
@hostname = determine_host
|
12
|
+
# @environment = Exceptional.environment
|
13
|
+
# @application_root = Exceptional.application_root
|
14
|
+
|
15
|
+
servers = %w[webrick mongrel thin litespeed passenger]
|
16
|
+
while servers.any? && @identifier.nil?
|
17
|
+
send 'is_'+(servers.shift)+'?'
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
def should_start_worker?
|
23
|
+
!identifier.nil?
|
24
|
+
end
|
25
|
+
|
26
|
+
def determine_mode
|
27
|
+
@server == :passenger ? :direct : :queue
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
"#{@hostname}:#{@identifier} [#{@server}]"
|
32
|
+
end
|
33
|
+
|
34
|
+
def determine_host
|
35
|
+
Socket.gethostname
|
36
|
+
end
|
37
|
+
|
38
|
+
def is_webrick?
|
39
|
+
if defined?(OPTIONS) && defined?(DEFAULT_PORT) && OPTIONS.respond_to?(:fetch)
|
40
|
+
# OPTIONS is set by script/server if launching webrick
|
41
|
+
@identifier = OPTIONS.fetch :port, DEFAULT_PORT
|
42
|
+
@server = :webrick
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def is_mongrel?
|
47
|
+
if defined? Mongrel::HttpServer
|
48
|
+
ObjectSpace.each_object(Mongrel::HttpServer) do |mongrel|
|
49
|
+
next unless mongrel.respond_to? :port
|
50
|
+
@server = :mongrel
|
51
|
+
@identifier = mongrel.port
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def is_thin?
|
57
|
+
if defined? Thin::Server
|
58
|
+
ObjectSpace.each_object(Thin::Server) do |thin_server|
|
59
|
+
@server = :thin
|
60
|
+
backend = thin_server.backend
|
61
|
+
if backend.respond_to? :port
|
62
|
+
@identifier = backend.port
|
63
|
+
elsif backend.respond_to? :socket
|
64
|
+
@identifier = backend.socket
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def is_litespeed?
|
71
|
+
if caller.pop =~ /fcgi-bin\/RailsRunner\.rb/
|
72
|
+
@server = :litespeed
|
73
|
+
@identifier = 'litespeed'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def is_passenger?
|
78
|
+
if defined? Passenger::AbstractServer
|
79
|
+
@server = :passenger
|
80
|
+
@identifier = 'passenger'
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# This class encapsulates data about an exception.
|
2
|
+
module Exceptional
|
3
|
+
class DataError < StandardError; end
|
4
|
+
|
5
|
+
class ExceptionData
|
6
|
+
::LANGUAGE = "ruby"
|
7
|
+
::BASE_ATTRS = [:exception_class, :exception_message, :exception_backtrace]
|
8
|
+
::OPTIONAL_ATTRS = [:framework, :controller_name, :action_name, :application_root,
|
9
|
+
:url, :occurred_at, :environment, :session, :parameters]
|
10
|
+
::ACCESSIBLE_ATTRS = ::BASE_ATTRS + ::OPTIONAL_ATTRS
|
11
|
+
::ATTRS = [:language] + ::ACCESSIBLE_ATTRS
|
12
|
+
|
13
|
+
::ACCESSIBLE_ATTRS.each do |attribute|
|
14
|
+
attr_accessor attribute
|
15
|
+
end
|
16
|
+
attr_reader :language
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
environment = {}
|
20
|
+
session = {}
|
21
|
+
parameters = {}
|
22
|
+
@language = ::LANGUAGE
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
::BASE_ATTRS.each do |method|
|
27
|
+
raise(DataError, "base data #{method} not set") if (self.send(method).nil? || self.send(method).empty?)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
hash = {}
|
33
|
+
::ATTRS.each do |attribute|
|
34
|
+
value = send(attribute)
|
35
|
+
hash[attribute] = value unless (value.nil? || value.empty?)
|
36
|
+
end
|
37
|
+
hash
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_json
|
41
|
+
self.to_hash.to_json
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
if defined? ActionController
|
2
|
+
|
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)
|
10
|
+
|
11
|
+
rescue_action_without_exceptional exception
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :rescue_action_without_exceptional, :rescue_action
|
15
|
+
alias_method :rescue_action, :rescue_action_with_exceptional
|
16
|
+
protected :rescue_action
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Exceptional
|
2
|
+
|
3
|
+
class Rails
|
4
|
+
|
5
|
+
def self.init
|
6
|
+
Exceptional.deployed_environment = DeployedEnvironment.new
|
7
|
+
# If no server environment detected, don't perform magic.
|
8
|
+
# Means rake, script/console etc perform as expected
|
9
|
+
return false unless Exceptional.deployed_environment.should_start_worker?
|
10
|
+
|
11
|
+
setup_log
|
12
|
+
Exceptional.log_config_info
|
13
|
+
|
14
|
+
if Exceptional.authenticate
|
15
|
+
|
16
|
+
if Exceptional.mode == :queue
|
17
|
+
Exceptional.worker = Agent::Worker.new(Exceptional.log)
|
18
|
+
Exceptional.worker_thread = Thread.new do
|
19
|
+
Exceptional.worker.run
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require File.join(File.dirname(__FILE__), 'integration', 'rails')
|
24
|
+
|
25
|
+
|
26
|
+
at_exit do
|
27
|
+
if Exceptional.mode == :queue
|
28
|
+
Exceptional.worker_thread.terminate if Exceptional.worker_thread
|
29
|
+
end
|
30
|
+
end
|
31
|
+
else
|
32
|
+
Exceptional.log! "Plugin not authenticated, check your API Key"
|
33
|
+
Exceptional.log! "Disabling Plugin."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.setup_log
|
38
|
+
log_file = "#{Exceptional.application_root}/log/exceptional.log"
|
39
|
+
|
40
|
+
@log = Logger.new log_file
|
41
|
+
@log.level = Logger::INFO
|
42
|
+
|
43
|
+
allowed_log_levels = ['debug', 'info', 'warn', 'error', 'fatal']
|
44
|
+
if Exceptional.log_level && allowed_log_levels.include?(Exceptional.log_level)
|
45
|
+
@log.level = eval("Logger::#{Exceptional.log_level.upcase}")
|
46
|
+
end
|
47
|
+
|
48
|
+
Exceptional.log = @log
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/lib/exceptional.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
require 'exceptional/rails'
|
3
|
+
require 'exceptional/deployed_environment'
|
4
|
+
require 'exceptional/agent/worker'
|
5
|
+
require 'exceptional/exception_data'
|
6
|
+
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
|
+
|
16
|
+
module Exceptional
|
17
|
+
class LicenseException < StandardError; end
|
18
|
+
class ConfigurationException < StandardError; end
|
19
|
+
|
20
|
+
::PROTOCOL_VERSION = 3
|
21
|
+
# Defaults for configuration variables
|
22
|
+
::REMOTE_HOST = "getexceptional.com"
|
23
|
+
::REMOTE_PORT = 80
|
24
|
+
::REMOTE_SSL_PORT = 443
|
25
|
+
::SSL = false
|
26
|
+
::LOG_LEVEL = 'info'
|
27
|
+
::LOG_PATH = nil
|
28
|
+
::WORKER_TIMEOUT = 10 # seconds
|
29
|
+
::MODE = :direct
|
30
|
+
|
31
|
+
class << self
|
32
|
+
attr_accessor :api_key, :log, :deployed_environment, :log_path, :worker,
|
33
|
+
:worker_thread, :environment, :application_root
|
34
|
+
attr_writer :remote_host, :remote_port, :ssl_enabled, :log_level
|
35
|
+
|
36
|
+
# rescue any exceptions within the given block,
|
37
|
+
# send it to exceptional,
|
38
|
+
# then raise
|
39
|
+
def rescue(&block)
|
40
|
+
begin
|
41
|
+
block.call
|
42
|
+
rescue Exception => e
|
43
|
+
self.catch(e)
|
44
|
+
raise(e)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# parse an exception into an ExceptionData object
|
49
|
+
def parse(exception)
|
50
|
+
exception_data = ExceptionData.new
|
51
|
+
exception_data.exception_backtrace = exception.backtrace
|
52
|
+
exception_data.exception_message = exception.message
|
53
|
+
exception_data.exception_class = exception.class.to_s
|
54
|
+
exception_data
|
55
|
+
end
|
56
|
+
|
57
|
+
# authenticate with getexceptional.com
|
58
|
+
# returns true if the configured api_key is registered and can send data
|
59
|
+
# otherwise false
|
60
|
+
def authenticate
|
61
|
+
begin
|
62
|
+
# TODO No data required to authenticate, send a nil string? hacky
|
63
|
+
# TODO should retry if a http connection failed
|
64
|
+
return @authenticated if @authenticated
|
65
|
+
authenticated = call_remote(:authenticate, "")
|
66
|
+
@authenticated = authenticated =~ /true/
|
67
|
+
rescue
|
68
|
+
@authenticated = false
|
69
|
+
ensure
|
70
|
+
return @authenticated
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# post the given exception data to getexceptional.com
|
75
|
+
def post(exception_data)
|
76
|
+
call_remote(:errors, exception_data.to_json)
|
77
|
+
end
|
78
|
+
|
79
|
+
# given a regular ruby Exception class, will parse into an ExceptionData
|
80
|
+
# object and post to getexceptional.com
|
81
|
+
def catch(exception)
|
82
|
+
exception_data = parse(exception)
|
83
|
+
post(exception_data)
|
84
|
+
end
|
85
|
+
|
86
|
+
# used with Rails, takes an exception, controller, request and parameters
|
87
|
+
# creates an ExceptionData object
|
88
|
+
# if Exceptional is running in :direct mode, will post to getexceptional.com
|
89
|
+
# if Exceptional is running in :queue mode, the data will be queued and posted later
|
90
|
+
def handle(exception, controller, request, params)
|
91
|
+
log! "Handling #{exception.message}", 'info'
|
92
|
+
e = parse(exception)
|
93
|
+
# Additional data for Rails Exceptions
|
94
|
+
e.framework = "rails"
|
95
|
+
e.controller_name = controller.controller_name
|
96
|
+
e.action_name = controller.action_name
|
97
|
+
e.application_root = self.application_root
|
98
|
+
e.occurred_at = Time.now.to_s
|
99
|
+
e.url = "#{request.protocol}#{request.host}#{request.request_uri}"
|
100
|
+
# Need to remove rack data from environment hash
|
101
|
+
safe_environment = request.env.to_hash
|
102
|
+
safe_environment.delete_if { |k,v| k =~ /rack/ }
|
103
|
+
e.environment = safe_environment
|
104
|
+
|
105
|
+
safe_session = {}
|
106
|
+
request.session.instance_variables.each do |v|
|
107
|
+
next if v =~ /cgi/
|
108
|
+
next if v =~ /db/
|
109
|
+
# remove prepended @'s
|
110
|
+
var = v.sub("@","")
|
111
|
+
safe_session[var] = request.session.instance_variable_get(v)
|
112
|
+
end
|
113
|
+
|
114
|
+
e.session = safe_session
|
115
|
+
e.parameters = params.to_hash
|
116
|
+
|
117
|
+
if mode == :queue
|
118
|
+
worker.add_exception(e)
|
119
|
+
else # :direct mode
|
120
|
+
begin
|
121
|
+
post e
|
122
|
+
rescue
|
123
|
+
log! "Error posting data to Exceptional."
|
124
|
+
log! e.message
|
125
|
+
log! e.backtace.join("\n"), 'debug'
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# TODO these configuration methods & defaults should have their own class
|
131
|
+
def remote_host
|
132
|
+
@remote_host || ::REMOTE_HOST
|
133
|
+
end
|
134
|
+
|
135
|
+
def remote_port
|
136
|
+
@remote_port || default_port
|
137
|
+
end
|
138
|
+
|
139
|
+
def log_level
|
140
|
+
@log_level || ::LOG_LEVEL
|
141
|
+
end
|
142
|
+
|
143
|
+
def mode
|
144
|
+
deployed_environment ? deployed_environment.determine_mode : ::MODE
|
145
|
+
end
|
146
|
+
|
147
|
+
def default_port
|
148
|
+
ssl_enabled? ? ::REMOTE_SSL_PORT : ::REMOTE_PORT
|
149
|
+
end
|
150
|
+
|
151
|
+
def ssl_enabled?
|
152
|
+
@ssl_enabled || ::SSL
|
153
|
+
end
|
154
|
+
|
155
|
+
def enabled?
|
156
|
+
@enabled
|
157
|
+
end
|
158
|
+
|
159
|
+
def log!(msg, level = 'info')
|
160
|
+
to_stderr msg
|
161
|
+
log.send level, msg if log
|
162
|
+
end
|
163
|
+
|
164
|
+
def to_stderr(msg)
|
165
|
+
if deployed_environment && deployed_environment.server != :unknown
|
166
|
+
STDERR.puts "** [Exceptional] " + msg
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def log_config_info
|
171
|
+
log! "API Key: #{api_key}", 'debug'
|
172
|
+
log! "Deployed Environment: #{deployed_environment.to_s}", 'debug'
|
173
|
+
log! "Remote Host: #{remote_host}:#{remote_port}", 'debug'
|
174
|
+
log! "Mode: #{mode}", 'debug'
|
175
|
+
log! "Log level: #{log_level}", 'debug'
|
176
|
+
log! "Log path: #{log_path}", 'debug'
|
177
|
+
end
|
178
|
+
|
179
|
+
def load_config(file)
|
180
|
+
begin
|
181
|
+
config = YAML::load(File.open(file))[self.environment]
|
182
|
+
@api_key = config['api-key'] unless config['api-key'].nil?
|
183
|
+
@ssl_enabled = config['ssl'] unless config['ssl'].nil?
|
184
|
+
@log_level = config['log-level'] unless config['log-level'].nil?
|
185
|
+
@enabled = config['enabled'] unless config['enabled'].nil?
|
186
|
+
@remote_port = config['remote-port'].to_i unless config['remote-port'].nil?
|
187
|
+
@remote_host = config['remote-host'] unless config['remote-host'].nil?
|
188
|
+
rescue Exception => e
|
189
|
+
raise ConfigurationException.new("Unable to load configuration file:#{file} for environment:#{environment}")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
protected
|
194
|
+
|
195
|
+
def valid_api_key?
|
196
|
+
@api_key && @api_key.length == 40
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
def call_remote(method, data)
|
201
|
+
if @api_key.nil?
|
202
|
+
raise LicenseException.new("API Key must be configured")
|
203
|
+
end
|
204
|
+
|
205
|
+
http = Net::HTTP.new(remote_host, remote_port)
|
206
|
+
uri = "/#{method.to_s}?&api_key=#{@api_key}&protocol_version=#{::PROTOCOL_VERSION}"
|
207
|
+
headers = { 'Content-Type' => 'application/x-gzip', 'Accept' => 'application/x-gzip' }
|
208
|
+
compressed_data = CGI::escape(Zlib::Deflate.deflate(data, Zlib::BEST_SPEED))
|
209
|
+
response = http.start do |http|
|
210
|
+
http.post(uri, compressed_data, headers)
|
211
|
+
end
|
212
|
+
|
213
|
+
if response.kind_of? Net::HTTPSuccess
|
214
|
+
return response.body
|
215
|
+
else
|
216
|
+
raise Exception.new("#{response.code}: #{response.message}")
|
217
|
+
end
|
218
|
+
|
219
|
+
rescue Exception => e
|
220
|
+
log! "Error contacting Exceptional: #{e}", 'info'
|
221
|
+
log! e.backtrace.join("\n"), 'debug'
|
222
|
+
raise e
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Exceptional::DeployedEnvironment do
|
4
|
+
|
5
|
+
describe "mongrel" do
|
6
|
+
|
7
|
+
before(:all) do
|
8
|
+
class << self
|
9
|
+
module ::Mongrel
|
10
|
+
class HttpServer
|
11
|
+
def port; 3000; end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Mongrel::HttpServer.new
|
17
|
+
@deployed_environment = Exceptional::DeployedEnvironment.new
|
18
|
+
end
|
19
|
+
|
20
|
+
it "server should be mongrel" do
|
21
|
+
@deployed_environment.server.should == :mongrel
|
22
|
+
end
|
23
|
+
|
24
|
+
it "identifier should be 3000" do
|
25
|
+
@deployed_environment.identifier.should == 3000
|
26
|
+
end
|
27
|
+
|
28
|
+
it "to_s should match" do
|
29
|
+
pending
|
30
|
+
@deployed_environment.to_s.should match(/(:3000 \[:mongrel\])/)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "mode should be queue" do
|
34
|
+
@deployed_environment.determine_mode.should == :queue
|
35
|
+
end
|
36
|
+
|
37
|
+
after(:all) do
|
38
|
+
ObjectSpace.garbage_collect
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "webrick" do
|
44
|
+
|
45
|
+
before(:all) do
|
46
|
+
class MockOptions
|
47
|
+
def fetch(*args)
|
48
|
+
3001
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
::OPTIONS = MockOptions.new
|
54
|
+
::DEFAULT_PORT = 3000
|
55
|
+
end
|
56
|
+
@deployed_environment = Exceptional::DeployedEnvironment.new
|
57
|
+
end
|
58
|
+
|
59
|
+
it "server should be webrick" do
|
60
|
+
@deployed_environment.server.should == :webrick
|
61
|
+
end
|
62
|
+
|
63
|
+
it "identifier should be 3001" do
|
64
|
+
@deployed_environment.identifier.should == 3001
|
65
|
+
end
|
66
|
+
|
67
|
+
it "mode should be queue" do
|
68
|
+
@deployed_environment.determine_mode.should == :queue
|
69
|
+
end
|
70
|
+
|
71
|
+
after(:all) do
|
72
|
+
ObjectSpace.garbage_collect
|
73
|
+
Object.class_eval { remove_const :OPTIONS }
|
74
|
+
Object.class_eval { remove_const :DEFAULT_PORT }
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "thin" do
|
80
|
+
|
81
|
+
before(:all) do
|
82
|
+
class << self
|
83
|
+
module ::Thin
|
84
|
+
class Server
|
85
|
+
def backend; self; end
|
86
|
+
def socket; "/socket/file.000"; end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
Thin::Server.new
|
91
|
+
|
92
|
+
@deployed_environment = Exceptional::DeployedEnvironment.new
|
93
|
+
end
|
94
|
+
|
95
|
+
it "server should be thin" do
|
96
|
+
@deployed_environment.server.should == :thin
|
97
|
+
end
|
98
|
+
|
99
|
+
it "identifier should be the socket file" do
|
100
|
+
@deployed_environment.identifier.should == '/socket/file.000'
|
101
|
+
end
|
102
|
+
|
103
|
+
it "mode should be queue" do
|
104
|
+
@deployed_environment.determine_mode.should == :queue
|
105
|
+
end
|
106
|
+
|
107
|
+
after(:all) do
|
108
|
+
ObjectSpace.garbage_collect
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "litespeed" do
|
114
|
+
|
115
|
+
before(:all) do
|
116
|
+
@deployed_environment = Exceptional::DeployedEnvironment.new
|
117
|
+
end
|
118
|
+
|
119
|
+
# Hmmph, how to determine if we're running under litespeed...
|
120
|
+
it "server should be unknown" do
|
121
|
+
@deployed_environment.server.should == :unknown
|
122
|
+
end
|
123
|
+
|
124
|
+
it "identifier should be nil" do
|
125
|
+
@deployed_environment.identifier.should == nil
|
126
|
+
end
|
127
|
+
|
128
|
+
it "mode should be queue" do
|
129
|
+
@deployed_environment.determine_mode.should == :queue
|
130
|
+
end
|
131
|
+
|
132
|
+
after(:all) do
|
133
|
+
ObjectSpace.garbage_collect
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
describe "passenger" do
|
139
|
+
|
140
|
+
before(:all) do
|
141
|
+
class << self
|
142
|
+
module ::Passenger
|
143
|
+
const_set "AbstractServer", 0
|
144
|
+
end
|
145
|
+
end
|
146
|
+
@deployed_environment = Exceptional::DeployedEnvironment.new
|
147
|
+
end
|
148
|
+
|
149
|
+
it "server should be passenger" do
|
150
|
+
@deployed_environment.server.should == :passenger
|
151
|
+
end
|
152
|
+
|
153
|
+
# Would be nicer to identify passenger by some
|
154
|
+
it "identifier should be passenger" do
|
155
|
+
@deployed_environment.identifier.should == 'passenger'
|
156
|
+
end
|
157
|
+
|
158
|
+
it "mode should be queue" do
|
159
|
+
@deployed_environment.determine_mode.should == :direct
|
160
|
+
end
|
161
|
+
|
162
|
+
after(:all) do
|
163
|
+
ObjectSpace.garbage_collect
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Exceptional::ExceptionData do
|
4
|
+
|
5
|
+
describe "with valid base data" do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
@exception_data = Exceptional::ExceptionData.new
|
9
|
+
@exception_data.exception_class = "Error"
|
10
|
+
@exception_data.exception_message = "There was an error"
|
11
|
+
@exception_data.exception_backtrace = "/var/www/app/fail.rb:42 Error('There was an error')"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "language should be ruby" do
|
15
|
+
@exception_data.language.should == "ruby"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should be valid" do
|
19
|
+
@exception_data.should be_valid
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should convert to hash" do
|
23
|
+
@exception_data.to_hash.should == {:exception_class => "Error", :exception_message => "There was an error",
|
24
|
+
:exception_backtrace => "/var/www/app/fail.rb:42 Error('There was an error')",
|
25
|
+
:language => "ruby"}
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should convert to json" do
|
29
|
+
@exception_data.to_json.class.should == String
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Exceptional do
|
4
|
+
|
5
|
+
describe "with no configuration" do
|
6
|
+
it "should connect to getexceptional.com by default" do
|
7
|
+
Exceptional.remote_host.should == "getexceptional.com"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should connect to port 80 by default" do
|
11
|
+
Exceptional.remote_port.should == 80
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should parse exception into exception data object" do
|
15
|
+
exception = mock(Exception, :message => "Something bad has happened", :backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"])
|
16
|
+
exception_data = Exceptional.parse(exception)
|
17
|
+
exception_data.kind_of?(Exceptional::ExceptionData).should be_true
|
18
|
+
exception_data.exception_message.should == exception.message
|
19
|
+
exception_data.exception_backtrace.should == exception.backtrace
|
20
|
+
exception_data.exception_class.should == exception.class.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should post exception" do
|
24
|
+
exception_data = mock(Exceptional::ExceptionData, :message => "Something bad has happened", :backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"], :class => Exception)
|
25
|
+
Exceptional.should_receive(:call_remote, :with => [:errors, exception_data])
|
26
|
+
Exceptional.post(exception_data)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should catch exception" do
|
30
|
+
exception = mock(Exception, :message => "Something bad has happened", :backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"])
|
31
|
+
exception_data = mock(Exceptional::ExceptionData, :message => "Something bad has happened", :backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"], :class => Exception)
|
32
|
+
Exceptional.should_receive(:parse, :with => [exception]).and_return(exception_data)
|
33
|
+
Exceptional.should_receive(:post, :with => [exception_data])
|
34
|
+
Exceptional.catch(exception)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should raise a license exception if api key is not set" do
|
38
|
+
exception_data = mock(Exceptional::ExceptionData, :message => "Something bad has happened", :backtrace => ["/app/controllers/buggy_controller.rb:29:in `index'"], :class => Exception)
|
39
|
+
Exceptional.api_key.should == nil
|
40
|
+
lambda { Exceptional.post(exception_data) }.should raise_error(Exceptional::LicenseException)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "with a custom host" do
|
46
|
+
|
47
|
+
it "should overwrite default host" do
|
48
|
+
Exceptional.remote_host = "localhost"
|
49
|
+
Exceptional.remote_host.should == "localhost"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should overwrite default port" do
|
53
|
+
Exceptional.remote_port = 3000
|
54
|
+
Exceptional.remote_port.should == 3000
|
55
|
+
Exceptional.remote_port = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "with ssl enabled" do
|
60
|
+
|
61
|
+
it "should connect to port 443" do
|
62
|
+
Exceptional.ssl_enabled = true
|
63
|
+
Exceptional.remote_port.should == 443
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: davidjrice-exceptional
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Rice
|
8
|
+
- Paul Campbell
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2008-10-13 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: json
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
description: Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)
|
26
|
+
email: david@getexceptional.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- History.txt
|
33
|
+
- Manifest
|
34
|
+
- README
|
35
|
+
files:
|
36
|
+
- History.txt
|
37
|
+
- Manifest
|
38
|
+
- README
|
39
|
+
- Rakefile
|
40
|
+
- exceptional.gemspec
|
41
|
+
- exceptional.yml
|
42
|
+
- init.rb
|
43
|
+
- install.rb
|
44
|
+
- lib/exceptional/agent/worker.rb
|
45
|
+
- lib/exceptional/deployed_environment.rb
|
46
|
+
- lib/exceptional/exception_data.rb
|
47
|
+
- lib/exceptional/integration/rails.rb
|
48
|
+
- lib/exceptional/rails.rb
|
49
|
+
- lib/exceptional/version.rb
|
50
|
+
- lib/exceptional.rb
|
51
|
+
has_rdoc: true
|
52
|
+
homepage: http://github.com/contrast/exceptional
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options:
|
55
|
+
- --main
|
56
|
+
- README
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.2.0
|
75
|
+
signing_key:
|
76
|
+
specification_version: 2
|
77
|
+
summary: Exceptional is the core Ruby library for communicating with http://getexceptional.com (hosted error tracking service)
|
78
|
+
test_files:
|
79
|
+
- spec/deployed_environment_spec.rb
|
80
|
+
- spec/exception_data_spec.rb
|
81
|
+
- spec/exceptional_spec.rb
|
82
|
+
- spec/spec_helper.rb
|
83
|
+
- spec/worker_spec.rbrb
|