sapience 0.2.4 → 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +21 -0
  3. data/.simplecov +19 -16
  4. data/.travis.yml +3 -3
  5. data/CHANGELOG.md +4 -0
  6. data/Gemfile +0 -8
  7. data/README.md +5 -0
  8. data/config/default.yml +4 -0
  9. data/docker-compose.yml +14 -0
  10. data/lib/sapience/appender/stream.rb +0 -1
  11. data/lib/sapience/base.rb +34 -4
  12. data/lib/sapience/configuration.rb +41 -20
  13. data/lib/sapience/core_ext/hash.rb +10 -7
  14. data/lib/sapience/core_ext/symbol.rb +15 -0
  15. data/lib/sapience/core_ext/thread.rb +1 -0
  16. data/lib/sapience/extensions/action_cable/tagged_logger_proxy.rb +2 -0
  17. data/lib/sapience/extensions/action_controller/live.rb +2 -0
  18. data/lib/sapience/extensions/action_controller/log_subscriber.rb +76 -121
  19. data/lib/sapience/extensions/action_dispatch/debug_exceptions.rb +2 -0
  20. data/lib/sapience/extensions/action_view/log_subscriber.rb +16 -6
  21. data/lib/sapience/extensions/action_view/streaming_template_renderer.rb +2 -0
  22. data/lib/sapience/extensions/active_job/logging.rb +1 -1
  23. data/lib/sapience/extensions/active_model_serializers/logging.rb +7 -2
  24. data/lib/sapience/extensions/active_record/log_subscriber.rb +45 -29
  25. data/lib/sapience/extensions/rails/rack/logger.rb +1 -0
  26. data/lib/sapience/extensions/rails/rack/logger_info_as_debug.rb +2 -0
  27. data/lib/sapience/formatters/color.rb +0 -1
  28. data/lib/sapience/formatters/default.rb +0 -1
  29. data/lib/sapience/formatters/json.rb +0 -1
  30. data/lib/sapience/formatters/raw.rb +0 -1
  31. data/lib/sapience/log.rb +54 -35
  32. data/lib/sapience/logger.rb +50 -71
  33. data/lib/sapience/rails.rb +17 -20
  34. data/lib/sapience/sapience.rb +23 -27
  35. data/lib/sapience/subscriber.rb +1 -13
  36. data/lib/sapience/version.rb +1 -1
  37. data/lib/sapience.rb +4 -3
  38. data/sapience.gemspec +6 -1
  39. data/test_app/Gemfile +7 -0
  40. data/test_app/Rakefile +2 -0
  41. data/test_app/app/models/post.rb +1 -1
  42. data/test_app/app/views/posts/_form.html.slim +18 -0
  43. data/test_app/app/views/posts/edit.html.slim +8 -0
  44. data/test_app/app/views/posts/index.html.slim +25 -0
  45. data/test_app/app/views/posts/new.html.slim +5 -0
  46. data/test_app/app/views/posts/show.html.slim +15 -0
  47. data/test_app/app/workers/test_worker.rb +17 -0
  48. data/test_app/bin/sneakers +10 -0
  49. data/test_app/config/initializers/sneakers.rb +15 -0
  50. data/test_app/config/sapience_example.yml +24 -0
  51. data/test_app/db/migrate/{20160812093621_create_posts.rb → 20160902141445_create_posts.rb} +1 -1
  52. data/test_app/db/schema.rb +1 -1
  53. data/test_app/lib/external_sneaker.rb +46 -0
  54. data/test_app/spec/factories/posts.rb +7 -0
  55. data/test_app/spec/factories/users.rb +8 -0
  56. data/test_app/spec/rails_helper.rb +8 -3
  57. data/test_app/spec/requests/posts_spec.rb +2 -1
  58. data/test_app/spec/views/posts/edit.html.slim_spec.rb +17 -0
  59. data/test_app/spec/views/posts/index.html.slim_spec.rb +17 -0
  60. data/test_app/spec/views/posts/new.html.slim_spec.rb +17 -0
  61. data/test_app/spec/views/posts/show.html.slim_spec.rb +14 -0
  62. data/test_app/spec/workers/test_worker_spec.rb +36 -0
  63. data/test_app.simplecov +19 -0
  64. metadata +95 -15
  65. data/.coveralls.yml +0 -1
  66. data/lib/sapience/extensions/action_controller/log_subscriber_processing.rb +0 -24
  67. data/test_app/app/views/posts/_form.html.erb +0 -32
  68. data/test_app/app/views/posts/create.html.erb +0 -2
  69. data/test_app/app/views/posts/destroy.html.erb +0 -2
  70. data/test_app/app/views/posts/edit.html.erb +0 -6
  71. data/test_app/app/views/posts/index.html.erb +0 -31
  72. data/test_app/app/views/posts/new.html.erb +0 -5
  73. data/test_app/app/views/posts/show.html.erb +0 -19
  74. data/test_app/app/views/posts/update.html.erb +0 -2
