appsignal 3.9.3-java → 3.11.0-java

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +22 -19
  3. data/.rubocop.yml +1 -1
  4. data/CHANGELOG.md +180 -0
  5. data/Gemfile +1 -0
  6. data/README.md +0 -1
  7. data/Rakefile +1 -1
  8. data/benchmark.rake +99 -42
  9. data/build_matrix.yml +10 -12
  10. data/gemfiles/webmachine1.gemfile +5 -4
  11. data/lib/appsignal/cli/demo.rb +0 -1
  12. data/lib/appsignal/config.rb +57 -97
  13. data/lib/appsignal/demo.rb +15 -20
  14. data/lib/appsignal/environment.rb +6 -1
  15. data/lib/appsignal/event_formatter/rom/sql_formatter.rb +1 -0
  16. data/lib/appsignal/event_formatter.rb +3 -2
  17. data/lib/appsignal/helpers/instrumentation.rb +490 -16
  18. data/lib/appsignal/hooks/action_cable.rb +21 -16
  19. data/lib/appsignal/hooks/active_job.rb +15 -14
  20. data/lib/appsignal/hooks/delayed_job.rb +1 -1
  21. data/lib/appsignal/hooks/shoryuken.rb +3 -63
  22. data/lib/appsignal/integrations/action_cable.rb +5 -7
  23. data/lib/appsignal/integrations/active_support_notifications.rb +1 -0
  24. data/lib/appsignal/integrations/capistrano/capistrano_2_tasks.rb +36 -35
  25. data/lib/appsignal/integrations/data_mapper.rb +1 -0
  26. data/lib/appsignal/integrations/delayed_job_plugin.rb +27 -33
  27. data/lib/appsignal/integrations/dry_monitor.rb +1 -0
  28. data/lib/appsignal/integrations/excon.rb +1 -0
  29. data/lib/appsignal/integrations/http.rb +1 -0
  30. data/lib/appsignal/integrations/net_http.rb +1 -0
  31. data/lib/appsignal/integrations/object.rb +6 -0
  32. data/lib/appsignal/integrations/padrino.rb +21 -25
  33. data/lib/appsignal/integrations/que.rb +13 -20
  34. data/lib/appsignal/integrations/railtie.rb +1 -1
  35. data/lib/appsignal/integrations/rake.rb +45 -15
  36. data/lib/appsignal/integrations/redis.rb +1 -0
  37. data/lib/appsignal/integrations/redis_client.rb +1 -0
  38. data/lib/appsignal/integrations/resque.rb +2 -5
  39. data/lib/appsignal/integrations/shoryuken.rb +75 -0
  40. data/lib/appsignal/integrations/sidekiq.rb +7 -25
  41. data/lib/appsignal/integrations/unicorn.rb +1 -0
  42. data/lib/appsignal/integrations/webmachine.rb +12 -9
  43. data/lib/appsignal/logger.rb +7 -3
  44. data/lib/appsignal/probes/helpers.rb +1 -0
  45. data/lib/appsignal/probes/mri.rb +1 -0
  46. data/lib/appsignal/probes/sidekiq.rb +1 -0
  47. data/lib/appsignal/probes.rb +3 -0
  48. data/lib/appsignal/rack/abstract_middleware.rb +67 -24
  49. data/lib/appsignal/rack/body_wrapper.rb +143 -0
  50. data/lib/appsignal/rack/event_handler.rb +39 -8
  51. data/lib/appsignal/rack/generic_instrumentation.rb +6 -4
  52. data/lib/appsignal/rack/grape_middleware.rb +3 -2
  53. data/lib/appsignal/rack/hanami_middleware.rb +1 -1
  54. data/lib/appsignal/rack/instrumentation_middleware.rb +62 -0
  55. data/lib/appsignal/rack/rails_instrumentation.rb +1 -3
  56. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -3
  57. data/lib/appsignal/rack/streaming_listener.rb +14 -59
  58. data/lib/appsignal/rack.rb +60 -0
  59. data/lib/appsignal/span.rb +1 -0
  60. data/lib/appsignal/transaction.rb +353 -104
  61. data/lib/appsignal/utils/data.rb +0 -1
  62. data/lib/appsignal/utils/hash_sanitizer.rb +0 -1
  63. data/lib/appsignal/utils/integration_logger.rb +0 -13
  64. data/lib/appsignal/utils/integration_memory_logger.rb +0 -13
  65. data/lib/appsignal/utils/json.rb +0 -1
  66. data/lib/appsignal/utils/query_params_sanitizer.rb +0 -1
  67. data/lib/appsignal/utils/stdout_and_logger_message.rb +0 -1
  68. data/lib/appsignal/utils.rb +6 -0
  69. data/lib/appsignal/version.rb +1 -1
  70. data/lib/appsignal.rb +9 -6
  71. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  72. data/spec/lib/appsignal/config_spec.rb +139 -43
  73. data/spec/lib/appsignal/hooks/action_cable_spec.rb +43 -74
  74. data/spec/lib/appsignal/hooks/activejob_spec.rb +9 -0
  75. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +2 -443
  76. data/spec/lib/appsignal/hooks/rake_spec.rb +100 -17
  77. data/spec/lib/appsignal/hooks/shoryuken_spec.rb +0 -171
  78. data/spec/lib/appsignal/integrations/delayed_job_plugin_spec.rb +459 -0
  79. data/spec/lib/appsignal/integrations/padrino_spec.rb +181 -131
  80. data/spec/lib/appsignal/integrations/que_spec.rb +3 -4
  81. data/spec/lib/appsignal/integrations/shoryuken_spec.rb +167 -0
  82. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +4 -4
  83. data/spec/lib/appsignal/integrations/sinatra_spec.rb +10 -2
  84. data/spec/lib/appsignal/integrations/webmachine_spec.rb +77 -17
  85. data/spec/lib/appsignal/rack/abstract_middleware_spec.rb +144 -11
  86. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +263 -0
  87. data/spec/lib/appsignal/rack/event_handler_spec.rb +81 -10
  88. data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +70 -17
  89. data/spec/lib/appsignal/rack/grape_middleware_spec.rb +1 -1
  90. data/spec/lib/appsignal/rack/instrumentation_middleware_spec.rb +38 -0
  91. data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +4 -2
  92. data/spec/lib/appsignal/rack/streaming_listener_spec.rb +43 -120
  93. data/spec/lib/appsignal/rack_spec.rb +63 -0
  94. data/spec/lib/appsignal/transaction_spec.rb +1675 -953
  95. data/spec/lib/appsignal/utils/integration_logger_spec.rb +12 -16
  96. data/spec/lib/appsignal/utils/integration_memory_logger_spec.rb +0 -10
  97. data/spec/lib/appsignal_spec.rb +517 -13
  98. data/spec/support/helpers/transaction_helpers.rb +44 -20
  99. data/spec/support/matchers/transaction.rb +15 -1
  100. data/spec/support/mocks/dummy_app.rb +1 -1
  101. data/spec/support/testing.rb +1 -1
  102. metadata +12 -4
  103. data/support/check_versions +0 -22
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Appsignal
4
4
  module Integrations
