resque-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.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
+