appsignal 3.9.3 → 3.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +22 -19
  3. data/CHANGELOG.md +92 -0
  4. data/README.md +0 -1
  5. data/Rakefile +1 -1
  6. data/build_matrix.yml +10 -12
  7. data/gemfiles/webmachine1.gemfile +5 -4
  8. data/lib/appsignal/config.rb +4 -0
  9. data/lib/appsignal/environment.rb +6 -1
  10. data/lib/appsignal/helpers/instrumentation.rb +163 -1
  11. data/lib/appsignal/hooks/active_job.rb +1 -6
  12. data/lib/appsignal/integrations/padrino.rb +21 -25
  13. data/lib/appsignal/integrations/rake.rb +46 -12
  14. data/lib/appsignal/integrations/sidekiq.rb +1 -11
  15. data/lib/appsignal/integrations/webmachine.rb +15 -9
  16. data/lib/appsignal/rack/abstract_middleware.rb +49 -12
  17. data/lib/appsignal/rack/body_wrapper.rb +143 -0
  18. data/lib/appsignal/rack/generic_instrumentation.rb +5 -4
  19. data/lib/appsignal/rack/grape_middleware.rb +1 -1
  20. data/lib/appsignal/rack/hanami_middleware.rb +1 -1
  21. data/lib/appsignal/rack/instrumentation_middleware.rb +62 -0
  22. data/lib/appsignal/rack/rails_instrumentation.rb +1 -3
  23. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -3
  24. data/lib/appsignal/rack/streaming_listener.rb +13 -59
  25. data/lib/appsignal/rack.rb +31 -0
  26. data/lib/appsignal/transaction.rb +50 -8
  27. data/lib/appsignal/version.rb +1 -1
  28. data/lib/appsignal.rb +3 -1
  29. data/spec/lib/appsignal/config_spec.rb +1 -0
  30. data/spec/lib/appsignal/hooks/rake_spec.rb +100 -17
  31. data/spec/lib/appsignal/integrations/padrino_spec.rb +181 -131
  32. data/spec/lib/appsignal/integrations/sinatra_spec.rb +10 -2
  33. data/spec/lib/appsignal/integrations/webmachine_spec.rb +65 -17
  34. data/spec/lib/appsignal/rack/abstract_middleware_spec.rb +96 -8
  35. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +263 -0
  36. data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +70 -17
  37. data/spec/lib/appsignal/rack/grape_middleware_spec.rb +1 -1
  38. data/spec/lib/appsignal/rack/instrumentation_middleware_spec.rb +38 -0
  39. data/spec/lib/appsignal/rack/streaming_listener_spec.rb +43 -120
  40. data/spec/lib/appsignal/transaction_spec.rb +163 -4
  41. data/spec/lib/appsignal_spec.rb +197 -6
  42. data/spec/support/mocks/dummy_app.rb +1 -1
  43. metadata +8 -4
  44. data/support/check_versions +0 -22
@@ -2,26 +2,60 @@
2
2
 
3
3
  module Appsignal
4
4
  module Integrations
5
+ # @api private
5
6
  module RakeIntegration
6
7
  def execute(*args)
7
- super
8
+ transaction =
9
+ if Appsignal.config[:enable_rake_performance_instrumentation]
10
+ Appsignal::Integrations::RakeIntegrationHelper.register_at_exit_hook
11
+ _appsignal_create_transaction
12
+ end
13
+
14
+ Appsignal.instrument "task.rake" do
15
+ super
16
+ end
8
17
  rescue Exception => error # rubocop:disable Lint/RescueException
9
- # Format given arguments and cast to hash if possible
10
- params, _ = args
11
- params = params.to_hash if params.respond_to?(:to_hash)
18
+ Appsignal::Integrations::RakeIntegrationHelper.register_at_exit_hook
19
+ transaction ||= _appsignal_create_transaction
20
+ transaction.set_error(error)
21
+ raise error
22
+ ensure
23
+ if transaction
24
+ # Format given arguments and cast to hash if possible
25
+ params, _ = args
26
+ params = params.to_hash if params.respond_to?(:to_hash)
27
+ transaction.set_params_if_nil(params)
28
+ transaction.set_action(name)
29
+ transaction.complete
30
+ end
31
+ end
12
32
 