5
+ # @api private
5
6
  module RedisClientIntegration
6
7
  def write(command)
7
8
  sanitized_command =
@@ -5,11 +5,7 @@ module Appsignal
5
5
  # @api private
6
6
  module ResqueIntegration
7
7
  def perform
8
- transaction = Appsignal::Transaction.create(
9
- SecureRandom.uuid,
10
- Appsignal::Transaction::BACKGROUND_JOB,
11
- Appsignal::Transaction::GenericRequest.new({})
12
- )
8
+ transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
13
9
 
14
10
  Appsignal.instrument "perform.resque" do
15
11
  super
@@ -34,6 +30,7 @@ module Appsignal
34
30
  end
35
31
  end
36
32
 
33
+ # @api private
37
34
  class ResqueHelpers
38
35
  def self.arguments(payload)
39
36
  case payload["class"]
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Integrations
5
+ # @api private
6
+ class ShoryukenMiddleware
7
+ def call(worker_instance, queue, sqs_msg, body, &block)
8
+ transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
9
+
10
+ Appsignal.instrument("perform_job.shoryuken", &block)
11
+ rescue Exception => error # rubocop:disable Lint/RescueException
12
+ transaction.set_error(error)
13
+ raise
14
+ ensure
15
+ batch = sqs_msg.is_a?(Array)
16
+ attributes = fetch_attributes(batch, sqs_msg)
17
+ transaction.set_action_if_nil("#{worker_instance.class.name}#perform")
18
+ transaction.set_params_if_nil { fetch_args(batch, sqs_msg, body) }
19
+ transaction.set_tags(attributes)
20
+ transaction.set_tags("queue" => queue)
21
+ transaction.set_tags("batch" => true) if batch
22
+
23
+ if attributes.key?("SentTimestamp")
24
+ transaction.set_queue_start(Time.at(attributes["SentTimestamp"].to_i).to_i)
25
+ end
26
+
27
+ Appsignal::Transaction.complete_current!
28
+ end
29
+
30
+ private
31
+
32
+ def fetch_attributes(batch, sqs_msg)
33
+ if batch
34
+ # We can't instrument batched message separately, the `yield` will
35
+ # perform all the batched messages.
36
+ # To provide somewhat useful metadata, Get first message based on
37
+ # SentTimestamp, and use its attributes as metadata for the
38
+ # transaction. We can't combine them all because then they would
39
+ # overwrite each other and the last message (in an sorted order)
40
+ # would be used as the source of the metadata. With the
41
+ # oldest/first message at least some useful information is stored
42
+ # such as the first received time and the number of retries for the
43
+ # first message. The newer message should have lower values and
44
+ # timestamps in their metadata.
45
+ first_msg =
46
+ sqs_msg.min do |a, b|
47
+ a.attributes["SentTimestamp"].to_i <=> b.attributes["SentTimestamp"].to_i
48
+ end
49
+ first_msg.attributes
50
+ else
51
+ sqs_msg.attributes.merge(:message_id => sqs_msg.message_id)
52
+ end
53
+ end
54
+
55
+ def fetch_args(batch, sqs_msg, body)
56
+ if batch
57
+ bodies = {}
58
+ sqs_msg.each_with_index do |msg, index|
59
+ # Store all separate bodies on a hash with the key being the
60
+ # message_id
61
+ bodies[msg.message_id] = body[index]
62
+ end
63
+ bodies
64
+ else
65
+ case body
66
+ when Hash
67
+ body
68
+ else
69
+ { :params => body }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -11,6 +11,7 @@ module Appsignal
11
11
  # about completing the transaction.
