resque-exceptional 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md ADDED
@@ -0,0 +1,3 @@
1
+ ## 0.0.1 (2010-10-13)
2
+
3
+ * Initial release.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Luke Antins
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ Software), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ resque-exceptional
2
+ ==================
3
+
4
+ resque-exceptional provides a Resque failure backend that sends exceptions
5
+ raised by jobs to http://getexceptional.com
6
+
7
+ Install & Quick Start
8
+ ---------------------
9
+
10
+ Before you jump into code, you'll need a http://getexceptional.com account.
11
+
12
+ To install:
13
+
14
+ $ gem install resque-exceptional
15
+
16
+ ### Example: Single Failure Backend
17
+
18
+ Using only the exceptional failure backend:
19
+
20
+ require 'resque'
21
+ require 'resque-exceptional'
22
+
23
+ Resque::Failure::Exceptional.configure do |config|
24
+ config.api_key = '505f2518c41866bb0be7ba434bb2b079'
25
+ config.use_ssl = false
26
+ end
27
+
28
+ Resque::Failure.backend = Resque::Failure::Exceptional
29
+
30
+ ### Example: Multiple Failure Backends
31
+
32
+ Using both the redis and exceptional failure backends:
33
+
34
+ require 'resque'
35
+ require 'resque-exceptional'
36
+
37
+ require 'resque/failure/multiple'
38
+ require 'resque/failure/redis'
39
+
40
+ Resque::Failure::Exceptional.configure do |config|
41
+ config.api_key = '505f2518c41866bb0be7ba434bb2b079'
42
+ end
43
+
44
+ Resque::Failure::Multiple.classes = [Resque::Failure::Redis, Resque::Failure::Exceptional]
45
+ Resque::Failure.backend = Resque::Failure::Multiple
46
+
47
+ Configuration Options
48
+ ---------------------
49
+
50
+ **Required**
51
+
52
+ * `api_key` - your getexceptional.com api key.
53
+
54
+ **HTTP Proxy Options** *(optional)*
55
+
56
+ * `proxy_host` - proxy server ip / hostname.
57
+ * `proxy_port` - proxy server port.
58
+ * `proxy_user` - proxy server username.
59
+ * `proxy_pass` - proxy server password.
60
+
61
+ **HTTP Client Options** *(optional)*
62
+
63
+ * `use_ssl` - set `true` if your plan supports ssl. (default: `false`)
64
+ * `http_open_timeout` - timeout in seconds to establish the connection. (default: `2`)
65
+ * `http_read_timeout` - timeout in seconds to wait for a reply. (default: `5`)
66
+
67
+ Note on Patches/Pull Requests
68
+ -----------------------------
69
+
70
+ * Fork the project.
71
+ * Make your feature addition or bug fix.
72
+ * Add tests for it. This is important so I don't break it in a future
73
+ version unintentionally.
74
+ * Commit, do not mess with the version. (if you want to have your own
75
+ version, that is fine but bump version in a commit by itself I can ignore
76
+ when I pull)
77
+ * Send me a pull request. Bonus points for topic branches.
78
+
79
+ Author
80
+ ------
81
+
82
+ Luke Antins :: http://lividpenguin.com :: @lantins
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+
3
+ require 'rake/testtask'
4
+ require 'fileutils'
5
+ require 'yard'
6
+ require 'yard/rake/yardoc_task'
7
+
8
+ task :default => :test
9
+
10
+ ##
11
+ # Test task.
12
+ Rake::TestTask.new(:test) do |task|
13
+ task.test_files = FileList['test/*_test.rb']
14
+ task.verbose = true
15
+ end
16
+
17
+ ##
18
+ # docs task.
19
+ YARD::Rake::YardocTask.new :yardoc do |t|
20
+ t.files = ['lib/**/*.rb']
21
+ t.options = ['--output-dir', "doc/",
22
+ '--files', 'LICENSE HISTORY.md',
23
+ '--readme', 'README.md',
24
+ '--title', 'resque-exceptional documentation']
25
+ end
@@ -0,0 +1,159 @@
1
+ module Resque
2
+ module Failure
3
+
4
+ # A resque failure backend that sends exception data to getexceptional.com
5
+ class Exceptional < Base
6
+ # Raised if the api_key is not set.
7
+ class APIKeyError < StandardError
8
+ end
9
+
10
+ # Our version number =)
11
+ Version = '0.0.1'
12
+
13
+ class << self
14
+ # API Settings.
15
+ attr_accessor :api_key
16
+ # HTTP Proxy Options.
17
+ attr_accessor :proxy_host, :proxy_port, :proxy_user, :proxy_pass
18
+ # HTTP Client Options.
19
+ attr_accessor :use_ssl, :http_open_timeout, :http_read_timeout
20
+ end
21
+
22
+ # Configures the failure backend. At a minimum you will need to set
23
+ # an api_key.
24
+ def self.configure
25
+ yield self
26
+ end
27
+
28
+ # Sends the exception data to the exceptional api.
29
+ #
30
+ # When a job fails, a new instance is created and #save is called.
31
+ def save
32
+ return unless response = http_post_request
33
+
34
+ if response.code == '200'
35
+ log "success - api accepted the exception data."
36
+ else
37
+ body = response.body if response.respond_to? :body
38
+ log "fail - expected: 200 OK, received: #{response.code} #{response.message}"
39
+ end
40
+ end
41
+
42
+ # Sends a HTTP Post to the exceptional api.
43
+ #
44
+ # @return [Net::HTTPResponse] http response data.
45
+ # @return [nil] if something went wrong.
46
+ def http_post_request
47
+ begin
48
+ return http_client.post(http_path_query, compressed_request, http_headers)
49
+ rescue APIKeyError
50
+ log 'error - you must set your api_key.'
51
+ rescue TimeoutError
52
+ log 'fail - timeout while contacting the api server.'
53
+ rescue Exception => e
54
+ log "fail - exception raised during http post. (#{e.class.name}: #{e.message})"
55
+ end
56
+ nil
57
+ end
58
+
59
+ # HTTP headers to send.
60
+ #
61
+ # @return [Hash] http headers.
62
+ def http_headers
63
+ { 'Content-Type' => 'application/json', 'Accept' => 'application/json' }
64
+ end
65
+
66
+ # Returns the compressed request data.
67
+ def compressed_request
68
+ Zlib::Deflate.deflate(api_request.to_json, Zlib::BEST_SPEED)
69
+ end
70
+
71
+ # Path & query options used by the HTTP Post.
72
+ #
73
+ # @raise [APIKeyError] if the api_key is not set.
74
+ # @return [String] http path & query options.
75
+ def http_path_query
76
+ raise APIKeyError, 'api key must be set.' unless self.class.api_key
77
+ hash_param = uniqueness_hash.nil? ? nil : "&hash=#{uniqueness_hash}"
78
+ "/api/errors?api_key=#{self.class.api_key}&protocol_version=5#{hash_param}"
79
+ end
80
+
81
+ # Calculates a uniqueness md5sum of the exception backtrace if available.
82
+ #
83
+ # nb. this isn't documented in the public api... not sure if we should
84
+ # use it or not...
85
+ def uniqueness_hash
86
+ return nil if (exception.backtrace.nil? || exception.backtrace.empty?)
87
+ Digest::MD5.hexdigest(exception.backtrace.join)
88
+ end
89
+
90
+ # Configures a HTTP client.
91
+ #
92
+ # @return [Net::HTTP] http client.
93
+ def http_client
94
+ # pass any proxy settings.
95
+ proxy = Net::HTTP::Proxy(self.class.proxy_host, self.class.proxy_port,
96
+ self.class.proxy_user, self.class.proxy_pass)
97
+ http = proxy.new('api.getexceptional.com', http_port)
98
+
99
+ # set http client options.
100
+ http.read_timeout = self.class.http_read_timeout || 5
101
+ http.open_timeout = self.class.http_open_timeout || 2
102
+ http.use_ssl = use_ssl?
103
+
104
+ http
105
+ end
106
+
107
+ # Helper method to return the correct HTTP port number.
108
+ def http_port
109
+ use_ssl? ? 443 : 80
110
+ end
111
+
112
+ # Helper method to check if were using SSL or not.
113
+ def use_ssl?
114
+ self.class.use_ssl || false
115
+ end
116
+
117
+ # Adds a prefix to log messages.
118
+ def log(msg)
119
+ super("resque-exception - #{msg}")
120
+ end
121
+
122
+ # API request data structure.
123
+ #
124
+ # @return [Hash] data structure expected by the api.
125
+ def api_request
126
+ {
127
+ 'request' => {
128
+ 'parameters' => {
129
+ 'queue' => queue.to_s,
130
+ 'job_class' => payload['class'].to_s,
131
+ 'job_args' => payload['args'],
132
+ 'worker' => worker.to_s
133
+ }
134
+ },
135
+ 'application_environment' => {
136
+ 'env' => ENV.to_hash,
137
+ 'application_root_directory' => ENV['PWD']
138
+ },
139
+ 'exception' => {
140
+ 'occurred_at' => Time.now.iso8601,
141
+ 'message' => "#{exception.class.name}: #{exception.message}",
142
+ 'backtrace' => Array(exception.backtrace),
143
+ 'exception_class' => exception.class.name
144
+ },
145
+ 'rescue_block' => {
146
+ 'name' => 'Resque Failure'
147
+ },
148
+ 'client' => {
149
+ 'name' => 'resque-exceptional',
150
+ 'version' => Resque::Failure::Exceptional::Version,
151
+ 'protocol_version' => 5
152
+ }
153
+ }
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,8 @@
1
+ require 'zlib'
2
+ require 'net/http'
3
+ require 'net/https'
4
+ require 'time'
5
+ require 'digest/md5'
6
+ require 'resque'
7
+
8
+ require 'resque/failure/exceptional'
@@ -0,0 +1,118 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ # Tests the failure backend works with resque, does not contact the api.
4
+ class ExceptionalTest < Test::Unit::TestCase
5
+ def setup
6
+ @exception = TestApp.grab_exception
7
+ @worker = FakeWorker.new
8
+ @queue = 'test_queue'
9
+ @payload = { 'class' => 'TestJob', 'args' => ['foo', 'bar'] }
10
+ @failure = Resque::Failure::Exceptional.new(@exception, @worker, @queue, @payload)
11
+ end
12
+
13
+ # test we can build a hash to send to the api.
14
+ def test_can_build_api_request_data_hash
15
+ data = @failure.api_request
16
+ assert_kind_of Hash, data, 'should build a hash'
17
+ end
18
+
19
+ # include the minimum entries required by the api.
20
+ def test_api_request_includes_minimum_api_entries
21
+ data = @failure.api_request
22
+
23
+ assert_kind_of Hash, data['application_environment']
24
+ assert_kind_of Hash, data['application_environment']['env']
25
+ assert_kind_of String, data['application_environment']['application_root_directory']
26
+
27
+ assert_kind_of Hash, data['exception']
28
+ # test occurred_at that starts like: 2010-10-13T01:56:49
29
+ assert_match /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, data['exception']['occurred_at']
30
+ assert_kind_of String, data['exception']['message']
31
+ assert_kind_of Array, data['exception']['backtrace']
32
+ assert_equal 'TestApp::Error', data['exception']['exception_class']
33
+ end
34
+
35
+ # include the resque information, we sneak it in as request data so we can
36
+ # view it via the web interface.
37
+ def test_api_request_includes_resque_info
38
+ data = @failure.api_request
39
+
40
+ assert_kind_of Hash, data['request']
41
+ assert_kind_of Hash, data['request']['parameters']
42
+ assert_kind_of String, data['request']['parameters']['queue']
43
+ assert_kind_of String, data['request']['parameters']['job_class']
44
+ assert_kind_of Array, data['request']['parameters']['job_args']
45
+ assert_kind_of String, data['request']['parameters']['worker']
46
+ end
47
+
48
+ # make it more obvious in the web interface this was a resque failure.
49
+ def test_api_request_includes_resque_block
50
+ data = @failure.api_request
51
+
52
+ assert_kind_of Hash, data['rescue_block']
53
+ assert_equal 'Resque Failure', data['rescue_block']['name']
54
+ end
55
+
56
+ # let them know who we are.
57
+ def test_api_request_includes_client_info
58
+ data = @failure.api_request
59
+
60
+ assert_equal 'resque-exceptional', data['client']['name'], 'should use the gem name'
61
+ assert_match /^\d+\.\d+\.\d+$/, data['client']['version'], 'should have a version number'
62
+ assert_kind_of Fixnum, data['client']['protocol_version']
63
+ end
64
+
65
+ # we need the ability to configure the failure backend before its created.
66
+ # config settings should be class variables.
67
+ def test_configure
68
+ Resque::Failure::Exceptional.configure do |config|
69
+ config.api_key = 'my api key.'
70
+ # everything below are http client options.
71
+ config.proxy_host = 'host.name.com'
72
+ config.proxy_port = 8080
73
+ config.proxy_user = 'foo'
74
+ config.proxy_pass = 'bar'
75
+ config.use_ssl = true
76
+ config.http_open_timeout = 5
77
+ config.http_read_timeout = 10
78
+ end
79
+
80
+ # reset everything to nil...
81
+ Resque::Failure::Exceptional.configure do |config|
82
+ options = %w{api_key proxy_host proxy_port proxy_user proxy_pass use_ssl
83
+ http_open_timeout http_read_timeout}
84
+ options.each { |opt| config.send("#{opt}=", nil) }
85
+ end
86
+ end
87
+
88
+ # failure backends need to define a save method.
89
+ def test_save_defined
90
+ assert_equal true, @failure.respond_to?(:save)
91
+ end
92
+
93
+ # we need a Net::HTTP client setup to send the data.
94
+ def test_http_client
95
+
96
+ end
97
+
98
+ # perform a test with the real api.
99
+ def test_live_fire
100
+ #omit 'comment this line, set your api key, test with real api!'
101
+ Resque::Failure::Exceptional.configure { |c| c.api_key = 'e6bee67c9a45ae41c8318e7d9a0e9d8d0dd9bc20' }
102
+ @failure.save
103
+ assert_match /^(resque-exception).*(success).*$/, @worker.log_history.first
104
+ # reset.
105
+ Resque::Failure::Exceptional.configure { |c| c.api_key = nil }
106
+ end
107
+
108
+ # we should fail if the api_key is not set.
109
+ def test_fail_if_api_key_nil
110
+ # should already be nil, but lets just be sure...
111
+ Resque::Failure::Exceptional.configure { |c| c.api_key = nil }
112
+ @failure.save
113
+ assert_match /^(resque-exception).*(error).*(api_key).*$/, @worker.log_history.first
114
+ end
115
+
116
+ # should handle exceptions raised during the HTTP Post.
117
+ # should return and not raise anything if we were successful.
118
+ end
@@ -0,0 +1,10 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ # make sure the worlds not fallen from beneith us.
4
+ class ResqueTest < Test::Unit::TestCase
5
+ def test_resque_version
6
+ major, minor, patch = Resque::Version.split('.')
7
+ assert_equal 1, major.to_i, 'major version does not match'
8
+ assert_operator minor.to_i, :>=, 8, 'minor version is too low'
9
+ end
10
+ end
@@ -0,0 +1,60 @@
1
+ dir = File.dirname(File.expand_path(__FILE__))
2
+ $LOAD_PATH.unshift dir + '/../lib'
3
+ $TESTING = true
4
+
5
+ require 'test/unit'
6
+ require 'rubygems'
7
+ require 'simplecov'
8
+ require 'rr'
9
+
10
+ SimpleCov.start do
11
+ add_filter "/test/"
12
+ end
13
+
14
+ class Test::Unit::TestCase
15
+ include RR::Adapters::TestUnit
16
+ end
17
+
18
+ # require our failure backend to test.
19
+ require 'resque-exceptional'
20
+
21
+ # fake worker.
22
+ class FakeWorker
23
+ attr_reader :log_history
24
+
25
+ def initialize
26
+ @log_history = []
27
+ end
28
+
29
+ def log(msg)
30
+ @log_history << msg
31
+ p msg
32
+ end
33
+
34
+ def to_s
35
+ 'mr. fake resque worker.'
36
+ end
37
+ end
38
+
39
+ # test exceptions.
40
+ module TestApp
41
+ class Error < StandardError
42
+ end
43
+
44
+ def self.method_bar
45
+ raise Error, 'example exception message. bar.'
46
+ end
47
+
48
+ def self.method_foo
49
+ method_bar
50
+ end
51
+
52
+ def self.grab_exception
53
+ begin
54
+ method_foo
55
+ rescue => e
56
+ return e
57
+ end
58
+ end
59
+
60
+ end
metadata ADDED
@@ -0,0 +1,150 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-exceptional
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Luke Antins
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-13 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: resque
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 55
30
+ segments:
31
+ - 1
32
+ - 8
33
+ - 0
34
+ version: 1.8.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: test-unit
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 3
46
+ segments:
47
+ - 0
48
+ version: "0"
49
+ type: :development
50
+ version_requirements: *id002
51
+ - !ruby/object:Gem::Dependency
52
+ name: rr
53
+ prerelease: false
54
+ requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ hash: 23
60
+ segments:
61
+ - 1
62
+ - 0
63
+ - 0
64
+ version: 1.0.0
65
+ type: :development
66
+ version_requirements: *id003
67
+ - !ruby/object:Gem::Dependency
68
+ name: yard
69
+ prerelease: false
70
+ requirement: &id004 !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 3
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ type: :development
80
+ version_requirements: *id004
81
+ - !ruby/object:Gem::Dependency
82
+ name: simplecov
83
+ prerelease: false
84
+ requirement: &id005 !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ hash: 19
90
+ segments:
91
+ - 0
92
+ - 3
93
+ - 0
94
+ version: 0.3.0
95
+ type: :development
96
+ version_requirements: *id005
97
+ description: " resque-exceptional provides a Resque failure backend that sends exceptions\n raised by jobs to getexceptional.com.\n"
98
+ email: luke@lividpenguin.com
99
+ executables: []
100
+
101
+ extensions: []
102
+
103
+ extra_rdoc_files: []
104
+
105
+ files:
106
+ - LICENSE
107
+ - Rakefile
108
+ - README.md
109
+ - HISTORY.md
110
+ - test/exceptional_test.rb
111
+ - test/resque_test.rb
112
+ - test/test_helper.rb
113
+ - lib/resque/failure/exceptional.rb
114
+ - lib/resque-exceptional.rb
115
+ has_rdoc: false
116
+ homepage: http://github.com/lantins/resque-exceptional
117
+ licenses: []
118
+
119
+ post_install_message:
120
+ rdoc_options: []
121
+
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ hash: 3
130
+ segments:
131
+ - 0
132
+ version: "0"
133
+ required_rubygems_version: !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ hash: 3
139
+ segments:
140
+ - 0
141
+ version: "0"
142
+ requirements: []
143
+
144
+ rubyforge_project:
145
+ rubygems_version: 1.3.7
146
+ signing_key:
147
+ specification_version: 3
148
+ summary: A Resque failure backend for getexceptional.com
149
+ test_files: []
150
+