13
- transaction = Appsignal::Transaction.create(
33
+ private
34
+
35
+ def _appsignal_create_transaction
36
+ Appsignal::Transaction.create(
14
37
  SecureRandom.uuid,
15
38
  Appsignal::Transaction::BACKGROUND_JOB,
16
- Appsignal::Transaction::GenericRequest.new(
17
- :params => params
18
- )
39
+ Appsignal::Transaction::GenericRequest.new({})
19
40
  )
20
- transaction.set_action(name)
21
- transaction.set_error(error)
22
- transaction.complete
41
+ end
42
+ end
43
+
44
+ # @api private
45
+ module RakeIntegrationHelper
46
+ # Register an `at_exit` hook when a task is executed. This will stop
47
+ # AppSignal when _all_ tasks are executed and Rake exits.
48
+ def self.register_at_exit_hook
49
+ return if @register_at_exit_hook
50
+
51
+ Kernel.at_exit(&method(:at_exit_hook))
52
+
53
+ @register_at_exit_hook = true
54
+ end
55
+
56
+ # The at_exit hook itself
57
+ def self.at_exit_hook
23
58
  Appsignal.stop("rake")
24
- raise error
25
59
  end
26
60
  end
27
61
  end
@@ -83,7 +83,7 @@ module Appsignal
83
83
  raise exception
84
84
  ensure
85
85
  if transaction
86
- transaction.set_params_if_nil(filtered_arguments(item))
86
+ transaction.set_params_if_nil(parse_arguments(item))
87
87
  transaction.set_http_or_background_queue_start
88
88
  Appsignal::Transaction.complete_current! unless exception
89
89
 
@@ -115,16 +115,6 @@ module Appsignal
115
115
  "#{sidekiq_action_name}#perform"
116
116
  end
117
117
 
118
- def filtered_arguments(job)
119
- arguments = parse_arguments(job)
120
- return unless arguments
121
-
122
- Appsignal::Utils::HashSanitizer.sanitize(
123
- arguments,
124
- Appsignal.config[:filter_parameters]
125
- )
126
- end
127
-
128
118
  def formatted_metadata(item)
129
119
  {}.tap do |hash|
130
120
  (item || {}).each do |key, value|
@@ -5,20 +5,26 @@ module Appsignal
5
5
  # @api private
6
6
  module WebmachineIntegration
7
7
  def run
8
- transaction = Appsignal::Transaction.create(
9
- SecureRandom.uuid,
10
- Appsignal::Transaction::HTTP_REQUEST,
11
- request,
12
- :params_method => :query
13
- )
14
-
15
- transaction.set_action_if_nil("#{resource.class.name}##{request.method}")
8
+ has_parent_transaction = Appsignal::Transaction.current?
9
+ transaction =
10
+ if has_parent_transaction
11
+ Appsignal::Transaction.current
12
+ else
13
+ Appsignal::Transaction.create(
14
+ SecureRandom.uuid,
15
+ Appsignal::Transaction::HTTP_REQUEST,
16
+ request
17
+ )
18
+ end
16
19
 
17
20
  Appsignal.instrument("process_action.webmachine") do
18
21
  super
19
22
  end
23
+ ensure
24
+ transaction.set_action_if_nil("#{resource.class.name}##{request.method}")
25
+ transaction.set_params_if_nil(request.query)
20
26
 
21
- Appsignal::Transaction.complete_current!
27
+ Appsignal::Transaction.complete_current! unless has_parent_transaction
22
28
  end
23
29
 
24
30
  private
@@ -4,6 +4,11 @@ require "rack"
4
4
 
5
5
  module Appsignal
6
6
  module Rack
7
+ # Base instrumentation middleware.
8
+ #
9
+ # Do not use this middleware directly. Instead use
10
+ # {InstrumentationMiddleware}.
11
+ #
7
12
  # @api private
8
13
  class AbstractMiddleware
9
14
  DEFAULT_ERROR_REPORTING = :default
@@ -14,7 +19,7 @@ module Appsignal
14
19
  @options = options
15
20
  @request_class = options.fetch(:request_class, ::Rack::Request)
16
21
  @params_method = options.fetch(:params_method, :params)
17
- @instrument_span_name = options.fetch(:instrument_span_name, "process.abstract")
22
+ @instrument_event_name = options.fetch(:instrument_event_name, nil)
18
23
  @report_errors = options.fetch(:report_errors, DEFAULT_ERROR_REPORTING)
19
24
  end
20
25
 
@@ -53,7 +58,7 @@ module Appsignal
53
58
  wrapped_instrumentation
54
59
  )