@@ -1,127 +1,82 @@
1
1
  require "action_controller/log_subscriber"
2
2
 
3
- class ActionController::LogSubscriber # rubocop:disable ClassAndModuleChildren
4
- # Log as debug to hide Processing messages in production
5
- def start_processing(event)
6
- controller_logger(event).debug { "Processing ##{event.payload[:action]}" }
7
- end
8
-
9
- def process_action(event) # rubocop:disable AbcSize, CyclomaticComplexity, PerceivedComplexity
10
- controller_logger(event).info do
11
- payload = event.payload.dup
12
- payload[:params].except!(*INTERNAL_PARAMS)
13
- payload.delete(:params) if payload[:params].empty?
14
-
15
- format = payload[:format]
16
- payload[:format] = format.to_s.upcase if format.is_a?(Symbol)
17
-
18
- payload[:path] = extract_path(payload[:path]) if payload.key?(:path)
19
-
20
- exception = payload.delete(:exception)
21
- if payload[:status].nil? && exception.present?
22
- exception_class_name = exception.first
23
- payload[:status] = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
24
- end
25
-
26
- # Rounds off the runtimes. For example, :view_runtime, :mongo_runtime, etc.
27
- payload.keys.each do |key|
28
- payload[key] = payload[key].to_f.round(2) if key.to_s.match(/(.*)_runtime/)
3
+ module Sapience
4
+ module Extensions
5
+ module ActionController
6
+ class LogSubscriber < ::ActionController::LogSubscriber # rubocop:disable ClassLength
7
+ alias_method :orig_start_processing, :start_processing
8
+ alias_method :orig_process_action, :process_action
9
+
10
+ # Log as debug to hide Processing messages in production
11
+ def start_processing(event)
12
+ debug { "Processing ##{event.payload[:action]}" }
13
+ end
14
+
15
+ def process_action(event) # rubocop:disable AbcSize, CyclomaticComplexity, PerceivedComplexity
16
+ return unless logger.info?
17
+ data = request(event.payload)
18
+ data.merge! runtimes(event)
19
+ data.merge! exception(event.payload)
20
+ info(data)
21
+ end
22
+
23
+ private
24
+
25
+ def request(payload) # rubocop:disable AbcSize
26
+ {
27
+ method: payload[:method].upcase,
28
+ request_path: request_path(payload),
29
+ format: format(payload),
30
+ status: payload[:status].to_i,
31
+ controller: payload[:params]["controller"],
32
+ action: payload[:params]["action"],
33
+ host: Sapience.config.host,
34
+ route: "#{payload[:params].delete("controller")}##{payload[:params]["action"]}",
35
+ message: "Completed ##{payload[:params].delete("action")}",
36
+ tags: Sapience.tags,
37
+ params: payload[:params],
38
+ }
39
+ end
40
+
41
+ def format(payload)
42
+ if ::ActionPack::VERSION::MAJOR == 3 && ::ActionPack::VERSION::MINOR == 0
43
+ payload[:formats].first
44
+ else
45
+ payload[:format]
46
+ end
47
+ end
48
+
49
+ def runtimes(event)
50
+ {
51
+ total: event.duration,
52
+ view: event.payload[:view_runtime],
53
+ db: event.payload[:db_runtime],
54
+ }.each_with_object({}) do |(name, runtime), runtimes|
55
+ runtimes[:runtimes] ||= {}
56
+ runtimes[:runtimes][name] = runtime.to_f.round(2) if runtime
57
+ runtimes
58
+ end
59
+ end
60
+
61
+ # Monkey patching to enable exception logging
62
+ def exception(payload)
63
+ if payload[:exception]
64
+ exception, message = payload[:exception]
65
+ message ||= exception.message
66
+ status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception)
67
+ backtrace = $ERROR_INFO.try(:backtrace).try(:first)
68
+ backtrace ||= exception.backtrace.first
69
+ message = "#{exception}\n#{message}\n#{backtrace}"
70
+ { status: status, error: message }
71
+ else
72
+ {}
73
+ end
74
+ end
75
+
76
+ def request_path(payload)
77
+ payload[:path].split("?").first
78
+ end
29
79
  end
