contrast-exceptional 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 +19 -0
- data/lib/exceptional/agent/worker.rb +56 -0
- data/lib/exceptional/deployed_environment.rb +86 -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 +231 -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
- data/spec/worker_spec.rb +21 -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.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
|
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,19 @@
|
|
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
|
+
File.copy example_config_file, config_file
|
19
|
+
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,86 @@
|
|
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
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
@@ -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,231 @@
|
|
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
|
+
# 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))
|
17
|
+
|
18
|
+
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
|
+
|
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
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
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
data/spec/worker_spec.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Exceptional::Agent::Worker do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@worker = Exceptional::Agent::Worker.new
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "after initialisation" do
|
10
|
+
|
11
|
+
it "should default worker timeout" do
|
12
|
+
@worker.timeout.should == 10
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should have no exceptions" do
|
16
|
+
@worker.exceptions.should == []
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: contrast-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.rb
|