Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rack middleware #3

Merged
merged 10 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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