30
-
31
- payload[:message] = "Completed ##{payload[:action]}"
32
- payload[:status_message] = Rack::Utils::HTTP_STATUS_CODES[payload[:status]] if payload[:status].present?
33
- payload[:duration] = event.duration
34
- # Causes excessive log output with Rails 5 RC1
35
- payload.delete(:headers)
36
- payload
37
- end
38
- end
39
-
40
- def halted_callback(event)
41
- controller_logger(event).info do
42
- "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected"
43
- end
44
- end
45
-
46
- def send_file(event)
47
- controller_logger(event).info("Sent file") { { path: event.payload[:path], duration: event.duration } }
48
- end
49
-
50
- def redirect_to(event)
51
- controller_logger(event).info("Redirected to") { { location: event.payload[:location] } }
52
- end
53
-
54
- def send_data(event)
55
- controller_logger(event).info("Sent data") { { file_name: event.payload[:filename], duration: event.duration } }
56
- end
57
-
58
- def unpermitted_parameters(event)
59
- controller_logger(event).debug do
60
- unpermitted_keys = event.payload[:keys]
61
- "Unpermitted parameter#{"s" if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}"
62
- end
63
- end
64
-
65
- private
66
-
67
- # Returns the logger for the supplied event.
68
- # Returns ActionController::Base.logger if no controller is present
69
- def controller_logger(event)
70
- if (controller = event.payload[:controller])
71
- begin
72
- controller.constantize.logger
73
- rescue NameError
74
- ActionController::Base.logger
75
- end
76
- else
77
- ActionController::Base.logger
78
- end
79
- end
80
-
81
- def extract_path(path)
82
- index = path.index("?")
83
- index ? path[0, index] : path
84
- end
85
-
86
- def write_fragment(event)
87
- controller_logger(event).info do
88
- key_or_path = event.payload[:key] || event.payload[:path]
89
- { message: "Write fragment #{key_or_path}", duration: event.duration }
90
- end
91
- end
92
-
93
- def read_fragment(event)
94
- controller_logger(event).info do
95
- key_or_path = event.payload[:key] || event.payload[:path]
96
- { message: "Read fragment #{key_or_path}", duration: event.duration }
97
- end
98
- end
99
-
100
- def exist_fragment(event)
101
- controller_logger(event).info do
102
- key_or_path = event.payload[:key] || event.payload[:path]
103
- { message: "Exist fragment #{key_or_path}", duration: event.duration }
104
- end
105
- end
106
-
107
- def expire_fragment(event)
108
- controller_logger(event).info do
109
- key_or_path = event.payload[:key] || event.payload[:path]
110
- { message: "Expire fragment #{key_or_path}", duration: event.duration }
111
- end
112
- end
113
-
114
- def expire_page(event)
115
- controller_logger(event).info do
116
- key_or_path = event.payload[:key] || event.payload[:path]
117
- { message: "Expire page #{key_or_path}", duration: event.duration }
118
- end
119
- end
120
-
121
- def write_page(event)
122
- controller_logger(event).info do
123
- key_or_path = event.payload[:key] || event.payload[:path]
124
- { message: "Write page #{key_or_path}", duration: event.duration }
125
80
  end
126
81
  end
127
82
  end
@@ -3,6 +3,8 @@
3
3
  class ActionDispatch::DebugExceptions # rubocop:disable ClassAndModuleChildren
4
4
  private
5
5
 
6
+ alias_method :orig_log_error, :log_error
7
+
6
8
  def log_error(_request, wrapper)
7
9
  ActiveSupport::Deprecation.silence do
8
10
  ActionController::Base.logger.fatal(wrapper.exception)