12
12
  #
13
13
  # Introduced in Sidekiq 5.1.
14
+ # @api private
14
15
  class SidekiqDeathHandler
15
16
  def call(_job_context, exception)
16
17
  return unless Appsignal.config[:sidekiq_report_errors] == "discard"
@@ -37,12 +38,7 @@ module Appsignal
37
38
  # Sidekiq error outside of the middleware scope.
38
39
  # Can be a job JSON parse error or some other error happening in
39
40
  # Sidekiq.
40
- transaction =
41
- Appsignal::Transaction.create(
42
- SecureRandom.uuid, # Newly generated job id
43
- Appsignal::Transaction::BACKGROUND_JOB,
44
- Appsignal::Transaction::GenericRequest.new({})
45
- )
41
+ transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
46
42
  transaction.set_action_if_nil("SidekiqInternal")
47
43
  transaction.set_metadata("sidekiq_error", sidekiq_context[:context])
48
44
  transaction.set_params_if_nil(:jobstr => sidekiq_context[:jobstr])
@@ -64,13 +60,7 @@ module Appsignal
64
60
 
65
61
  def call(_worker, item, _queue, &block)
66
62
  job_status = nil
67
- transaction = Appsignal::Transaction.create(
68
- item["jid"],
69
- Appsignal::Transaction::BACKGROUND_JOB,
70
- Appsignal::Transaction::GenericRequest.new(
71
- :queue_start => item["enqueued_at"]
72
- )
73
- )
63
+ transaction = Appsignal::Transaction.create(Appsignal::Transaction::BACKGROUND_JOB)
74
64
  transaction.set_action_if_nil(formatted_action_name(item))
