celerbrake 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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/lib/celerbrake/capistrano/capistrano2.rb +40 -0
  3. data/lib/celerbrake/capistrano/capistrano3.rb +23 -0
  4. data/lib/celerbrake/capistrano.rb +8 -0
  5. data/lib/celerbrake/delayed_job.rb +64 -0
  6. data/lib/celerbrake/logger.rb +105 -0
  7. data/lib/celerbrake/rack/context_filter.rb +69 -0
  8. data/lib/celerbrake/rack/http_headers_filter.rb +44 -0
  9. data/lib/celerbrake/rack/http_params_filter.rb +27 -0
  10. data/lib/celerbrake/rack/instrumentable.rb +140 -0
  11. data/lib/celerbrake/rack/middleware.rb +102 -0
  12. data/lib/celerbrake/rack/request_body_filter.rb +33 -0
  13. data/lib/celerbrake/rack/request_store.rb +34 -0
  14. data/lib/celerbrake/rack/route_filter.rb +51 -0
  15. data/lib/celerbrake/rack/session_filter.rb +25 -0
  16. data/lib/celerbrake/rack/user.rb +74 -0
  17. data/lib/celerbrake/rack/user_filter.rb +25 -0
  18. data/lib/celerbrake/rack.rb +39 -0
  19. data/lib/celerbrake/rails/action_cable/notify_callback.rb +22 -0
  20. data/lib/celerbrake/rails/action_cable.rb +37 -0
  21. data/lib/celerbrake/rails/action_controller.rb +40 -0
  22. data/lib/celerbrake/rails/action_controller_notify_subscriber.rb +32 -0
  23. data/lib/celerbrake/rails/action_controller_performance_breakdown_subscriber.rb +51 -0
  24. data/lib/celerbrake/rails/action_controller_route_subscriber.rb +33 -0
  25. data/lib/celerbrake/rails/active_job.rb +50 -0
  26. data/lib/celerbrake/rails/active_record.rb +36 -0
  27. data/lib/celerbrake/rails/active_record_subscriber.rb +46 -0
  28. data/lib/celerbrake/rails/app.rb +78 -0
  29. data/lib/celerbrake/rails/backtrace_cleaner.rb +23 -0
  30. data/lib/celerbrake/rails/curb.rb +32 -0
  31. data/lib/celerbrake/rails/event.rb +93 -0
  32. data/lib/celerbrake/rails/excon_subscriber.rb +25 -0
  33. data/lib/celerbrake/rails/http.rb +18 -0
  34. data/lib/celerbrake/rails/http_client.rb +16 -0
  35. data/lib/celerbrake/rails/net_http.rb +18 -0
  36. data/lib/celerbrake/rails/railtie.rb +54 -0
  37. data/lib/celerbrake/rails/railties/action_controller_tie.rb +90 -0
  38. data/lib/celerbrake/rails/railties/active_record_tie.rb +74 -0
  39. data/lib/celerbrake/rails/railties/middleware_tie.rb +62 -0
  40. data/lib/celerbrake/rails/typhoeus.rb +16 -0
  41. data/lib/celerbrake/rails.rb +32 -0
  42. data/lib/celerbrake/rake/tasks.rb +112 -0
  43. data/lib/celerbrake/rake.rb +66 -0
  44. data/lib/celerbrake/resque.rb +62 -0
  45. data/lib/celerbrake/shoryuken.rb +52 -0
  46. data/lib/celerbrake/sidekiq/retryable_jobs_filter.rb +53 -0
  47. data/lib/celerbrake/sidekiq.rb +53 -0
  48. data/lib/celerbrake/sneakers.rb +72 -0
  49. data/lib/celerbrake/version.rb +7 -0
  50. data/lib/celerbrake.rb +32 -0
  51. data/lib/generators/celerbrake_generator.rb +22 -0
  52. data/lib/generators/celerbrake_initializer.rb.erb +80 -0
  53. metadata +380 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f04c2e12ebb741062851f7f603a5eec5457ab3e5a12a03e6b3ea7d50b934bbb7
