temescal 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 668a0e9c9bb3d5400a4b9b28302607767828bc0c
4
+ data.tar.gz: 1f1a62d3e48d5db8cd32b853a06ea116be8a3b69
5
+ SHA512:
6
+ metadata.gz: 614f71816ca80c27852abe47a8aef37702fc8eced6143613ed25735dd1bc3bc051a7e9d4f4dd1eefb1709aedc6537191354abaab13a837cc57bbc70469f91564
7
+ data.tar.gz: 1bf5f51abe2f31d8e2cd7b8595f97c4d6a86083f99d34f1d800e7e96495de41bae2017b62ec1bb6b7f1ced4a8ccf4fd04062b7c7ea7edde87484a2fd3c0799e9
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013-2014 Todd Bealmear
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ temescal
2
+ ========
3
+
4
+ [![Build Status](https://travis-ci.org/todd/temescal.png?branch=master)](https://travis-ci.org/todd/temescal) [![Coverage Status](https://coveralls.io/repos/todd/temescal/badge.png?branch=master)](https://coveralls.io/r/todd/temescal?branch=master) [![Code Climate](https://codeclimate.com/github/todd/temescal.png)](https://codeclimate.com/github/todd/temescal) [![Dependency Status](https://gemnasium.com/todd/temescal.png)](https://gemnasium.com/todd/temescal)
5
+
6
+ Temescal is Rack middleware that will automatically rescue exceptions for JSON APIs and render a nice, clean JSON response with the error information. No need to write custom error handling logic for your apps - Temescal will take care of it for you!
7
+ ## Getting Started
8
+ Add the gem to your Gemfile and run `bundle install`:
9
+ ```
10
+ gem 'temescal'
11
+ ```
12
+ Since Temescal is just Rack middleware, adding it to your application is super easy. For Rails, add an initializer:
13
+ ```ruby
14
+ Rails.application.config.middleware.use Temescal::Middleware do |config|
15
+ config.monitors = :airbrake, :new_relic
16
+ config.default_message = "Oops! Something went kablooey!"
17
+ end
18
+ ```
19
+ For Sinatra:
20
+ ```ruby
21
+ use Temescal::Middleware do |config|
22
+ config.monitors = :airbrake, :new_relic
23
+ config.default_message = "Oops! Something went kablooey!"
24
+ end
25
+ ```
26
+ ## Default Behavior
27
+ By default, Temescal will render a JSON response formatted as such (using StandardError with a message of "Foobar" as an example):
28
+ ```json
29
+ {
30
+ meta: {
31
+ status: 500,
32
+ error: "StandardError",
33
+ message: "Foobar"
34
+ }
35
+ }
36
+ ```
37
+ Temescal will also log the error for you through STDERR.
38
+ ## Monitors
39
+ Though Temescal will log an error for you, it won't necessarily be picked up by your monitoring solution of choice in a production environment. Luckily, Temescal provides integration with popular monitoring services. At the moment, only two (Airbrake, New Relic) are supported, but more will be added if there's a need. If you use a different monitoring service that you'd like to see supported, pull requests are more than welcome!
40
+
41
+ Note that you'll need the gem for your monitor installed and configured for your application in order for Temescal to properly work with it.
42
+
43
+ ## Configuration
44
+ Temescal provides several configuration options for you. You can set these options when configuring the middleware for your application.
45
+
46
+ `monitors` to set the monitors you'd like to use with Temescal. It takes symbols of monitor names (currently `:airbrake` and `:new_relic`).
47
+
48
+ `raise_errors` to set whether you'd like to override Temescal and raise all errors without rendering a Temescal response. Set to `true` to enable.
49
+
50
+ `default_message` to set an all-encompasing message to use in responses instead of the exception's message. Takes a string.
51
+
52
+ ## License
53
+ Copyright 2013-2014 Todd Bealmear. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new(:spec)
4
+
5
+ task :default => :spec
data/lib/temescal.rb ADDED
@@ -0,0 +1,6 @@
1
+ module Temescal
2
+ require 'temescal/configuration'
3
+ require 'temescal/middleware'
4
+ require 'temescal/monitors'
5
+ require 'temescal/response'
6
+ end
@@ -0,0 +1,54 @@
1
+ module Temescal
2
+ class Configuration
3
+
4
+ # Public: Getter for monitors array.
5
+ attr_reader :monitors
6
+
7
+ # Public: Setter for raise_errors option.
8
+ attr_writer :raise_errors
9
+
10
+ # Public: Getter/Setter for default JSON message.
11
+ attr_accessor :default_message
12
+
13
+ # Public: Initializes configuration and monitors option.
14
+ #
15
+ # Returns a new Configuration object.
16
+ def initialize
17
+ @monitors = []
18
+ end
19
+
20
+ # Public: Setter for monitors option.
21
+ #
22
+ # monitors - Zero or more Symbols representing monitoring services
23
+ # supported by Temescal.
24
+ #
25
+ # Raises NameError if a monitor Symbol is invalid.
26
+ def monitors=(*monitors)
27
+ monitors.flatten.each do |monitor|
28
+ monitor = camelize_symbol(monitor)
29
+ @monitors << Temescal::Monitors.const_get(monitor)
30
+ end
31
+
32
+ rescue NameError => exception
33
+ strategy = exception.message.split(" ").last
34
+ raise NameError.new("#{strategy} is not a valid monitoring strategy")
35
+ end
36
+
37
+ # Public: Getter for raise_errors.
38
+ #
39
+ # Returns true if raise_errors is configured to true or the application is
40
+ # running in a test environment, false otherwise.
41
+ def raise_errors?
42
+ @raise_errors == true || ENV["RACK_ENV"] == "test"
43
+ end
44
+
45
+ private
46
+
47
+ # Private: Converts a snake cased Symbol to a camel cased String.
48
+ #
49
+ # Returns the converted String.
50
+ def camelize_symbol(symbol)
51
+ symbol.to_s.split(/_/).map { |word| word.capitalize }.join
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,55 @@
1
+ module Temescal
2
+ class Middleware
3
+
4
+ # Public: Initializes the middleware.
5
+ #
6
+ # app - The Rack application.
7
+ # block - Optional block for configuring the middleware.
8
+ #
9
+ # Returns an instance of the middleware.
10
+ def initialize(app, &block)
11
+ @app = app
12
+ yield(configuration) if block
13
+ end
14
+
15
+ # Public: call method for Rack application. Rescues from an exception and
16
+ # does the following: 1) Logs the error 2) Reports error to configured
17
+ # monitoring services 3) Generates a JSON response with error information.
18
+ #
19
+ # env - The environment of the request.
20
+ #
21
+ # Returns an array of response data for Rack.
22
+ def call(env)
23
+ begin
24
+ @status, @headers, @response = @app.call(env)
25
+ rescue => error
26
+ raise if configuration.raise_errors?
27
+
28
+ @error = error
29
+ message = configuration.default_message || @error.message
30
+
31
+ $stderr.print formatted_error
32
+ configuration.monitors.each { |monitor| monitor.report(@error) }
33
+
34
+ @status = @error.respond_to?(:http_status) ? @error.http_status : 500
35
+ @response = Response.build(@status, @error, message)
36
+ @headers = { "Content-Type" => "application/json" }
37
+ end
38
+ [@status, @headers, @response]
39
+ end
40
+
41
+ private
42
+
43
+ # Private: Getter for middleware configuration.
44
+ def configuration
45
+ @_configuration ||= Configuration.new
46
+ end
47
+
48
+ # Private: Formats the exception for logging.
49
+ def formatted_error
50
+ message = "\n#{@error.class}: #{@error.message}\n "
51
+ message << @error.backtrace.join("\n ")
52
+ message << "\n\n"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,35 @@
1
+ module Temescal
2
+
3
+ # Public: Collection of reporting strategies for different monitoring
4
+ # services.
5
+ module Monitors
6
+
7
+ # Public: Abstract strategy for monitor reporting strategies.
8
+ class MonitorsStrategy
9
+ # Raises NotImplementedError
10
+ def self.report(exception)
11
+ raise NotImplementedError
12
+ end
13
+ end
14
+
15
+ # Public: Reporting strategy for Airbrake.
16
+ class Airbrake < MonitorsStrategy
17
+ # Public: Reports an exception to Airbrake.
18
+ #
19
+ # exception - The caught exception.
20
+ def self.report(exception)
21
+ ::Airbrake.notify exception
22
+ end
23
+ end
24
+
25
+ # Public: Reporting strategy for New Relic.
26
+ class NewRelic < MonitorsStrategy
27
+ # Public: Reports an exception to New Relic.
28
+ #
29
+ # exception - The caught exception.
30
+ def self.report(exception)
31
+ ::NewRelic::Agent.notice_error exception
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ require 'json'
2
+
3
+ module Temescal
4
+ module Response
5
+
6
+ # Public: Builds a response body for the Rack response.
7
+ #
8
+ # status - The HTTP status code of the response.
9
+ # exception - The caught exception.
10
+ # message - The error message.
11
+ #
12
+ # Returns an Array containing a JSON string as the response body.
13
+ def self.build(status, exception, message)
14
+ [
15
+ {
16
+ meta: {
17
+ status: status,
18
+ error: exception.class.to_s,
19
+ message: message
20
+ }
21
+ }.to_json
22
+ ]
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module Temescal
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,25 @@
1
+ require "spec_helper"
2
+
3
+ class Temescal::Monitors::Monitor; end
4
+ class Temescal::Monitors::FakeMonitor; end
5
+ class Temescal::Monitors::FooMonitor; end
6
+
7
+ describe Temescal::Configuration do
8
+ let(:config) { Temescal::Configuration.new }
9
+
10
+ context "#monitors=" do
11
+ it "should add a list of valid monitor classes to the monitors array from snake-cased arguments" do
12
+ config.monitors = :monitor, :fake_monitor, :foo_monitor
13
+
14
+ expect(config.monitors).to eq [
15
+ Temescal::Monitors::Monitor,
16
+ Temescal::Monitors::FakeMonitor,
17
+ Temescal::Monitors::FooMonitor
18
+ ]
19
+ end
20
+
21
+ it "should raise an error if a monitor strategy is invalid" do
22
+ expect { config.monitors = :foo }.to raise_error(NameError, /Foo is not a valid monitoring strategy/)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,112 @@
1
+ require "spec_helper"
2
+
3
+ describe Temescal::Middleware do
4
+ context "Ok response" do
5
+ let(:app) { ->(env) { [200, env, "app" ] } }
6
+
7
+ let(:middleware) do
8
+ Temescal::Middleware.new(app)
9
+ end
10
+
11
+ it "should render the application's response" do
12
+ code, _, _ = middleware.call env_for('http://foobar.com')
13
+
14
+ expect(code).to eq 200
15
+ end
16
+ end
17
+
18
+ context "Bad response" do
19
+ before do
20
+ Temescal::Monitors::NewRelic.stub(:report)
21
+ $stderr.stub(:print)
22
+
23
+ # Travis automatically sets RACK_ENV=test
24
+ # Need to override for tests to run correctly.
25
+ ENV["RACK_ENV"] = nil
26
+ end
27
+
28
+ let(:app) { ->(env) { raise StandardError.new("Foobar") } }
29
+
30
+ let(:middleware) do
31
+ Temescal::Middleware.new(app) do |config|
32
+ config.monitors = :new_relic
33
+ end
34
+ end
35
+
36
+ it "should respond with a 500 if the exception does not have a http_status attribute" do
37
+ code, _, _ = middleware.call env_for("http://foobar.com")
38
+
39
+ expect(code).to eq 500
40
+ end
41
+
42
+ it "should respond with the appropriate status if the exception has a http_status attribute" do
43
+ StandardError.any_instance.stub(:http_status).and_return(403)
44
+
45
+ code, _, _ = middleware.call env_for("http://foobar.com")
46
+
47
+ expect(code).to eq 403
48
+ end
49
+
50
+ it "should set the correct content type header" do
51
+ _, headers, _ = middleware.call env_for("http://foobar.com")
52
+
53
+ expect(headers["Content-Type"]).to eq "application/json"
54
+ end
55
+
56
+ it "should render a JSON response for the error" do
57
+ _, _, response = middleware.call env_for("http://foobar.com")
58
+
59
+ json = JSON.parse(response.first)["meta"]
60
+ expect(json["status"]).to eq 500
61
+ expect(json["error"]).to eq "StandardError"
62
+ expect(json["message"]).to eq "Foobar"
63
+ end
64
+
65
+ it "should log the error" do
66
+ expect($stderr).to receive(:print).with(an_instance_of String)
67
+
68
+ middleware.call env_for("http://foobar.com")
69
+ end
70
+
71
+ it "should report the error to the specified monitors" do
72
+ expect(Temescal::Monitors::NewRelic).to receive(:report).with(an_instance_of StandardError)
73
+
74
+ middleware.call env_for("http://foobar.com")
75
+ end
76
+
77
+ context "with default_message set" do
78
+ it "should build a response with the specified message instead of the exception message" do
79
+ middleware = Temescal::Middleware.new(app) do |config|
80
+ config.default_message = "An error has occured - we'll get on it right away!"
81
+ end
82
+
83
+ _, _, response = middleware.call env_for("http://foobar.com")
84
+
85
+ json = JSON.parse(response.first)["meta"]
86
+ expect(json["message"]).to eq "An error has occured - we'll get on it right away!"
87
+ end
88
+ end
89
+ end
90
+
91
+ context "Override middleware to raise exception" do
92
+ before do
93
+ $stderr.stub(:print)
94
+ end
95
+
96
+ let(:app) { ->(env) { raise StandardError.new("Foobar") } }
97
+
98
+ let(:middleware) do
99
+ Temescal::Middleware.new(app) do |config|
100
+ config.raise_errors = true
101
+ end
102
+ end
103
+
104
+ it "should raise the error" do
105
+ expect { middleware.call env_for("http://foobar.com") }.to raise_error StandardError
106
+ end
107
+ end
108
+
109
+ def env_for url, opts={}
110
+ Rack::MockRequest.env_for(url, opts)
111
+ end
112
+ end
@@ -0,0 +1,29 @@
1
+ require "spec_helper"
2
+
3
+ describe Temescal::Monitors do
4
+ let(:exception) { StandardError.new }
5
+
6
+ context "MonitorsStrategy" do
7
+ it "should raise an error when the report method is called" do
8
+ expect { Temescal::Monitors::MonitorsStrategy.report(exception) }.to raise_error(NotImplementedError)
9
+ end
10
+ end
11
+
12
+ context "Airbrake" do
13
+ it "should send the exception to Airbrake when report is called" do
14
+ Airbrake.stub(:send_notice)
15
+
16
+ expect(Airbrake).to receive(:notify).with(exception)
17
+
18
+ Temescal::Monitors::Airbrake.report exception
19
+ end
20
+ end
21
+
22
+ context "NewRelic" do
23
+ it "should send the exception to New Relic when report is called" do
24
+ expect(NewRelic::Agent).to receive(:notice_error).with(exception)
25
+
26
+ Temescal::Monitors::NewRelic.report exception
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ if ENV['CI']
2
+ require 'coveralls'
3
+ Coveralls.wear!
4
+ else
5
+ require 'awesome_print'
6
+ require 'pry-debugger'
7
+
8
+ require 'simplecov'
9
+ SimpleCov.start
10
+ end
11
+
12
+ require 'rack'
13
+
14
+ require 'airbrake'
15
+ require 'newrelic_rpm'
16
+
17
+ require 'temescal'
18
+
19
+ RSpec.configure do |config|
20
+ config.treat_symbols_as_metadata_keys_with_true_values = true
21
+ config.run_all_when_everything_filtered = true
22
+ config.filter_run :focus
23
+
24
+ # Run specs in random order to surface order dependencies. If you find an
25
+ # order dependency and want to debug it, you can fix the order by providing
26
+ # the seed, which is printed after each run.
27
+ # --seed 1234
28
+ config.order = 'random'
29
+ end
data/temescal.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ $:.unshift File.expand_path('../lib', __FILE__)
4
+ require 'temescal/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.add_dependency 'rack'
8
+ spec.add_development_dependency 'bundler'
9
+
10
+ spec.authors = ['Todd Bealmear']
11
+ spec.description = %q{Rack Middleware for gracefully handling exceptions for JSON APIs.}
12
+ spec.email = ['todd@t0dd.io']
13
+ spec.files = %w(LICENSE README.md Rakefile temescal.gemspec)
14
+ spec.files += Dir.glob('lib/**/*.rb')
15
+ spec.files += Dir.glob('spec/**/*')
16
+ spec.homepage = 'https://github.com/todd/temescal'
17
+ spec.licenses = ['MIT']
18
+ spec.name = 'temescal'
19
+ spec.require_paths = ['lib']
20
+ spec.summary = spec.description
21
+ spec.version = Temescal::VERSION
22
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: temescal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Todd Bealmear
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rack
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Rack Middleware for gracefully handling exceptions for JSON APIs.
42
+ email:
43
+ - todd@t0dd.io
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - temescal.gemspec
52
+ - lib/temescal/configuration.rb
53
+ - lib/temescal/middleware.rb
54
+ - lib/temescal/monitors.rb
55
+ - lib/temescal/response.rb
56
+ - lib/temescal/version.rb
57
+ - lib/temescal.rb
58
+ - spec/lib/temescal/configuration_spec.rb
59
+ - spec/lib/temescal/middleware_spec.rb
60
+ - spec/lib/temescal/monitors_spec.rb
61
+ - spec/spec_helper.rb
62
+ homepage: https://github.com/todd/temescal
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.1.10
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Rack Middleware for gracefully handling exceptions for JSON APIs.
86
+ test_files: []