75
65
 
76
66
  formatted_metadata(item).each do |key, value|
@@ -83,8 +73,10 @@ module Appsignal
83
73
  raise exception
84
74
  ensure
85
75
  if transaction
86
- transaction.set_params_if_nil(filtered_arguments(item))
87
- transaction.set_http_or_background_queue_start
76
+ transaction.set_params_if_nil { parse_arguments(item) }
77
+ queue_start = (item["enqueued_at"].to_f * 1000.0).to_i # Convert seconds to milliseconds
78
+ transaction.set_queue_start(queue_start)
79
+ transaction.set_tags(:request_id => item["jid"])
88
80
  Appsignal::Transaction.complete_current! unless exception
89
81
 
90
82
  queue = item["queue"] || "unknown"
@@ -115,16 +107,6 @@ module Appsignal
115
107
  "#{sidekiq_action_name}#perform"
116
108
  end
117
109
 
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
110
  def formatted_metadata(item)
129
111
  {}.tap do |hash|
130
112
  (item || {}).each do |key, value|
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Appsignal
4
4
  module Integrations
5
+ # @api private
5
6
  module UnicornIntegration
6
7
  # Make sure that appsignal is started and the last transaction
7
8
  # in a worker gets flushed.
@@ -5,20 +5,23 @@ 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(Appsignal::Transaction::HTTP_REQUEST)
14
+ end
16
15
 
17
16
  Appsignal.instrument("process_action.webmachine") do
18
17
  super
19
18
  end
19
+ ensure
20
+ transaction.set_action_if_nil("#{resource.class.name}##{request.method}")
21
+ transaction.set_params_if_nil(request.query)
22
+ transaction.set_headers_if_nil { request.headers if request.respond_to?(:headers) }
20
23
 
21
- Appsignal::Transaction.complete_current!
24
+ Appsignal::Transaction.complete_current! unless has_parent_transaction
22
25
  end
23
26
 
24
27
  private
@@ -4,7 +4,10 @@ require "logger"
4
4
  require "set"
5
5
 
6
6
  module Appsignal
7
- # Logger that flushes logs to the AppSignal logging service
7
+ # Logger that flushes logs to the AppSignal logging service.
8
+ #
9
+ # @see https://docs.appsignal.com/logging/platforms/integrations/ruby.html
10
+ # AppSignal Ruby logging documentation.
8
11
  class Logger < ::Logger
9
12
  PLAINTEXT = 0
10
13
  LOGFMT = 1
@@ -144,8 +147,9 @@ module Appsignal
144
147
  # as our logger directly inherits from Ruby base logger.
145
148
  #
146
149
  # Links:
147
- # https://github.com/rails/rails/blob/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/activesupport/lib/active_support/logger.rb#L60-L76
148
- # https://github.com/rails/rails/blob/main/activesupport/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/active_support/logger_silence.rb
150
+ #
151
+ # - https://github.com/rails/rails/blob/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/activesupport/lib/active_support/logger.rb#L60-L76
152
+ # - https://github.com/rails/rails/blob/main/activesupport/e11ebc04cfbe41c06cdfb70ee5a9fdbbd98bb263/active_support/logger_silence.rb
149
153
  def silence(_severity = ERROR, &block)