@@ -1,9 +1,19 @@
1
- class ActionView::LogSubscriber # rubocop:disable ClassAndModuleChildren
2
- def info(message = nil, &block)
3
- debug(message, &block)
4
- end
1
+ require "action_view/log_subscriber"
2
+
3
+ module Sapience
4
+ module Extensions
5
+ module ActionView
6
+ class LogSubscriber < ::ActionView::LogSubscriber
7
+ include Sapience::Loggable
8
+
9
+ def info(message = nil, &block)
10
+ logger.debug(message, &block)
11
+ end
5
12
 
6
- def info?
7
- debug?
13
+ def info?
14
+ logger.debug?
15
+ end
16
+ end
17
+ end
8
18
  end
9
19
  end
@@ -4,6 +4,8 @@ class ActionView::StreamingTemplateRenderer # rubocop:disable ClassAndModuleChil
4
4
  class Body
5
5
  private
6
6
 
7
+ alias_method :log_error_original, :log_error
8
+
7
9
  def log_error(exception) #:nodoc:
8
10
  ActionView::Base.logger.fatal(exception)
9
11
  end
@@ -6,7 +6,7 @@ module ActiveJob::Logging # rubocop:disable ClassAndModuleChildren
6
6
 
7
7
  private
8
8
 
9
- alias_method :tag_logger_old, :tag_logger
9
+ alias_method :orig_tag_logger, :tag_logger
10
10
 
11
11
  def tag_logger(*tags, &block)
12
12
  logger.tagged(*tags, &block)
@@ -1,14 +1,19 @@
1
1
  # Patch ActiveModelSerializers logger
2
+ require "active_model_serializers"
2
3
  require "active_model_serializers/logging"
3
4
 
4
5
  module ActiveModelSerializers::Logging # rubocop:disable ClassAndModuleChildren
5
- include Sapience::Loggable
6
+ def self.included(base)
7
+ base.send(:include, Sapience::Loggable)
8
+ end
6
9
 
7
10
  private
8
11
 
9
- alias_method :tag_logger_old, :tag_logger
12
+ alias_method :orig_tag_logger, :tag_logger
10
13
 
11
14
  def tag_logger(*tags, &block)
12
15
  logger.tagged(*tags, &block)
13
16
  end
14
17
  end
18
+
19
+ ActiveModelSerializers.send(:include, Sapience::Loggable)
@@ -1,35 +1,51 @@
1
- ActiveRecord::LogSubscriber # rubocop:disable Lint/Void
2
-
3
- class ActiveRecord::LogSubscriber # rubocop:disable ClassAndModuleChildren
4
- def sql(event) # rubocop:disable AbcSize
5
- self.class.runtime += event.duration
6
-
7
- return unless logger.debug?
8
-
9
- payload = event.payload
10
- name = payload[:name]
11
- return if IGNORE_PAYLOAD_NAMES.include?(name)
12
-
13
- log = {
14
- message: name,
15
- sql: payload[:sql],
16
- duration: event.duration,
17
- }
18
- unless (payload[:binds] || []).empty?
19
- log[:binds] = binds = {}
20
- # Changed with Rails 5
21
- if Rails.version.to_i >= 5
22
- payload[:binds].each do |attr|
23
- attr_name, value = render_bind(attr)
24
- binds[attr_name] = value
1
+ require "active_support/notifications"
2
+ require "active_record/log_subscriber"
3
+
4
+ module Sapience
5
+ module Extensions
6
+ module ActiveRecord
7
+ class LogSubscriber < ::ActiveRecord::LogSubscriber
8
+ include Sapience::Loggable
9
+
10
+ def identity(event)
11
+ lsevent = logstash_event(event)
12
+ logger << lsevent.to_json + "\n" if logger && lsevent
25
13
  end