55
60
  else
56
- instrument_app_call(request.env)
61
+ instrument_app_call(request.env, transaction)
57
62
  end
58
63
  ensure
59
64
  add_transaction_metadata_after(transaction, request)
@@ -72,20 +77,32 @@ module Appsignal
72
77
  # don't report any exceptions here, the top instrumentation middleware
73
78
  # will be the one reporting the exception.
74
79
  #
75
- # Either another {GenericInstrumentation} or {EventHandler} is higher in
76
- # the stack and will report the exception and complete the transaction.
80
+ # Either another {AbstractMiddleware} or {EventHandler} is higher in the
81
+ # stack and will report the exception and complete the transaction.
77
82
  #
78
83
  # @see {#instrument_app_call_with_exception_handling}
79
- def instrument_app_call(env)
80
- if @instrument_span_name
81
- Appsignal.instrument(@instrument_span_name) do
82
- @app.call(env)
84
+ def instrument_app_call(env, transaction)
85
+ if @instrument_event_name
86
+ Appsignal.instrument(@instrument_event_name) do
87
+ call_app(env, transaction)
83
88
  end
84
89
  else
85
- @app.call(env)
90
+ call_app(env, transaction)
86
91
  end
87
92
  end
88
93
 
94
+ def call_app(env, transaction)
95
+ status, headers, obody = @app.call(env)
96
+ body =
97
+ if obody.is_a? Appsignal::Rack::BodyWrapper
98
+ obody
99
+ else
100
+ # Instrument response body and closing of the response body
101
+ Appsignal::Rack::BodyWrapper.wrap(obody, transaction)
102
+ end
103
+ [status, headers, body]
104
+ end
105
+
89
106
  # Instrument the request fully. This is used by the top instrumentation
90
107
  # middleware in the middleware stack. Unlike
91
108
  # {#instrument_app_call} this will report any exceptions being
@@ -93,7 +110,7 @@ module Appsignal
93
110
  #
94
111
  # @see {#instrument_app_call}
95
112
  def instrument_app_call_with_exception_handling(env, transaction, wrapped_instrumentation)
96
- instrument_app_call(env)
113
+ instrument_app_call(env, transaction)
97
114
  rescue Exception => error # rubocop:disable Lint/RescueException
98
115
  report_errors =
99
116
  if @report_errors == DEFAULT_ERROR_REPORTING
@@ -122,14 +139,14 @@ module Appsignal
122
139
  # Call `super` to also include the default set metadata.
123
140
  def add_transaction_metadata_after(transaction, request)
124
141
  default_action =
125
- request.env["appsignal.route"] || request.env["appsignal.action"]
142
+ appsignal_route_env_value(request) || appsignal_action_env_value(request)
126
143
  transaction.set_action_if_nil(default_action)
127
144
  transaction.set_metadata("path", request.path)
128
145
 
129
146
  request_method = request_method_for(request)
130
147
  transaction.set_metadata("method", request_method) if request_method
131
148
 
132
- transaction.set_params_if_nil(params_for(request))
149
+ transaction.set_params_if_nil { params_for(request) }
133
150
  transaction.set_http_or_background_queue_start
134
151
  end
135
152
 
@@ -155,6 +172,26 @@ module Appsignal
155
172
  def request_for(env)
156
173
  @request_class.new(env)
157
174
  end