150
154
  block.call
151
155
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
+ # @api private
5
6
  module Helpers
6
7
  private
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
+ # @api private
5
6
  class MriProbe
6
7
  include Helpers
7
8
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
+ # @api private
5
6
  class SidekiqProbe
6
7
  include Helpers
7
8
 
@@ -2,8 +2,10 @@
2
2
 
3
3
  module Appsignal
4
4
  module Probes
5
+ # @api private
5
6
  ITERATION_IN_SECONDS = 60
6
7
 
8
+ # @api private
7
9
  class ProbeCollection
8
10
  def initialize
9
11
  @probes = {}
@@ -72,6 +74,7 @@ module Appsignal
72
74
 
73
75
  # @see ProbeCollection
74
76
  # @return [ProbeCollection] Returns list of probes.
77
+ # @api private
75
78
  def probes
76
79
  @probes ||= ProbeCollection.new
77
80
  end
@@ -4,6 +4,12 @@ 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
+ #
12
+ # @abstract
7
13
  # @api private
8
14
  class AbstractMiddleware
9
15
  DEFAULT_ERROR_REPORTING = :default
@@ -14,7 +20,7 @@ module Appsignal
14
20
  @options = options
15
21
  @request_class = options.fetch(:request_class, ::Rack::Request)
16
22
  @params_method = options.fetch(:params_method, :params)
17
- @instrument_span_name = options.fetch(:instrument_span_name, "process.abstract")
23
+ @instrument_event_name = options.fetch(:instrument_event_name, nil)
18
24
  @report_errors = options.fetch(:report_errors, DEFAULT_ERROR_REPORTING)
19
25
  end
20
26
 
@@ -28,11 +34,7 @@ module Appsignal
28
34
  if wrapped_instrumentation
29
35
  env[Appsignal::Rack::APPSIGNAL_TRANSACTION]
30
36
  else
31
- Appsignal::Transaction.create(
32
- SecureRandom.uuid,
33
- Appsignal::Transaction::HTTP_REQUEST,
34
- request
35
- )
37
+ Appsignal::Transaction.create(Appsignal::Transaction::HTTP_REQUEST)
36
38
  end
37
39
 
38
40
  unless wrapped_instrumentation
@@ -53,7 +55,7 @@ module Appsignal
53
55
  wrapped_instrumentation
54
56
  )
55
57
  else
56
- instrument_app_call(request.env)
58
+ instrument_app_call(request.env, transaction)
57
59
  end
58
60
  ensure
59
61
  add_transaction_metadata_after(transaction, request)
@@ -72,28 +74,40 @@ module Appsignal
72
74
  # don't report any exceptions here, the top instrumentation middleware
73
75
  # will be the one reporting the exception.
74
76
  #
75
- # Either another {GenericInstrumentation} or {EventHandler} is higher in
76
- # the stack and will report the exception and complete the transaction.
77
+ # Either another {AbstractMiddleware} or {EventHandler} is higher in the
78
+ # stack and will report the exception and complete the transaction.
77
79
  #
78
- # @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)
80
+ # @see #instrument_app_call_with_exception_handling
81
+ def instrument_app_call(env, transaction)
82
+ if @instrument_event_name
83
+ Appsignal.instrument(@instrument_event_name) do
84
+ call_app(env, transaction)
83
85
  end
84
86
  else
85
- @app.call(env)
87
+ call_app(env, transaction)
86
88
  end
87
89
  end
88
90
 
91
+ def call_app(env, transaction)
92
+ status, headers, obody = @app.call(env)
93
+ body =
94
+ if obody.is_a? Appsignal::Rack::BodyWrapper
95
+ obody
96
+ else
97
+ # Instrument response body and closing of the response body
98
+ Appsignal::Rack::BodyWrapper.wrap(obody, transaction)
99
+ end
100
+ [status, headers, body]
101
+ end
102
+
89
103
  # Instrument the request fully. This is used by the top instrumentation
