Skip to content

Commit

Permalink
Add rack middleware (#3)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Ohm <[email protected]>
  • Loading branch information
Marc Tudurí and Sebastian Ohm authored Jan 6, 2022
1 parent 2c987cb commit bf6a617
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ source "https://rubygems.org"

gem "rspec"
gem "timecop"
gem "rack"
gem "webmock"
14 changes: 14 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
GEM
remote: https://rubygems.org/
specs:
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
crack (0.4.5)
rexml
diff-lcs (1.4.4)
hashdiff (1.0.1)
public_suffix (4.0.6)
rack (2.2.3)
rexml (3.2.5)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
Expand All @@ -16,13 +24,19 @@ GEM
rspec-support (~> 3.10.0)
rspec-support (3.10.2)
timecop (0.9.4)
webmock (3.14.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)

PLATFORMS
ruby

DEPENDENCIES
rack
rspec
timecop
webmock

BUNDLED WITH
2.1.4
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,17 @@ end
puts(exporter.export)
```

### Run tests
## Use a Rack middleware

You can use this library as a [Rack](https://github.com/rack/rack) middleware, that allow us you to capture any error happening during the life of a request. You need to use it with an instance of a [pushgateway](https://github.com/soundcloud/periskop-pushgateway/).

```ruby
require 'periskop/rack/middleware'

use Periskop::Rack::Middleware, {pushgateway_address: "http://localhost:7878"}
```

## Run tests

1. `make prepare`
2. `make test`
10 changes: 9 additions & 1 deletion lib/periskop/client/collector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def add_exception(exception, context)
exception.class.name,
exception.message,
exception.backtrace,
exception.cause
get_cause(exception)
)
exception_with_context = ExceptionWithContext.new(
exception_instance,
Expand All @@ -58,6 +58,14 @@ def add_exception(exception, context)
aggregated_exception = @aggregated_exceptions_dict[aggregation_key]
aggregated_exception.add_exception(exception_with_context)
end

def get_cause(exception)
if RUBY_VERSION > '2.0'
return exception.cause
end

nil
end
end
end
end
11 changes: 11 additions & 0 deletions lib/periskop/client/exporter.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
require 'net/http'
require 'uri'

module Periskop
module Client
# Exporter exposes in json format all collected exceptions from the specified `collector`
Expand All @@ -9,6 +12,14 @@ def initialize(collector)
def export
@collector.aggregated_exceptions.to_json
end

def push_to_gateway(addr)
uri = URI.parse("#{addr}/errors")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri, 'Content-Type' => 'application/json')
request.body = export
http.request(request)
end
end
end
end
87 changes: 87 additions & 0 deletions lib/periskop/rack/middleware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require 'periskop/client/collector'
require 'periskop/client/exporter'
require 'periskop/client/models'

module Periskop
module Rack
class Middleware
attr_accessor :collector

def initialize(app, options = {})
@app = app
@pushgateway_address = options.fetch(:pushgateway_address)
@collector = Periskop::Client::ExceptionCollector.new
@exporter = Periskop::Client::Exporter.new(@collector)
end

def call(env)
begin
response = @app.call(env)
rescue Exception => ex
report_push(env, ex)
raise(ex)
end

maybe_ex = framework_exception(env)
report_push(env, maybe_ex) if maybe_ex

response
end

private

# Web framework middlewares often store rescued exceptions inside the
# Rack env, but Rack doesn't have a standard key for it:
#
# - Rails uses action_dispatch.exception: https://goo.gl/Kd694n
# - Sinatra uses sinatra.error: https://goo.gl/LLkVL9
# - Goliath uses rack.exception: https://goo.gl/i7e1nA
def framework_exception(env)
env['rack.exception'] ||
env['sinatra.error'] ||
env['action_dispatch.exception']
end

def find_request(env)
if defined?(ActionDispatch::Request)
ActionDispatch::Request.new(env)
elsif defined?(Sinatra::Request)
Sinatra::Request.new(env)
else
::Rack::Request.new(env)
end
end

def get_http_headers(request_env)
header_prefixes = %w[
HTTP_
CONTENT_TYPE
CONTENT_LENGTH
].freeze

request_env.map.with_object({}) do |(key, value), headers|
if header_prefixes.any? { |prefix| key.to_s.start_with?(prefix) }
headers[key] = value
end
headers
end
end

def get_http_context(env)
request = find_request(env)
Periskop::Client::HTTPContext.new(request.request_method, request.url, get_http_headers(request.env), nil)
end

def report_push(env, maybe_ex)
ex =
if maybe_ex.is_a?(Exception)
maybe_ex
else
RuntimeError.new(maybe_ex.to_s)
end
@collector.report_with_context(ex, get_http_context(env))
@exporter.push_to_gateway(@pushgateway_address)
end
end
end
end
10 changes: 9 additions & 1 deletion spec/periskop/client/exporter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,21 @@
end
end

def get_aggregation_hash()
if RUBY_VERSION < '3.0'
'cfbf9f17'
else
'138b8e97'
end
end

it 'exports to the expected format' do
expected_json = <<HEREDOC
{
"target_uuid": "5d9893c6-51d6-11ea-8aad-f894c260afe5",
"aggregated_errors": [
{
"aggregation_key": "StandardError@cfbf9f17",
"aggregation_key": "StandardError@#{get_aggregation_hash()}",
"total_count": 1,
"severity": "error",
"created_at": "2019-10-11T12:47:25Z",
Expand Down
28 changes: 28 additions & 0 deletions spec/periskop/rack/middleware_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require 'rack'
require 'periskop/rack/middleware'
require 'webmock/rspec'

describe Periskop::Rack::Middleware do
class App
def call(_)
raise StandardError
end
end

let :middleware do
stub_request(:post, 'http://localhost:7878/errors')
Periskop::Rack::Middleware.new(App.new, pushgateway_address: 'http://localhost:7878')
end

it 'captures exception on error' do
begin
middleware.call env_for('http://example.com?q=s', {})
rescue StandardError
expect(middleware.collector.aggregated_exceptions_dict.size).to eq(1)
end
end

def env_for(url, opts = {})
Rack::MockRequest.env_for(url, opts)
end
end

0 comments on commit bf6a617

Please sign in to comment.