175
+
176
+ def appsignal_route_env_value(request)
177
+ request.env["appsignal.route"].tap do |value|
178
+ next unless value
179
+
180
+ Appsignal::Utils::StdoutAndLoggerMessage.warning \
181
+ "Setting the action name with the request env 'appsignal.route' is deprecated. " \
182
+ "Please use `Appsignal.set_action` instead. "
183
+ end
184
+ end
185
+
186
+ def appsignal_action_env_value(request)
187
+ request.env["appsignal.action"].tap do |value|
188
+ next unless value
189
+
190
+ Appsignal::Utils::StdoutAndLoggerMessage.warning \
191
+ "Setting the action name with the request env 'appsignal.action' is deprecated. " \
192
+ "Please use `Appsignal.set_action` instead. "
193
+ end
194
+ end
158
195
  end
159
196
  end
160
197
  end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Rack
5
+ # @api private
6
+ class BodyWrapper
7
+ def self.wrap(original_body, appsignal_transaction)
8
+ # The logic of how Rack treats a response body differs based on which methods
9
+ # the body responds to. This means that to support the Rack 3.x spec in full
10
+ # we need to return a wrapper which matches the API of the wrapped body as closely
11
+ # as possible. Pick the wrapper from the most specific to the least specific.
12
+ # See https://github.com/rack/rack/blob/main/SPEC.rdoc#the-body-
13
+ #
14
+ # What is important is that our Body wrapper responds to the same methods Rack
15
+ # (or a webserver) would be checking and calling, and passes through that functionality
16
+ # to the original body.
17
+ #
18
+ # This comment https://github.com/rails/rails/pull/49627#issuecomment-1769802573
19
+ # is of particular interest to understand why this has to be somewhat complicated.
20
+ if original_body.respond_to?(:to_path)
21
+ PathableBodyWrapper.new(original_body, appsignal_transaction)
22
+ elsif original_body.respond_to?(:to_ary)
23
+ ArrayableBodyWrapper.new(original_body, appsignal_transaction)
24
+ elsif !original_body.respond_to?(:each) && original_body.respond_to?(:call)
25
+ # This body only supports #call, so we must be running a Rack 3 application
26
+ # It is possible that a body exposes both `each` and `call` in the hopes of
27
+ # being backwards-compatible with both Rack 3.x and Rack 2.x, however
28
+ # this is not going to work since the SPEC says that if both are available,
29
+ # `each` should be used and `call` should be ignored.
30
+ # So for that case we can drop to our default EnumerableBodyWrapper
31
+ CallableBodyWrapper.new(original_body, appsignal_transaction)
32
+ else
33
+ EnumerableBodyWrapper.new(original_body, appsignal_transaction)
34
+ end
35
+ end
36
+
37
+ def initialize(body, appsignal_transaction)
38
+ @body_already_closed = false
39
+ @body = body
40
+ @transaction = appsignal_transaction
41
+ end
42
+
43
+ # This must be present in all Rack bodies and will be called by the serving adapter
44
+ def close
45
+ # The @body_already_closed check is needed so that if `to_ary`
46
+ # of the body has already closed itself (as prescribed) we do not
47
+ # attempt to close it twice
48
+ if !@body_already_closed && @body.respond_to?(:close)
49
+ Appsignal.instrument("close_response_body.rack") { @body.close }
50
+ end
51
+ @body_already_closed = true
52
+ rescue Exception => error # rubocop:disable Lint/RescueException
53
+ @transaction.set_error(error)
54
+ raise error
55
+ end
56
+ end
57
+
58
+ # The standard Rack body wrapper which exposes "each" for iterating
59
+ # over the response body. This is supported across all 3 major Rack
60
+ # versions.
61
+ #
62
+ # @api private
63
+ class EnumerableBodyWrapper < BodyWrapper
64
+ def each(&blk)
65
+ # This is a workaround for the Rails bug when there was a bit too much
66
+ # eagerness in implementing to_ary, see:
67
+ # https://github.com/rails/rails/pull/44953
68
+ # https://github.com/rails/rails/pull/47092
69
+ # https://github.com/rails/rails/pull/49627
70
+ # https://github.com/rails/rails/issues/49588
71
+ # While the Rack SPEC does not mandate `each` to be callable
72
+ # in a blockless way it is still a good idea to have it in place.
73
+ return enum_for(:each) unless block_given?
74
+
75
+ Appsignal.instrument("process_response_body.rack", "Process Rack response body (#each)") do
76
+ @body.each(&blk)
77
+ end
78
+ rescue Exception => error # rubocop:disable Lint/RescueException
79
+ @transaction.set_error(error)
80
+ raise error
81
+ end
82
+ end
83
+
84
+ # The callable response bodies are a new Rack 3.x feature, and would not work
85
+ # with older Rack versions. They must not respond to `each` because
86
+ # "If it responds to each, you must call each and not call". This is why
87
+ # it inherits from BodyWrapper directly and not from EnumerableBodyWrapper
88
+ #
89
+ # @api private
90
+ class CallableBodyWrapper < BodyWrapper
91
+ def call(stream)
92
+ # `stream` will be closed by the app we are calling, no need for us
93
+ # to close it ourselves
94
+ Appsignal.instrument("process_response_body.rack", "Process Rack response body (#call)") do
95
+ @body.call(stream)
96
+ end
97
+ rescue Exception => error # rubocop:disable Lint/RescueException
98
+ @transaction.set_error(error)
99
+ raise error
100
+ end
101
+ end
102
+
103
+ # "to_ary" takes precedence over "each" and allows the response body
104
+ # to be read eagerly. If the body supports that method, it takes precedence
105
+ # over "each":
106
+ # "Middleware may call to_ary directly on the Body and return a new Body in its place"
107
+ # One could "fold" both the to_ary API and the each() API into one Body object, but
108
+ # to_ary must also call "close" after it executes - and in the Rails implementation
109
+ # this pecularity was not handled properly.
110
+ #
111
+ # @api private
112
+ class ArrayableBodyWrapper < EnumerableBodyWrapper
113
+ def to_ary
114
+ @body_already_closed = true
115
+ Appsignal.instrument(
116
+ "process_response_body.rack",
117
+ "Process Rack response body (#to_ary)"
118
+ ) do
119
+ @body.to_ary
120
+ end
121
+ rescue Exception => error # rubocop:disable Lint/RescueException
122
+ @transaction.set_error(error)
123
+ raise error
124
+ end
125
+ end
126
+
127
+ # Having "to_path" on a body allows Rack to serve out a static file, or to
128
+ # pass that file to the downstream webserver for sending using X-Sendfile
129
+ class PathableBodyWrapper < EnumerableBodyWrapper
130
+ def to_path
131
+ Appsignal.instrument(
132
+ "process_response_body.rack",
133
+ "Process Rack response body (#to_path)"
134
+ ) do
135
+ @body.to_path
136
+ end
137
+ rescue Exception => error # rubocop:disable Lint/RescueException
138
+ @transaction.set_error(error)
139
+ raise error
140
+ end
141
+ end
142
+ end
143
+ end
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
4
-
5
3
  module Appsignal
