From bf6a617068e28f75074cf2f4ecf61d1ea8f9a835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Tudur=C3=AD?= Date: Thu, 6 Jan 2022 15:25:37 +0100 Subject: [PATCH] Add rack middleware (#3) Co-authored-by: Sebastian Ohm --- Gemfile | 2 + Gemfile.lock | 14 +++++ README.md | 12 +++- lib/periskop/client/collector.rb | 10 ++- lib/periskop/client/exporter.rb | 11 ++++ lib/periskop/rack/middleware.rb | 87 +++++++++++++++++++++++++++ spec/periskop/client/exporter_spec.rb | 10 ++- spec/periskop/rack/middleware_spec.rb | 28 +++++++++ 8 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 lib/periskop/rack/middleware.rb create mode 100644 spec/periskop/rack/middleware_spec.rb diff --git a/Gemfile b/Gemfile index 5f91769..dbf4793 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,5 @@ source "https://rubygems.org" gem "rspec" gem "timecop" +gem "rack" +gem "webmock" diff --git a/Gemfile.lock b/Gemfile.lock index 239e37e..9ed2d98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -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 diff --git a/README.md b/README.md index 71de15d..e9602ba 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/lib/periskop/client/collector.rb b/lib/periskop/client/collector.rb index a5f7177..ac6cc88 100644 --- a/lib/periskop/client/collector.rb +++ b/lib/periskop/client/collector.rb @@ -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, @@ -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 diff --git a/lib/periskop/client/exporter.rb b/lib/periskop/client/exporter.rb index 2bf41dc..5bedcdb 100644 --- a/lib/periskop/client/exporter.rb +++ b/lib/periskop/client/exporter.rb @@ -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` @@ -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 diff --git a/lib/periskop/rack/middleware.rb b/lib/periskop/rack/middleware.rb new file mode 100644 index 0000000..53ef46d --- /dev/null +++ b/lib/periskop/rack/middleware.rb @@ -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 diff --git a/spec/periskop/client/exporter_spec.rb b/spec/periskop/client/exporter_spec.rb index 878143c..d058c48 100644 --- a/spec/periskop/client/exporter_spec.rb +++ b/spec/periskop/client/exporter_spec.rb @@ -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 = <