26
- else
27
- payload[:binds].each do |col, v|
28
- attr_name, value = render_bind(col, v)
29
- binds[attr_name] = value
14
+ alias_method :sql, :identity
15
+
16
+ private
17
+
18
+ def logstash_event(event)
19
+ data = event.payload
20
+
21
+ return if "SCHEMA" == data[:name]
22
+
23
+ data.merge! runtimes(event)
24
+ data.merge! extract_sql(data)
25
+
26
+ data.merge! tags(data)
27
+ debug(data)
28
+ end
29
+
30
+ def runtimes(event)
31
+ if event.duration
32
+ { duration: event.duration.to_f.round(2) }
33
+ else
34
+ {}
35
+ end
36
+ end
37
+
38
+ def extract_sql(data)
39
+ { sql: data[:sql].squeeze(" ") }
40
+ end
41
+
42
+ def tags(data)
43
+ tags = Sapience.tags.dup
44
+ tags.push("request")
45
+ tags.push("exception") if data[:exception]
46
+ { tags: tags }
30
47
  end
31
48
  end
32
49
  end
33
- debug(log)
34
50
  end
35
51
  end
@@ -1,5 +1,6 @@
1
1
  # Replace rack started message with a semantic equivalent
2
2
  class Rails::Rack::Logger # rubocop:disable ClassAndModuleChildren
3
+ alias_method :started_request_message_original, :started_request_message
3
4
  def started_request_message(request)
4
5
  {
5
6
  message: "Started",
@@ -13,6 +13,8 @@ class Rails::Rack::Logger # rubocop:disable ClassAndModuleChildren
13
13
  end
14
14
  end
15
15
 
16
+ alias_method :orig_logger, :logger
17
+
16
18
  def logger
17
19
  @logger ||= begin
18
20
  logger = Sapience["Rails"]
@@ -62,7 +62,6 @@ module Sapience
62
62
  end
63
63
  message
64
64
  end
65
-
66
65
  end
67
66
  end
68
67
  end
@@ -35,7 +35,6 @@ module Sapience
35
35
  end
36
36
  message
37
37
  end
38
-
39
38
  end
40
39
  end
41
40
  end
@@ -16,7 +16,6 @@ module Sapience
16
16
  h[:timestamp] = format_time(log.time)
17
17
  h.to_json
18
18
  end
19
-
20
19
  end
21
20
  end
22
21
  end
@@ -6,7 +6,6 @@ module Sapience
6
6
  def call(log, logger)
7
7
  log.to_h(log_host ? logger.host : nil, log_application ? logger.application : nil)
8
8
  end
9
-
10
9
  end
11
10
  end
12
11
  end
data/lib/sapience/log.rb CHANGED
@@ -38,7 +38,7 @@ module Sapience
38
38
  # Object supplied when measure_x was called
39
39
  #
40
40
  # backtrace [Array<String>]
41
- # The backtrace captured at source when the log level >= Sapience.config.backtrace_level
41
+ # The backtrace_level captured at source when the log level >= Sapience.config.backtrace_level
42
42
  #
43
43
  # metric_amount [Numeric]
44
44
  # Used for numeric or counter metrics.
@@ -47,34 +47,17 @@ module Sapience
47
47
  # rubocop:disable LineLength
48
48
  Log = Struct.new(:level, :thread_name, :name, :message, :payload, :time, :duration, :tags, :level_index, :exception, :metric, :backtrace, :metric_amount) do
49
49
  MAX_EXCEPTIONS_TO_UNWRAP = 5
50
- # Call the block for exception and any nested exception
51
- def each_exception # rubocop:disable AbcSize, PerceivedComplexity, CyclomaticComplexity
52
- # With thanks to https://github.com/bugsnag/bugsnag-ruby/blob/6348306e44323eee347896843d16c690cd7c4362/lib/bugsnag/notification.rb#L81
53
- depth = 0
54
- exceptions = []
55
- ex = exception
56
- while !ex.nil? && !exceptions.include?(ex) && exceptions.length < MAX_EXCEPTIONS_TO_UNWRAP
57
- exceptions << ex
58
- yield(ex, depth)
59
-
60
- depth += 1
61
- ex =
62
- if ex.respond_to?(:cause) && ex.cause
63
- ex.cause
64
- elsif ex.respond_to?(:continued_exception) && ex.continued_exception
65
- ex.continued_exception
66
- elsif ex.respond_to?(:original_exception) && ex.original_exception
67
- ex.original_exception
68
- end
69
- end
70
- end
50
+ MILLISECONDS_IN_SECOND = 1_000
51
+ MILLISECONDS_IN_MINUTE = 60_000
52
+ MILLISECONDS_IN_HOUR = 3_600_000
53
+ MILLISECONDS_IN_DAY = 86_400_000
71
54
 
72
55
  # Returns [String] the exception backtrace including all of the child / caused by exceptions
73
56
  def backtrace_to_s
74
57
  trace = ""
75
58
  each_exception do |exception, i|
76
59
  if i == 0
77
- trace = (exception.backtrace || []).join("\n")
60
+ trace << (exception.backtrace || []).join("\n")
78
61
  else
79
62
  trace << "\nCause: #{exception.class.name}: #{exception.message}\n#{(exception.backtrace || []).join("\n")}"
80
63
  end
@@ -90,19 +73,28 @@ module Sapience
90
73
  end
91
74
 
92
75
  # Returns [String] the duration in human readable form
93
- def duration_human # rubocop:disable AbcSize
76
+ def duration_human # rubocop:disable AbcSize, CyclomaticComplexity, PerceivedComplexity
94
77
  return nil unless duration
95
- seconds = duration / 1000
96
- if seconds >= 86_400.0 # 1 day
97
- "#{(seconds / 86_400).to_i}d #{Time.at(seconds).strftime("%-Hh %-Mm")}"
98
- elsif seconds >= 3600.0 # 1 hour
99
- Time.at(seconds).strftime("%-Hh %-Mm")
100
- elsif seconds >= 60.0 # 1 minute
101
- Time.at(seconds).strftime("%-Mm %-Ss")
102
- elsif seconds >= 1.0 # 1 second
103
- format "%.3fs", seconds
78
+ days, ms = duration.divmod(MILLISECONDS_IN_DAY)
79
+ hours, ms = ms.divmod(MILLISECONDS_IN_HOUR)
80
+ minutes, ms = ms.divmod(MILLISECONDS_IN_MINUTE)
81
+ seconds, ms = ms.divmod(MILLISECONDS_IN_SECOND)
82
+
83
+ str = ""
84
+ str << "#{days}d" if days > 0
85
+ str << " #{hours}h" if hours > 0
86
+ str << " #{minutes}m" if minutes > 0
87
+ str << " #{seconds}s" if seconds > 0
88
+ str << " #{ms}ms" if ms > 0
89
+
90
+ if days > 0 || hours > 0 || minutes > 0
91
+ str.strip
104
92
  else
105
- duration_to_s
93
+ if seconds >= 1.0
94
+ format "%.3fs", duration / MILLISECONDS_IN_SECOND.to_f
95
+ else
96
+ duration_to_s
97
+ end
106
98
  end
107
99
  end
108
100
 
@@ -210,7 +202,7 @@ module Sapience
210
202
  message: exception.message,
211
203
  stack_trace: exception.backtrace,
212
204
  }