6
- # @api private
7
4
  module Rack
5
+ # @api private
8
6
  class GenericInstrumentation < AbstractMiddleware
9
7
  def initialize(app, options = {})
10
- options[:instrument_span_name] ||= "process_action.generic"
8
+ options[:instrument_event_name] ||= "process_action.generic"
11
9
  super
12
10
  end
13
11
 
@@ -16,5 +14,8 @@ module Appsignal
16
14
  transaction.set_action_if_nil("unknown")
17
15
  end
18
16
  end
17
+
18
+ # @api private
19
+ class GenericInstrumentationAlias < GenericInstrumentation; end
19
20
  end
20
21
  end
@@ -5,7 +5,7 @@ module Appsignal
5
5
  # @api private
6
6
  class GrapeMiddleware < Appsignal::Rack::AbstractMiddleware
7
7
  def initialize(app, options = {})
8
- options[:instrument_span_name] = "process_request.grape"
8
+ options[:instrument_event_name] = "process_request.grape"
9
9
  options[:report_errors] = lambda { |env| !env["grape.skip_appsignal_error"] }
10
10
  super
11
11
  end
@@ -6,7 +6,7 @@ module Appsignal
6
6
  class HanamiMiddleware < AbstractMiddleware
7
7
  def initialize(app, options = {})