90
104
  # middleware in the middleware stack. Unlike
91
105
  # {#instrument_app_call} this will report any exceptions being
92
106
  # raised.
93
107
  #
94
- # @see {#instrument_app_call}
108
+ # @see #instrument_app_call
95
109
  def instrument_app_call_with_exception_handling(env, transaction, wrapped_instrumentation)
96
- instrument_app_call(env)
110
+ instrument_app_call(env, transaction)
97
111
  rescue Exception => error # rubocop:disable Lint/RescueException
98
112
  report_errors =
99
113
  if @report_errors == DEFAULT_ERROR_REPORTING
@@ -122,15 +136,22 @@ module Appsignal
122
136
  # Call `super` to also include the default set metadata.
123
137
  def add_transaction_metadata_after(transaction, request)
124
138
  default_action =
125
- request.env["appsignal.route"] || request.env["appsignal.action"]
139
+ appsignal_route_env_value(request) || appsignal_action_env_value(request)
126
140
  transaction.set_action_if_nil(default_action)
127
141
  transaction.set_metadata("path", request.path)
128
142
 
129
143
  request_method = request_method_for(request)
130
144
  transaction.set_metadata("method", request_method) if request_method
131
145
 
132
- transaction.set_params_if_nil(params_for(request))
133
- transaction.set_http_or_background_queue_start
146
+ transaction.set_params_if_nil { params_for(request) }
147
+ transaction.set_session_data_if_nil do
148
+ request.session if request.respond_to?(:session)
149
+ end
150
+ transaction.set_headers_if_nil do
151
+ request.env if request.respond_to?(:env)
152
+ end
153
+ queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
154
+ transaction.set_queue_start(queue_start) if queue_start
134
155
  end
135
156
 
136
157
  def params_for(request)
@@ -138,9 +159,9 @@ module Appsignal
138
159
 
139
160
  request.send(@params_method)
140
161
  rescue => error
141
- # Getting params from the request has been know to fail.
142
- Appsignal.internal_logger.debug(
143
- "Exception while getting params in #{self.class} from '#{@params_method}': #{error}"
162
+ Appsignal.internal_logger.error(
163
+ "Exception while fetching params from '#{@request_class}##{@params_method}': " \
164
+ "#{error.class} #{error}"
144
165
  )
145
166
  nil
146
167
  end
@@ -148,13 +169,35 @@ module Appsignal
148
169
  def request_method_for(request)
149
170
  request.request_method
150
171
  rescue => error
151
- Appsignal.internal_logger.error("Unable to report HTTP request method: '#{error}'")
172
+ Appsignal.internal_logger.error(
173
+ "Exception while fetching the HTTP request method: #{error.class}: #{error}"
174
+ )
152
175
  nil
153
176
  end
154
177
 
155
178
  def request_for(env)
156
179
  @request_class.new(env)
157
180
  end
181
+
182
+ def appsignal_route_env_value(request)
183
+ request.env["appsignal.route"].tap do |value|
184
+ next unless value
185
+
186
+ Appsignal::Utils::StdoutAndLoggerMessage.warning \
187
+ "Setting the action name with the request env 'appsignal.route' is deprecated. " \
188
+ "Please use `Appsignal.set_action` instead. "
189
+ end
190
+ end
191
+
192
+ def appsignal_action_env_value(request)
193
+ request.env["appsignal.action"].tap do |value|
194
+ next unless value
195
+
196
+ Appsignal::Utils::StdoutAndLoggerMessage.warning \
197
+ "Setting the action name with the request env 'appsignal.action' is deprecated. " \
198
+ "Please use `Appsignal.set_action` instead. "
199
+ end
200
+ end
158
201
  end
159
202
  end
160
203
  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