213
- root = root[name]
205
+ root = root[name]
214
206
  end
215
207
  end
216
208
 
@@ -218,6 +210,33 @@ module Sapience
218
210
  h[:metric] = metric if metric
219
211
  h
220
212
  end
213
+
214
+ private
215
+
216
+ # Call the block for exception and any nested exception
217
+ def each_exception # rubocop:disable AbcSize, PerceivedComplexity, CyclomaticComplexity
218
+ # With thanks to https://github.com/bugsnag/bugsnag-ruby/blob/6348306e44323eee347896843d16c690cd7c4362/lib/bugsnag/notification.rb#L81
219
+ depth = 0
220
+ exceptions = []
221
+ ex = exception
222
+ while !ex.nil? && !exceptions.include?(ex) && exceptions.length < MAX_EXCEPTIONS_TO_UNWRAP
223
+ exceptions << ex
224
+ yield(ex, depth)
225
+
226
+ depth += 1
227
+ ex =
228
+ # continued_exception is only used by REXML
229
+ # original_exception is deprecated in Rails 5+
230
+ # Not worth testing to thoroughly?
231
+ if ex.respond_to?(:cause) && ex.cause
232
+ ex.cause
233
+ elsif ex.respond_to?(:continued_exception) && ex.continued_exception
234
+ ex.continued_exception
235
+ elsif ex.respond_to?(:original_exception) && ex.original_exception
236
+ ex.original_exception
237
+ end
238
+ end
239
+ end
221
240
  end
222
241
  # rubocop:enable LineLength
223
242
  end