8
8
  options[:params_method] ||= :params
9
- options[:instrument_span_name] ||= "process_action.hanami"
9
+ options[:instrument_event_name] ||= "process_action.hanami"
10
10
  super
11
11
  end
12
12
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Rack
5
+ # Rack instrumentation middleware.
6
+ #
7
+ # This Ruby gem automatically instruments several Rack based libraries,
8
+ # like Rails and Sinatra. This middleware does not need to be added
9
+ # manually to these frameworks.
10
+ #
11
+ # This instrumentation middleware will wrap an app and report how long the
12
+ # request and response took, report errors that occurred in the app, and
13
+ # report metadata about the request method and path.
14
+ #
15
+ # The action name for the endpoint is not set by default, which is required
16
+ # for performance monitoring. Set the action name in each endpoint using
17
+ # the {Appsignal::Helpers::Instrumentation#set_action} helper.
18
+ #
19
+ # If multiple of these middlewares, or
20
+ # {AbstractMiddleware} subclasses are present in an app, only the top
21
+ # middleware will report errors from apps and other middleware.
22
+ #
23
+ # This middleware is best used in combination with the {EventHandler}.
24
+ #
25
+ # @example
26
+ # # config.ru
27
+ # require "appsignal"
28
+ # # Configure and start AppSignal
29
+ #
30
+ # # Add the EventHandler first
31
+ # use ::Rack::Events, [Appsignal::Rack::EventHandler.new]
32
+ # # Add the instrumentation middleware second
33
+ # use Appsignal::Rack::InstrumentationMiddleware
34
+ #
35
+ # # Other middleware
36
+ #
37
+ # # Start app
38
+ #
39
+ # @example Customize instrumentation event category
40
+ # use Appsignal::Rack::InstrumentationMiddleware,
41
+ # :instrument_event_name => "custom.goup"
42
+ #
43
+ # @example Disable error reporting for this middleware
44
+ # use Appsignal::Rack::InstrumentationMiddleware, :report_errors => false
45
+ #
46
+ # @example Always report errors, even when wrapped by other instrumentation middleware
47
+ # use Appsignal::Rack::InstrumentationMiddleware, :report_errors => true
48
+ #
49
+ # @example Disable error reporting for this middleware based on the request env
50
+ # use Appsignal::Rack::InstrumentationMiddleware,
51
+ # :report_errors => lambda { |env| env["some_key"] == "some value" }
52
+ #
53
+ # @see https://docs.appsignal.com/ruby/integrations/rack.html
54
+ # @api public
55
+ class InstrumentationMiddleware < AbstractMiddleware
56
+ def initialize(app, options = {})
57
+ options[:instrument_event_name] ||= "process_request_middleware.rack"
58
+ super
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
4
-
5
3
  module Appsignal
6
4
  module Rack
7
5
  # @api private
@@ -9,7 +7,7 @@ module Appsignal
9
7
  def initialize(app, options = {})
10
8
  options[:request_class] ||= ActionDispatch::Request
11
9
  options[:params_method] ||= :filtered_parameters
12
- options[:instrument_span_name] = nil
10
+ options[:instrument_event_name] = nil
13
11
  options[:report_errors] = true
14
12
  super
15
13
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "rack"
4
-
5
3
  module Appsignal
6
4
  module Rack
7
5
  # Stub old middleware. Prevents Sinatra middleware being loaded twice.
@@ -34,7 +32,7 @@ module Appsignal
34
32
  def initialize(app, options = {})
35
33
  options[:request_class] ||= Sinatra::Request
36
34
  options[:params_method] ||= :params
37
- options[:instrument_span_name] ||= "process_action.sinatra"
35
+ options[:instrument_event_name] ||= "process_action.sinatra"
38
36
  super
39
37
  @raise_errors_on = raise_errors?(app)
40
38
  end