4
+ data.tar.gz: 2d8e37ce50181434ed2b8baf785907d1b5d5d8d46f3e57b7680efcedcdde2604
5
+ SHA512:
6
+ metadata.gz: 927a1d719d732e119aab50304d69083de97783b590beff37bd270bb2d5277d25a376051f8f344fac27b37800eb3a7584c5c357445672f99ef7b32b12eb085e74
7
+ data.tar.gz: c8b0b139d4077f2afd93a9414ac7b917e09d0f59ce6e7bb13a66a3cef3bbd8658b8d18f8e11704a03aaac4d40f4d406a0560163abf89b2ca6095aea88dff1972
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Celerbrake
4
+ # The Capistrano v2 integration.
5
+ module Capistrano
6
+ # rubocop:disable Metrics/AbcSize
7
+ def self.load_into(config)
8
+ config.load do
9
+ after 'deploy', 'celerbrake:deploy'
10
+ after 'deploy:migrations', 'celerbrake:deploy'
11
+ after 'deploy:cold', 'celerbrake:deploy'
12
+
13
+ namespace :celerbrake do
14
+ desc "Notify Celerbrake of the deploy"
15
+ task :deploy, except: { no_release: true }, on_error: :continue do
16
+ run(
17
+ <<-CMD, once: true
18
+ cd #{config.release_path} && \
19
+
20
+ RACK_ENV=#{fetch(:rack_env, nil)} \
21
+ RAILS_ENV=#{fetch(:rails_env, nil)} \
22
+
23
+ bundle exec rake celerbrake:deploy \
24
+ USERNAME=#{Shellwords.shellescape(ENV.fetch('USER', nil) || ENV.fetch('USERNAME', nil))} \
25
+ ENVIRONMENT=#{fetch(:celerbrake_env, fetch(:rails_env, 'production'))} \
26
+ REVISION=#{current_revision.strip} \
27
+ REPOSITORY=#{repository} \
28
+ VERSION=#{fetch(:app_version, nil)}
29
+ CMD
30
+ )
31
+ logger.info 'Notified Celerbrake of the deploy'
32
+ end
33
+ end
34
+ end
35
+ end
36
+ # rubocop:enable Metrics/AbcSize
37
+ end
38
+ end
39
+
40
+ Celerbrake::Capistrano.load_into(Capistrano::Configuration.instance)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :celerbrake do
4
+ desc "Notify Celerbrake of the deploy"
5
+ task :deploy do
6
+ role = roles(:all, select: :primary).first || roles(:all).first
7
+ on role do
8
+ within release_path do
9
+ with rails_env: fetch(:rails_env, fetch(:stage)) do
10
+ execute :bundle, :exec, :rake, <<-CMD
11
+ celerbrake:deploy USERNAME=#{Shellwords.shellescape(local_user)} \
12
+ ENVIRONMENT=#{fetch(:celerbrake_env, fetch(:rails_env, fetch(:stage)))} \
13
+ REVISION=#{fetch(:current_revision)} \
14
+ REPOSITORY=#{fetch(:repo_url)} \
15
+ VERSION=#{fetch(:app_version)}
16
+ CMD
17
+
18
+ info 'Notified Celerbrake of the deploy'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Capistrano::VERSION) &&
4
+ Gem::Version.new(Capistrano::VERSION).release >= Gem::Version.new('3.0.0')
5
+ require 'celerbrake/capistrano/capistrano3'
6
+ else
7
+ require 'celerbrake/capistrano/capistrano2'
8
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Delayed
4
+ module Plugins
5
+ # Provides integration with Delayed Job.
6
+ # rubocop:disable Metrics/BlockLength
7
+ class Celerbrake < ::Delayed::Plugin
8
+ callbacks do |lifecycle|
9
+ lifecycle.around(:invoke_job) do |job, *args, &block|
10
+ timing = ::Celerbrake::Benchmark.measure do
11
+ # Forward the call to the next callback in the callback chain
12
+ block.call(job, *args)
13
+ end
14
+ rescue Exception => exception # rubocop:disable Lint/RescueException
15
+ params = job.as_json
16
+
17
+ # If DelayedJob is used through ActiveJob, it contains extra info.
18
+ if job.payload_object.respond_to?(:job_data)
19
+ params[:active_job] = job.payload_object.job_data
20
+ job_class = job.payload_object.job_data['job_class']
21
+ end
22
+
23
+ action = job_class || job.payload_object.class.name
24
+
25
+ ::Celerbrake.notify(exception, params) do |notice|
26
+ notice[:context][:component] = 'delayed_job'
27
+ notice[:context][:action] = action
28
+ end
29
+
30
+ ::Celerbrake.notify_queue(
31
+ queue: action,
32
+ error_count: 1,
33
+ timing: 0.01,
34
+ )
35
+
36
+ raise exception
37
+ else
38
+ ::Celerbrake.notify_queue(
39
+ queue: job_class || job.payload_object.class.name,
40
+ error_count: 0,
41
+ timing: timing,
42
+ )
43
+ end
44
+ end
45
+ end
46
+ # rubocop:enable Metrics/BlockLength
47
+ end
48
+ end
49
+
50
+ if RUBY_ENGINE == 'jruby' && defined?(Delayed::Backend::ActiveRecord::Job)
51
+ # Workaround against JRuby bug:
52
+ # https://github.com/jruby/jruby/issues/3338
53
+ # rubocop:disable Style/ClassAndModuleChildren
54
+ class Delayed::Backend::ActiveRecord::Job
55
+ alias old_to_ary to_ary
56
+
57
+ def to_ary
58
+ old_to_ary || [self]
59
+ end
60
+ end
61
+ # rubocop:enable Style/ClassAndModuleChildren
62
+ end
63
+
64
+ Delayed::Worker.plugins << Delayed::Plugins::Celerbrake
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'delegate'
5
+
6
+ module Celerbrake
7
+ # Decorator for +Logger+ from stdlib. Endows loggers the ability to both log
8
+ # and report errors to Celerbrake.
9
+ #
10
+ # @example
11
+ # # Create a logger like you normally do and decorate it.
12
+ # logger = Celerbrake::CelerbrakeLogger.new(Logger.new($stdout))
13
+ #
14
+ # # Just use the logger like you normally do.
15
+ # logger.fatal('oops')
16
+ class CelerbrakeLogger < SimpleDelegator
17
+ # @example
18
+ # # Assign a custom Celerbrake notifier
19
+ # logger.celerbrake_notifier = Celerbrake::NoticeNotifier.new
20
+ # @return [Celerbrake::Notifier] notifier to be used to send notices
21
+ attr_accessor :celerbrake_notifier
22
+
23
+ # @return [Integer]
24
+ attr_reader :celerbrake_level
25
+
26
+ def initialize(logger)
27
+ super
28
+
29
+ __setobj__(logger)
30
+ @celerbrake_notifier = Celerbrake
31
+ self.level = logger.level
32
+ end
33
+
34
+ # @see Logger#warn
35
+ def warn(progname = nil, &block)
36
+ notify_celerbrake(Logger::WARN, progname)
37
+ super
38
+ end
39
+
40
+ # @see Logger#error
41
+ def error(progname = nil, &block)
42
+ notify_celerbrake(Logger::ERROR, progname)
43
+ super
44
+ end
45
+
46
+ # @see Logger#fatal
47
+ def fatal(progname = nil, &block)
48
+ notify_celerbrake(Logger::FATAL, progname)
49
+ super
50
+ end
51
+
52
+ # @see Logger#unknown
53
+ def unknown(progname = nil, &block)
54
+ notify_celerbrake(Logger::UNKNOWN, progname)
55
+ super
56
+ end
57
+
58
+ # @see Logger#level=
59
+ def level=(value)
60
+ self.celerbrake_level = value < Logger::WARN ? Logger::WARN : value
61
+ super
62
+ end
63
+
64
+ # Sets celerbrake severity level. Does not permit values below `Logger::WARN`.
65
+ #
66
+ # @example
67
+ # logger.celerbrake_level = Logger::FATAL
68
+ # @return [void]
69
+ def celerbrake_level=(level)
70
+ if level < Logger::WARN
71
+ raise "Celerbrake severity level #{level} is not allowed. " \
72
+ "Minimum allowed level is #{Logger::WARN}"
73
+ end
74
+ @celerbrake_level = level
75
+ end
76
+
77
+ private
78
+
79
+ def notify_celerbrake(severity, progname)
80
+ return if severity < @celerbrake_level || !@celerbrake_notifier
81
+
82
+ @celerbrake_notifier.notify(progname) do |notice|
83
+ # Get rid of unwanted internal Logger frames. Examples:
84
+ # * /ruby-2.4.0/lib/ruby/2.4.0/logger.rb
85
+ # * /gems/activesupport-4.2.7.1/lib/active_support/logger.rb
86
+ backtrace = notice[:errors].first[:backtrace]
87
+ notice[:errors].first[:backtrace] =
88
+ backtrace.drop_while { |frame| frame[:file] =~ %r{/logger.rb\z} }
89
+
90
+ notice[:context][:component] = 'log'
91
+ notice[:context][:severity] = normalize_severity(severity)
92
+ end
93
+ end
94
+
95
+ def normalize_severity(severity)
96
+ (case severity
97
+ when Logger::WARN then 'warning'
98
+ when Logger::ERROR, Logger::UNKNOWN then 'error'
99
+ when Logger::FATAL then 'critical'
100
+ else
101
+ raise "Unknown celerbrake severity: #{severity}"
102
+ end).freeze
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Celerbrake
4
+ module Rack
5
+ # Adds context (URL, User-Agent, framework version, controller and more).
6
+ #
7
+ # @since v5.7.0
8
+ class ContextFilter
9
+ # @return [Integer]
10
+ attr_reader :weight
11
+
12
+ def initialize
13
+ @weight = 99
14
+ end
15
+
16
+ # @see Celerbrake::FilterChain#refine
17
+ def call(notice)
18
+ return unless (request = notice.stash[:rack_request])
19
+
20
+ context = notice[:context]
21
+
22
+ context[:url] = request.url
23
+ context[:userAgent] = request.user_agent
24
+
25
+ add_ip(context, request)
26
+ add_framework_version(context)
27
+
28
+ controller = request.env['action_controller.instance']
29
+ return unless controller
30
+
31
+ context[:component] = controller.controller_name
32
+ context[:action] = controller.action_name
33
+ end
34
+
35
+ private
36
+
37
+ def add_framework_version(context)
38
+ if context.key?(:versions)
39
+ context[:versions].merge!(framework_version)
40
+ else
41
+ context[:versions] = framework_version
42
+ end
43
+ end
44
+
45
+ def framework_version
46
+ @framework_version ||=
47
+ if defined?(::Rails) && ::Rails.respond_to?(:version)
48
+ { 'rails' => ::Rails.version }
49
+ elsif defined?(::Sinatra)
50
+ { 'sinatra' => Sinatra::VERSION }
51
+ else
52
+ {
53
+ 'rack_version' => ::Rack.version,
54
+ 'rack_release' => ::Rack.release,
55
+ }
56
+ end
57
+ end
58
+
59
+ def add_ip(context, request)
60
+ context[:userAddr] =
61
+ if request.respond_to?(:remote_ip)
62
+ request.remote_ip
63
+ else
64
+ request.ip
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Celerbrake
4
+ module Rack
5
+ # Adds HTTP request parameters.
6
+ #
7
+ # @since v5.7.0
8
+ class HttpHeadersFilter
9
+ # @return [Array<String>] the prefixes of the majority of HTTP headers in
10
+ # Rack (some prefixes match the header names for simplicity)
11
+ HTTP_HEADER_PREFIXES = %w[
12
+ HTTP_
13
+ CONTENT_TYPE
14
+ CONTENT_LENGTH
15
+ ].freeze
16
+
17
+ # @return [Integer]
18
+ attr_reader :weight
19
+
20
+ def initialize
21
+ @weight = 98
22
+ end
23
+
24
+ # @see Celerbrake::FilterChain#refine
25
+ def call(notice)
26
+ return unless (request = notice.stash[:rack_request])
27
+
28
+ http_headers = request.env.map.with_object({}) do |(key, value), headers|
29
+ if HTTP_HEADER_PREFIXES.any? { |prefix| key.to_s.start_with?(prefix) }
30
+ headers[key] = value
31
+ end
32
+
33
+ headers
34
+ end
35
+
36
+ notice[:context].merge!(
37
+ httpMethod: request.request_method,
38
+ referer: request.referer,
39
+ headers: http_headers,
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Celerbrake
4
+ module Rack
5
+ # Adds HTTP request parameters.
6
+ #
7
+ # @since v5.7.0
8
+ class HttpParamsFilter
9
+ # @return [Integer]
10
+ attr_reader :weight
11
+
12
+ def initialize
13
+ @weight = 97
14
+ end
15
+
16
+ # @see Celerbrake::FilterChain#refine
17
+ def call(notice)
18
+ return unless (request = notice.stash[:rack_request])
19
+
20
+ notice[:params].merge!(request.params)
21
+
22
+ rails_params = request.env['action_dispatch.request.parameters']
23
+ notice[:params].merge!(rails_params) if rails_params
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Celerbrake
4
+ module Rack
5
+ # Instrumentable holds methods that simplify instrumenting Rack apps.
6
+ # @example
7
+ # class UsersController
8
+ # extend Celerbrake::Rack::Instrumentable
9
+ #
10
+ # def index
11
+ # # ...
12
+ # end
13
+ # celerbrake_capture_timing :index
14
+ # end
15
+ #
16
+ # @api public
17
+ # @since v9.2.0
18
+ module Instrumentable
19
+ def celerbrake_capture_timing(method_name, label: method_name.to_s)
20
+ instrumentable = ::Celerbrake::Rack::Instrumentable
21
+ if instrumentable.should_prepend?(self, method_name)
22
+ instrumentable.prepend_capture_timing(self, method_name, label)
23
+ else
24
+ instrumentable.chain_capture_timing(self, method_name, label)
25
+ end
26
+ method_name
27
+ end
28
+
29
+ # @api private
30
+ def __celerbrake_capture_timing_module__
31
+ # Module used to store prepended wrapper methods, saved as an instance
32
+ # variable so each target class/module gets its own module. This just
33
+ # a convenience to avoid prepending a lot of anonymous modules.
34
+ @__celerbrake_capture_timing_module__ ||= ::Module.new
35
+ end
36
+ private :__celerbrake_capture_timing_module__
37
+
38
+ # Using api private self methods so they don't get defined in the target
39
+ # class or module, but can still be called by the above method.
40
+
41
+ # @api private
42
+ def self.should_prepend?(klass, method_name)
43
+ # Don't chain already-prepended or operator methods.
44
+ klass.module_exec do
45
+ self_class_idx = ancestors.index(self)
46
+ method_owner_idx = ancestors.index(instance_method(method_name).owner)
47
+ method_owner_idx < self_class_idx || !(/\A\W/ =~ method_name).nil?
48
+ end
49
+ end
50
+
51
+ # @api private
52
+ def self.prepend_capture_timing(klass, method_name, label)
53
+ args = method_signature
54
+ visibility = method_visibility(klass, method_name)
55
+
56
+ # Generate the wrapper method.
57
+ klass.module_exec do
58
+ mod = __celerbrake_capture_timing_module__
59
+ mod.module_exec do
60
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
61
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
62
+ def #{method_name}(#{args})
63
+ Celerbrake::Rack.capture_timing(#{label.to_s.inspect}) do
64
+ super
65
+ end
66
+ end
67
+ #{visibility} :#{method_name}
68
+ RUBY
69
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
70
+ end
71
+ prepend mod
72
+ end
73
+ end
74
+
75
+ # @api private
76
+ def self.chain_capture_timing(klass, method_name, label)
77
+ args = method_signature
78
+ visibility = method_visibility(klass, method_name)
79
+
80
+ # Generate the wrapper method.
81
+ aliased = method_name.to_s.sub(/([?!=])$/, '')
82
+ punctuation = Regexp.last_match(1)
83
+ wrapped_method_name = "#{aliased}_without_celerbrake#{punctuation}"
84
+ needs_removal = method_needs_removal(klass, method_name)
85
+ klass.module_exec do
86
+ alias_method wrapped_method_name, method_name
87
+ remove_method method_name if needs_removal
88
+ # rubocop:disable Style/DocumentDynamicEvalDefinition
89
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
90
+ def #{method_name}(#{args})
91
+ Celerbrake::Rack.capture_timing(#{label.to_s.inspect}) do
92
+ __send__("#{aliased}_without_celerbrake#{punctuation}", #{args})
93
+ end
94
+ end
95
+ #{visibility} :#{method_name}
96
+ RUBY
97
+ # rubocop:enable Style/DocumentDynamicEvalDefinition
98
+ end
99
+ end
100
+
101
+ # @api private
102
+ def self.method_visibility(klass, method_name)
103
+ klass.module_exec do
104
+ if protected_method_defined?(method_name)
105
+ "protected"
106
+ elsif private_method_defined?(method_name)
107
+ "private"
108
+ else
109
+ "public"
110
+ end
111
+ end
112
+ end
113
+
114
+ # @api private
115
+ # A method instead of a constant so it isn't accessible in the target.
116
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7")
117
+ def self.method_signature
118
+ "*args, **kw_args, &block"
119
+ end
120
+ else
121
+ def self.method_signature
122
+ "*args, &block"
123
+ end
124
+ end
125
+
126
+ # @api private
127
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6")
128
+ def self.method_needs_removal(klass, method_name)
129
+ klass.method_defined?(method_name, false) ||
130
+ klass.private_method_defined?(method_name, false)
131
+ end
132
+ else
133
+ def self.method_needs_removal(klass, method_name)
134
+ klass.instance_methods(false).include?(method_name) ||
135
+ klass.private_instance_methods(false).include?(method_name)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Celerbrake
4
+ module Rack
5
+ # Celerbrake Rack middleware for Rails and Sinatra applications (or any other
6
+ # Rack-compliant app). Any errors raised by the upstream application will be
7
+ # delivered to Celerbrake and re-raised.
8
+ #
9
+ # The middleware automatically sends information about the framework that
10
+ # uses it (name and version).
11
+ #
12
+ # For Rails apps the middleware collects route performance statistics.
13
+ class Middleware
14
+ def initialize(app)
15
+ @app = app
16
+ end
17
+
18
+ # Thread-safe {call!}.
19
+ #
20
+ # @param [Hash] env the Rack environment
21
+ # @see https://github.com/celerbrake/celerbrake/issues/904
22
+ def call(env)
23
+ dup.call!(env)
24
+ end
25
+
26
+ # Rescues any exceptions, sends them to Celerbrake and re-raises the
27
+ # exception. We also duplicate middleware to guarantee thread-safety.
28
+ #
29
+ # @param [Hash] env the Rack environment
30
+ def call!(env)
31
+ before_call(env)
32
+
33
+ begin
34
+ response = @app.call(env)
35
+ rescue Exception => ex # rubocop:disable Lint/RescueException
36
+ notify_celerbrake(ex)
37
+ raise ex
38
+ end
39
+
40
+ exception = framework_exception(env)
41
+ notify_celerbrake(exception) if exception
42
+
43
+ response
44
+ ensure
45
+ # Clear routes for the next request.
46
+ RequestStore.clear
47
+ end
48
+
49
+ private
50
+
51
+ def before_call(env)
52
+ # Rails hooks such as ActionControllerRouteSubscriber rely on this.
53
+ RequestStore[:routes] = {}
54
+ RequestStore[:request] = find_request(env)
55
+ end
56
+
57
+ def find_request(env)
58
+ if defined?(ActionDispatch::Request)
59
+ ActionDispatch::Request.new(env)
60
+ elsif defined?(Sinatra::Request)
61
+ Sinatra::Request.new(env)
62
+ else
63
+ ::Rack::Request.new(env)
64
+ end
65
+ end
66
+
67
+ def notify_celerbrake(exception)
68
+ notice = Celerbrake.build_notice(exception)
69
+ return unless notice
70
+
71
+ # ActionDispatch::Request correctly captures server port when using SSL:
72
+ # See: https://github.com/celerbrake/celerbrake/issues/802
73
+ notice.stash[:rack_request] = RequestStore[:request]
74
+
75
+ Celerbrake.notify(notice)
76
+ end
77
+
78
+ # Web framework middlewares often store rescued exceptions inside the
79
+ # Rack env, but Rack doesn't have a standard key for it:
80
+ #
81
+ # - Rails uses action_dispatch.exception: https://goo.gl/Kd694n
82
+ # - Sinatra uses sinatra.error: https://goo.gl/LLkVL9
83
+ # - Goliath uses rack.exception: https://goo.gl/i7e1nA
84
+ def framework_exception(env)
85
+ env['action_dispatch.exception'] ||
86
+ env['sinatra.error'] ||
87
+ env['rack.exception']
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ [
94
+ Celerbrake::Rack::ContextFilter,
95
+ Celerbrake::Rack::UserFilter,
96
+ Celerbrake::Rack::SessionFilter,
97
+ Celerbrake::Rack::HttpParamsFilter,
98
+ Celerbrake::Rack::HttpHeadersFilter,
99
+ Celerbrake::Rack::RouteFilter,
100
+ ].each do |filter|
101
+ Celerbrake.add_filter(filter.new)
102
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Celerbrake
4
+ module Rack
5
+ # A filter that appends Rack request body to the notice.
6
+ #
7
+ # @example
8
+ # # Read and append up to 512 bytes from Rack request's body.
9
+ # Celerbrake.add_filter(Celerbrake::Rack::RequestBodyFilter.new(512))
10
+ #
11
+ # @since v5.7.0
12
+ # @note This filter is *not* used by default.
13
+ class RequestBodyFilter
14
+ # @return [Integer]
15
+ attr_reader :weight
16
+
17
+ # @param [Integer] length The maximum number of bytes to read
18
+ def initialize(length = 4096)
19
+ @length = length
20
+ @weight = 95
21
+ end
22
+
23
+ # @see Celerbrake::FilterChain#refine
24
+ def call(notice)
25
+ return unless (request = notice.stash[:rack_request])
26
+ return unless request.body
27
+
28
+ notice[:environment][:body] = request.body.read(@length)
29
+ request.body.rewind
30
+ end
31
+ end
32
+ end
33
+ end