@@ -1,73 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Appsignal::Utils::StdoutAndLoggerMessage.warning \
4
+ "The constant Appsignal::Rack::StreamingListener has been deprecated. " \
5
+ "Please update the constant name to " \
6
+ "Appsignal::Rack::InstrumentationMiddleware."
7
+
3
8
  module Appsignal
4
9
  module Rack
5
- # Appsignal module that tracks exceptions in Streaming rack responses.
10
+ # Instrumentation middleware that tracks exceptions in streaming Rack
11
+ # responses.
6
12
  #
7
13
  # @api private
8
- class StreamingListener
14
+ class StreamingListener < AbstractMiddleware
9
15
  def initialize(app, options = {})
10
- Appsignal.internal_logger.debug "Initializing Appsignal::Rack::StreamingListener"
11
- @app = app
12
- @options = options
13
- end
14
-
15
- def call(env)
16
- if Appsignal.active?
17
- call_with_appsignal_monitoring(env)
18
- else
19
- @app.call(env)
20
- end
16
+ options[:instrument_event_name] ||= "process_streaming_request.rack"
17
+ super
21
18
  end
22
19
 
23
- def call_with_appsignal_monitoring(env)
24
- request = ::Rack::Request.new(env)
25
- transaction = Appsignal::Transaction.create(
26
- SecureRandom.uuid,
27
- Appsignal::Transaction::HTTP_REQUEST,
28
- request
29
- )
20
+ def add_transaction_metadata_after(transaction, request)
21
+ transaction.set_action_if_nil(request.env["appsignal.action"])
30
22
 
31
- # Instrument a `process_action`, to set params/action name
32
- status, headers, body =
33
- Appsignal.instrument("process_action.rack") do
34
- @app.call(env)
35
- rescue Exception => e # rubocop:disable Lint/RescueException
36
- transaction.set_error(e)
37
- raise e
38
- ensure
39
- transaction.set_action_if_nil(env["appsignal.action"])
40
- transaction.set_metadata("path", request.path)
41
- transaction.set_metadata("method", request.request_method)
42
- transaction.set_http_or_background_queue_start
43
- end
44
-
45
- # Wrap the result body with our StreamWrapper
46
- [status, headers, StreamWrapper.new(body, transaction)]
23
+ super
47
24
  end
48
25
  end
49
26
  end
50
-
51
- class StreamWrapper
52
- def initialize(stream, transaction)
53
- @stream = stream
54
- @transaction = transaction
55
- end
56
-
57
- def each(&block)
58
- @stream.each(&block)
59
- rescue Exception => e # rubocop:disable Lint/RescueException
60
- @transaction.set_error(e)
61
- raise e
62
- end
63
-
64
- def close
65
- @stream.close if @stream.respond_to?(:close)
66
- rescue Exception => e # rubocop:disable Lint/RescueException
67
- @transaction.set_error(e)
68
- raise e
69
- ensure
70
- Appsignal::Transaction.complete_current!
71
- end
72
- end
73
27
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ # @api private
5
+ module Rack
6
+ # Alias constants that have moved with a warning message that points to the
7
+ # place to update the reference.
8
+ def self.const_missing(name)
9
+ case name
10
+ when :GenericInstrumentation
11
+ require "appsignal/rack/generic_instrumentation"
12
+
13
+ callers = caller
14
+ Appsignal::Utils::StdoutAndLoggerMessage.warning \
15
+ "The constant Appsignal::Rack::GenericInstrumentation has been deprecated. " \
16
+ "Please use the new Appsignal::Rack::InstrumentationMiddleware middleware. " \
17
+ "This new middleware does not default the action name to 'unknown'. " \
18
+ "Set the action name for the endpoint using the Appsignal.set_action helper. " \
19
+ "Read our Rack docs for more information " \
20
+ "https://docs.appsignal.com/ruby/integrations/rack.html " \
21
+ "Update the constant name to " \
22
+ "Appsignal::Rack::InstrumentationMiddleware in the following file to " \
23
+ "remove this message.\n#{callers.first}"
24
+ # Return the alias so it can't ever get stuck in a recursive loop
25
+ Appsignal::Rack::GenericInstrumentationAlias
26
+ else
27
+ super
28
+ end
29
+ end
30
+ end
31
+ end