appsignal 3.6.1-java → 3.6.3-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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -4
  3. data/Rakefile +1 -1
  4. data/ext/agent.rb +27 -27
  5. data/lib/appsignal/auth_check.rb +1 -1
  6. data/lib/appsignal/config.rb +14 -1
  7. data/lib/appsignal/event_formatter/sequel/sql_formatter.rb +1 -1
  8. data/lib/appsignal/integrations/railtie.rb +14 -2
  9. data/lib/appsignal/logger.rb +5 -5
  10. data/lib/appsignal/rack/generic_instrumentation.rb +4 -17
  11. data/lib/appsignal/rack/rails_instrumentation.rb +3 -15
  12. data/lib/appsignal/rack/sinatra_instrumentation.rb +3 -15
  13. data/lib/appsignal/rack/streaming_listener.rb +35 -26
  14. data/lib/appsignal/version.rb +1 -1
  15. data/lib/appsignal.rb +0 -1
  16. data/lib/puma/plugin/appsignal.rb +1 -1
  17. data/spec/lib/appsignal/capistrano2_spec.rb +2 -2
  18. data/spec/lib/appsignal/capistrano3_spec.rb +2 -2
  19. data/spec/lib/appsignal/cli/diagnose_spec.rb +1 -1
  20. data/spec/lib/appsignal/config_spec.rb +10 -5
  21. data/spec/lib/appsignal/environment_spec.rb +3 -3
  22. data/spec/lib/appsignal/event_formatter/mongo_ruby_driver/query_formatter_spec.rb +1 -1
  23. data/spec/lib/appsignal/hooks/resque_spec.rb +1 -1
  24. data/spec/lib/appsignal/hooks_spec.rb +1 -1
  25. data/spec/lib/appsignal/integrations/railtie_spec.rb +27 -2
  26. data/spec/lib/appsignal/integrations/sidekiq_spec.rb +1 -1
  27. data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +2 -3
  28. data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +2 -4
  29. data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +1 -3
  30. data/spec/lib/appsignal/rack/streaming_listener_spec.rb +53 -9
  31. data/spec/lib/appsignal/transaction_spec.rb +2 -2
  32. data/spec/lib/appsignal_spec.rb +1 -1
  33. data/spec/lib/puma/appsignal_spec.rb +1 -1
  34. metadata +3 -5
  35. data/lib/appsignal/rack/body_wrapper.rb +0 -161
  36. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +0 -220
@@ -160,7 +160,7 @@ if DependencyHelper.capistrano2_present?
160
160
  run
161
161
  end
162
162
 
163
- it "transmits the overriden revision" do
163
+ it "transmits the overridden revision" do
164
164
  expect(output).to include \
165
165
  "Notifying AppSignal of deploy with: revision: abc123, user: batman",
166
166
  "AppSignal has been notified of this deploy!"
@@ -174,7 +174,7 @@ if DependencyHelper.capistrano2_present?
174
174
  run
175
175
  end
176
176
 
177
- it "transmits the overriden deploy user" do
177
+ it "transmits the overridden deploy user" do
178
178
  expect(output).to include \
179
179
  "Notifying AppSignal of deploy with: revision: 503ce0923ed177a3ce000005," \
180
180
  " user: robin",
@@ -172,7 +172,7 @@ if DependencyHelper.capistrano3_present?
172
172
  run
173
173
  end
174
174
 
175
- it "transmits the overriden revision" do
175
+ it "transmits the overridden revision" do
176
176
  expect(output).to include \
177
177
  "Notifying AppSignal of deploy with: revision: abc123, user: batman",
178
178
  "AppSignal has been notified of this deploy!"
@@ -186,7 +186,7 @@ if DependencyHelper.capistrano3_present?
186
186
  run
187
187
  end
188
188
 
189
- it "transmits the overriden deploy user" do
189
+ it "transmits the overridden deploy user" do
190
190
  expect(output).to include \
191
191
  "Notifying AppSignal of deploy with: revision: 503ce0923ed177a3ce000005, " \
192
192
  "user: robin",
@@ -466,7 +466,7 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
466
466
  end
467
467
  end
468
468
 
469
- context "when the extention returns invalid JSON" do
469
+ context "when the extension returns invalid JSON" do
470
470
  before do
471
471
  expect(Appsignal::Extension).to receive(:diagnose).and_return("invalid agent\njson")
472
472
  run
@@ -6,7 +6,8 @@ describe Appsignal::Config do
6
6
  configured_env_keys = (
7
7
  config::ENV_STRING_KEYS +
8
8
  config::ENV_BOOLEAN_KEYS +
9
- config::ENV_ARRAY_KEYS
9
+ config::ENV_ARRAY_KEYS +
10
+ config::ENV_FLOAT_KEYS
10
11
  ).sort
11
12
 
12
13
  expect(mapped_env_keys).to eql(configured_env_keys)
@@ -264,7 +265,7 @@ describe Appsignal::Config do
264
265
  end
265
266
  end
266
267
 
267
- context "with an overriden config file" do
268
+ context "with an overridden config file" do
268
269
  let(:config) do
269
270
  project_fixture_config("production", {}, Appsignal.internal_logger,
270
271
  File.join(project_fixture_path, "config", "appsignal.yml"))
@@ -275,7 +276,7 @@ describe Appsignal::Config do
275
276
  expect(config.active?).to be_truthy
276
277
  end
277
278
 
278
- context "with an invalid overriden config file" do
279
+ context "with an invalid overridden config file" do
279
280
  let(:config) do
280
281
  project_fixture_config("production", {}, Appsignal.internal_logger,
281
282
  File.join(project_fixture_path, "config", "missing.yml"))
@@ -300,7 +301,7 @@ describe Appsignal::Config do
300
301
  stdout = std_stream
301
302
  stderr = std_stream
302
303
  log = capture_logs { capture_std_streams(stdout, stderr) { config } }
303
- message = "An error occured while loading the AppSignal config file. " \
304
+ message = "An error occurred while loading the AppSignal config file. " \
304
305
  "Skipping file config. " \
305
306
  "In future versions AppSignal will not start on a config file " \
306
307
  "error. To opt-in to this new behavior set " \
@@ -326,7 +327,7 @@ describe Appsignal::Config do
326
327
  ENV["APPSIGNAL_PUSH_API_KEY"] = "something valid"
327
328
  ENV["APPSIGNAL_INACTIVE_ON_CONFIG_FILE_ERROR"] = "1"
328
329
  log = capture_logs { capture_std_streams(stdout, stderr) { config } }
329
- message = "An error occured while loading the AppSignal config file. " \
330
+ message = "An error occurred while loading the AppSignal config file. " \
330
331
  "Not starting AppSignal because APPSIGNAL_INACTIVE_ON_CONFIG_FILE_ERROR is set.\n" \
331
332
  "File: #{File.join(config_path, "config", "appsignal.yml").inspect}\n" \
332
333
  "KeyError: key not found"
@@ -414,6 +415,7 @@ describe Appsignal::Config do
414
415
  :push_api_key => "aaa-bbb-ccc",
415
416
  :active => true,
416
417
  :bind_address => "0.0.0.0",
418
+ :cpu_count => 1.5,
417
419
  :name => "App name",
418
420
  :debug => true,
419
421
  :dns_servers => ["8.8.8.8", "8.8.4.4"],
@@ -436,6 +438,7 @@ describe Appsignal::Config do
436
438
  ENV["APPSIGNAL_ACTIVE"] = "true"
437
439
  ENV["APPSIGNAL_APP_NAME"] = "App name"
438
440
  ENV["APPSIGNAL_BIND_ADDRESS"] = "0.0.0.0"
441
+ ENV["APPSIGNAL_CPU_COUNT"] = "1.5"
439
442
  ENV["APPSIGNAL_DEBUG"] = "true"
440
443
  ENV["APPSIGNAL_DNS_SERVERS"] = "8.8.8.8,8.8.4.4"
441
444
  ENV["APPSIGNAL_IGNORE_ACTIONS"] = "action1,action2"
@@ -631,6 +634,7 @@ describe Appsignal::Config do
631
634
  let(:config) { project_fixture_config(:production) }
632
635
  before do
633
636
  config[:bind_address] = "0.0.0.0"
637
+ config[:cpu_count] = 1.5
634
638
  config[:logging_endpoint] = "http://localhost:123"
635
639
  config[:http_proxy] = "http://localhost"
636
640
  config[:ignore_actions] = %w[action1 action2]
@@ -654,6 +658,7 @@ describe Appsignal::Config do
654
658
  .to end_with("spec/support/fixtures/projects/valid")
655
659
  expect(ENV.fetch("_APPSIGNAL_AGENT_PATH", nil)).to end_with("/ext")
656
660
  expect(ENV.fetch("_APPSIGNAL_BIND_ADDRESS", nil)).to eq("0.0.0.0")
661
+ expect(ENV.fetch("_APPSIGNAL_CPU_COUNT", nil)).to eq("1.5")
657
662
  expect(ENV.fetch("_APPSIGNAL_DEBUG_LOGGING", nil)).to eq "false"
658
663
  expect(ENV.fetch("_APPSIGNAL_LOG", nil)).to eq "stdout"
659
664
  expect(ENV.fetch("_APPSIGNAL_LOG_FILE_PATH", nil)).to end_with("/tmp/appsignal.log")
@@ -90,7 +90,7 @@ describe Appsignal::Environment do
90
90
  end
91
91
  end
92
92
 
93
- context "when something unforseen errors" do
93
+ context "when something unforeseen errors" do
94
94
  it "does not re-raise the error and writes it to the log" do
95
95
  klass = Class.new do
96
96
  def inspect
@@ -127,7 +127,7 @@ describe Appsignal::Environment do
127
127
  expect(rake_spec.version.to_s).to_not be_empty
128
128
  end
129
129
 
130
- context "when something unforseen errors" do
130
+ context "when something unforeseen errors" do
131
131
  it "does not re-raise the error and writes it to the log" do
132
132
  expect(Bundler).to receive(:rubygems).and_raise(RuntimeError, "bundler error")
133
133
 
@@ -148,7 +148,7 @@ describe Appsignal::Environment do
148
148
  expect_environment_metadata("ruby_a_test_enabled", "true")
149
149
  end
150
150
 
151
- context "when something unforseen errors" do
151
+ context "when something unforeseen errors" do
152
152
  it "does not re-raise the error and writes it to the log" do
153
153
  klass = Class.new do
154
154
  def to_s
@@ -21,7 +21,7 @@ describe Appsignal::EventFormatter::MongoRubyDriver::QueryFormatter do
21
21
  formatter.format(strategy, command)
22
22
  end
23
23
 
24
- context "when strategy is unkown" do
24
+ context "when strategy is unknown" do
25
25
  let(:strategy) { :bananas }
26
26
 
27
27
  it "should return an empty hash" do
@@ -160,7 +160,7 @@ describe Appsignal::Hooks::ResqueHook do
160
160
  end
161
161
  after { Object.send(:remove_const, :ActiveJobMock) }
162
162
 
163
- it "does not set arguments but lets the ActiveJob intergration handle it" do
163
+ it "does not set arguments but lets the ActiveJob integration handle it" do
164
164
  perform_job(
165
165
  ResqueTestJob,
166
166
  "class" => "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper",
@@ -47,7 +47,7 @@ describe Appsignal::Hooks do
47
47
  Appsignal::Hooks.hooks.delete(:mock_present_hook)
48
48
  end
49
49
 
50
- it "should not install if depencies are not present" do
50
+ it "should not install if dependencies are not present" do
51
51
  Appsignal::Hooks::Hook.register(:mock_not_present_hook, MockNotPresentHook)
52
52
 
53
53
  expect(Appsignal::Hooks.hooks[:mock_not_present_hook]).to be_instance_of(MockNotPresentHook)
@@ -256,10 +256,28 @@ if DependencyHelper.rails_present?
256
256
  end
257
257
 
258
258
  context "when no transaction is active" do
259
+ class ExampleRailsRequestMock
260
+ def path
261
+ "path"
262
+ end
263
+
264
+ def method
265
+ "GET"
266
+ end
267
+
268
+ def filtered_parameters
269
+ { :user_id => 123, :password => "[FILTERED]" }
270
+ end
271
+ end
272
+
259
273
  class ExampleRailsControllerMock
260
274
  def action_name
261
275
  "index"
262
276
  end
277
+
278
+ def request
279
+ @request ||= ExampleRailsRequestMock.new
280
+ end
263
281
  end
264
282
 
265
283
  class ExampleRailsJobMock
@@ -275,7 +293,7 @@ if DependencyHelper.rails_present?
275
293
  clear_current_transaction!
276
294
  end
277
295
 
278
- it "fetches the action from the controller in the context" do
296
+ it "fetches the action, path and method from the controller in the context" do
279
297
  # The controller key is set by Rails when raised in a controller
280
298
  given_context = { :controller => ExampleRailsControllerMock.new }
281
299
  with_rails_error_reporter do
@@ -285,7 +303,14 @@ if DependencyHelper.rails_present?
285
303
  transaction = last_transaction
286
304
  transaction_hash = transaction.to_h
287
305
  expect(transaction_hash).to include(
288
- "action" => "ExampleRailsControllerMock#index"
306
+ "action" => "ExampleRailsControllerMock#index",
307
+ "metadata" => hash_including(
308
+ "path" => "path",
309
+ "method" => "GET"
310
+ ),
311
+ "sample_data" => hash_including(
312
+ "params" => { "user_id" => 123, "password" => "[FILTERED]" }
313
+ )
289
314
  )
290
315
  end
291
316
 
@@ -8,7 +8,7 @@ describe Appsignal::Integrations::SidekiqErrorHandler do
8
8
  end
9
9
  around { |example| keep_transactions { example.run } }
10
10
 
11
- context "without a current transction" do
11
+ context "without a current transaction" do
12
12
  let(:exception) do
13
13
  raise ExampleStandardError, "uh oh"
14
14
  rescue => error
@@ -50,7 +50,7 @@ describe Appsignal::Rack::GenericInstrumentation do
50
50
  expect(app).to receive(:call).with(env)
51
51
  end
52
52
 
53
- context "with an exception raised from call()", :error => true do
53
+ context "with an exception", :error => true do
54
54
  let(:error) { ExampleException }
55
55
  let(:app) do
56
56
  double.tap do |d|
@@ -58,9 +58,8 @@ describe Appsignal::Rack::GenericInstrumentation do
58
58
  end
59
59
  end
60
60
 
61
- it "records the exception and completes the transaction" do
61
+ it "records the exception" do
62
62
  expect_any_instance_of(Appsignal::Transaction).to receive(:set_error).with(error)
63
- expect(Appsignal::Transaction).to receive(:complete_current!)
64
63
  end
65
64
  end
66
65
 
@@ -65,8 +65,7 @@ if DependencyHelper.rails_present?
65
65
 
66
66
  describe "#call_with_appsignal_monitoring" do
67
67
  def run
68
- _status, _headers, body = middleware.call(env)
69
- body.close # Rack will always call close() on the body
68
+ middleware.call(env)
70
69
  end
71
70
 
72
71
  it "calls the wrapped app" do
@@ -127,8 +126,7 @@ if DependencyHelper.rails_present?
127
126
  end
128
127
  end
129
128
 
130
- it "records the exception and completes the transaction" do
131
- expect(Appsignal::Transaction).to receive(:complete_current!)
129
+ it "records the exception" do
132
130
  expect { run }.to raise_error(error)
133
131
 
134
132
  transaction_hash = last_transaction.to_h
@@ -3,9 +3,7 @@ if DependencyHelper.sinatra_present?
3
3
 
4
4
  module SinatraRequestHelpers
5
5
  def make_request(env)
6
- _status, _headers, body = middleware.call(env)
7
- # Close the body so that the transaction gets completed
8
- body&.close
6
+ middleware.call(env)
9
7
  end
10
8
 
11
9
  def make_request_with_error(env, error)
@@ -90,11 +90,10 @@ describe Appsignal::Rack::StreamingListener do
90
90
  context "with an exception in the instrumentation call" do
91
91
  let(:error) { ExampleException }
92
92
 
93
- it "should add the exception to the transaction and complete the transaction" do
93
+ it "should add the exception to the transaction" do
94
94
  allow(app).to receive(:call).and_raise(error)
95
95
 
96
96
  expect(transaction).to receive(:set_error).with(error)
97
- expect(Appsignal::Transaction).to receive(:complete_current!).and_call_original
98
97
 
99
98
  expect do
100
99
  listener.call_with_appsignal_monitoring(env)
@@ -102,19 +101,64 @@ describe Appsignal::Rack::StreamingListener do
102
101
  end
103
102
  end
104
103
 
105
- it "should wrap the response body in a wrapper" do
104
+ it "should wrap the body in a wrapper" do
105
+ expect(Appsignal::StreamWrapper).to receive(:new)
106
+ .with("body", transaction)
107
+ .and_return(wrapper)
108
+
106
109
  body = listener.call_with_appsignal_monitoring(env)[2]
107
110
 
108
- expect(body).to be_kind_of(Appsignal::Rack::BodyWrapper)
111
+ expect(body).to be_a(Appsignal::StreamWrapper)
109
112
  end
110
113
  end
111
114
  end
112
115
 
113
116
  describe Appsignal::StreamWrapper do
114
- it ".new returns an EnumerableWrapper" do
115
- fake_body = double(:each => nil)
116
- fake_txn = double
117
- stream_wrapper = Appsignal::StreamWrapper.new(fake_body, fake_txn)
118
- expect(stream_wrapper).to be_kind_of(Appsignal::Rack::EnumerableBodyWrapper)
117
+ let(:stream) { double }
118
+ let(:transaction) do
119
+ Appsignal::Transaction.create(SecureRandom.uuid, Appsignal::Transaction::HTTP_REQUEST, {})
120
+ end
121
+ let(:wrapper) { Appsignal::StreamWrapper.new(stream, transaction) }
122
+
123
+ describe "#each" do
124
+ it "calls the original stream" do
125
+ expect(stream).to receive(:each)
126
+
127
+ wrapper.each
128
+ end
129
+
130
+ context "when #each raises an error" do
131
+ let(:error) { ExampleException }
132
+
133
+ it "records the exception" do
134
+ allow(stream).to receive(:each).and_raise(error)
135
+
136
+ expect(transaction).to receive(:set_error).with(error)
137
+
138
+ expect { wrapper.send(:each) }.to raise_error(error)
139
+ end
140
+ end
141
+ end
142
+
143
+ describe "#close" do
144
+ it "closes the original stream and completes the transaction" do
145
+ expect(stream).to receive(:close)
146
+ expect(Appsignal::Transaction).to receive(:complete_current!)
147
+
148
+ wrapper.close
149
+ end
150
+
151
+ context "when #close raises an error" do
152
+ let(:error) { ExampleException }
153
+
154
+ it "records the exception and completes the transaction" do
155
+ allow(stream).to receive(:close).and_raise(error)
156
+
157
+ expect(transaction).to receive(:set_error).with(error)
158
+ expect(transaction).to receive(:complete)
159
+
160
+ expect { wrapper.send(:close) }.to raise_error(error)
161
+ end
162
+ end
119
163
  end
120
164
  end
@@ -303,7 +303,7 @@ describe Appsignal::Transaction do
303
303
  context "with overridden options" do
304
304
  let(:options) { { :params_method => :filtered_params } }
305
305
 
306
- it "sets the overriden :params_method" do
306
+ it "sets the overridden :params_method" do
307
307
  expect(subject[:params_method]).to eq :filtered_params
308
308
  end
309
309
  end
@@ -751,7 +751,7 @@ describe Appsignal::Transaction do
751
751
  e
752
752
  end
753
753
 
754
- it "should also respond to add_exception for backwords compatibility" do
754
+ it "should also respond to add_exception for backwards compatibility" do
755
755
  expect(transaction).to respond_to(:add_exception)
756
756
  end
757
757
 
@@ -258,7 +258,7 @@ describe Appsignal do
258
258
  end
259
259
 
260
260
  describe ".listen_for_error" do
261
- it "does not record anyhing" do
261
+ it "does not record anything" do
262
262
  error = RuntimeError.new("specific error")
263
263
  expect do
264
264
  Appsignal.listen_for_error do
@@ -204,7 +204,7 @@ RSpec.describe "Puma plugin" do
204
204
  }
205
205
  end
206
206
 
207
- it "collects puma stats as guage metrics with the (summed) worker metrics" do
207
+ it "collects puma stats as gauge metrics with the (summed) worker metrics" do
208
208
  run_plugin(stats_data, appsignal_plugin) do
209
209
  expect(logs).to_not include([:error, kind_of(String)])
210
210
  expect_gauge(:workers, 2, "type" => "count")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appsignal
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.6.1
4
+ version: 3.6.3
5
5
  platform: java
6
6
  authors:
7
7
  - Robert Beekman
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-03-05 00:00:00.000000000 Z
13
+ date: 2024-03-20 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -282,7 +282,6 @@ files:
282
282
  - lib/appsignal/probes/helpers.rb
283
283
  - lib/appsignal/probes/mri.rb
284
284
  - lib/appsignal/probes/sidekiq.rb
285
- - lib/appsignal/rack/body_wrapper.rb
286
285
  - lib/appsignal/rack/generic_instrumentation.rb
287
286
  - lib/appsignal/rack/rails_instrumentation.rb
288
287
  - lib/appsignal/rack/sinatra_instrumentation.rb
@@ -381,7 +380,6 @@ files:
381
380
  - spec/lib/appsignal/probes/gvl_spec.rb
382
381
  - spec/lib/appsignal/probes/mri_spec.rb
383
382
  - spec/lib/appsignal/probes/sidekiq_spec.rb
384
- - spec/lib/appsignal/rack/body_wrapper_spec.rb
385
383
  - spec/lib/appsignal/rack/generic_instrumentation_spec.rb
386
384
  - spec/lib/appsignal/rack/rails_instrumentation_spec.rb
387
385
  - spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb
@@ -467,7 +465,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
467
465
  - !ruby/object:Gem::Version
468
466
  version: '0'
469
467
  requirements: []
470
- rubygems_version: 3.4.15
468
+ rubygems_version: 3.3.7
471
469
  signing_key:
472
470
  specification_version: 4
473
471
  summary: Logs performance and exception data from your app to appsignal.com
@@ -1,161 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Appsignal
4
- # @api private
5
- module Rack
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. This can be done using delegation via i.e. SimpleDelegate
17
- # but we also need "close" to get called correctly so that the Appsignal transaction
18
- # gets completed - which will not happen, for example, when #to_ary gets called
19
- # just on the delegated Rack body.
20
- #
21
- # This comment https://github.com/rails/rails/pull/49627#issuecomment-1769802573
22
- # is of particular interest to understand why this has to be somewhat complicated.
23
- if original_body.respond_to?(:to_path)
24
- PathableBodyWrapper.new(original_body, appsignal_transaction)
25
- elsif original_body.respond_to?(:to_ary)
26
- ArrayableBodyWrapper.new(original_body, appsignal_transaction)
27
- elsif !original_body.respond_to?(:each) && original_body.respond_to?(:call)
28
- # This body only supports #call, so we must be running a Rack 3 application
29
- # It is possible that a body exposes both `each` and `call` in the hopes of
30
- # being backwards-compatible with both Rack 3.x and Rack 2.x, however
31
- # this is not going to work since the SPEC says that if both are available,
32
- # `each` should be used and `call` should be ignored.
33
- # So for that case we can drop by to our default EnumerableBodyWrapper
34
- CallableBodyWrapper.new(original_body, appsignal_transaction)
35
- else
36
- EnumerableBodyWrapper.new(original_body, appsignal_transaction)
37
- end
38
- end
39
-
40
- def initialize(body, appsignal_transaction)
41
- @body_already_closed = false
42
- @body = body
43
- @transaction = appsignal_transaction
44
- end
45
-
46
- # This must be present in all Rack bodies and will be called by the serving adapter
47
- def close
48
- # The @body_already_closed check is needed so that if `to_ary`
49
- # of the body has already closed itself (as prescribed) we do not
50
- # attempt to close it twice
51
- if !@body_already_closed && @body.respond_to?(:close)
52
- Appsignal.instrument("response_body_close.rack") { @body.close }
53
- end
54
- @body_already_closed = true
55
- rescue Exception => error # rubocop:disable Lint/RescueException
56
- @transaction.set_error(error)
57
- raise error
58
- ensure
59
- complete_transaction!
60
- end
61
-
62
- def complete_transaction!
63
- # We need to call the Transaction class method and not
64
- # @transaction.complete because the transaction is still
65
- # thread-local and it needs to remove itself from the
66
- # thread variables correctly, which does not happen on
67
- # Transaction#complete.
68
- #
69
- # In the future it would be a good idea to ensure
70
- # that the current transaction is the same as @transaction,
71
- # or allow @transaction to complete itself and remove
72
- # itself from Thread.current
73
- Appsignal::Transaction.complete_current!
74
- end
75
- end
76
-
77
- # The standard Rack body wrapper which exposes "each" for iterating
78
- # over the response body. This is supported across all 3 major Rack
79
- # versions.
80
- #
81
- # @api private
82
- class EnumerableBodyWrapper < BodyWrapper
83
- def each(&blk)
84
- # This is a workaround for the Rails bug when there was a bit too much
85
- # eagerness in implementing to_ary, see:
86
- # https://github.com/rails/rails/pull/44953
87
- # https://github.com/rails/rails/pull/47092
88
- # https://github.com/rails/rails/pull/49627
89
- # https://github.com/rails/rails/issues/49588
90
- # While the Rack SPEC does not mandate `each` to be callable
91
- # in a blockless way it is still a good idea to have it in place.
92
- return enum_for(:each) unless block_given?
93
-
94
- Appsignal.instrument("process_response_body.rack", "Process Rack response body (#each)") do
95
- @body.each(&blk)
96
- end
97
- rescue Exception => error # rubocop:disable Lint/RescueException
98
- @transaction.set_error(error)
99
- raise error
100
- end
101
- end
102
-
103
- # The callable response bodies are a new Rack 3.x feature, and would not work
104
- # with older Rack versions. They must not respond to `each` because
105
- # "If it responds to each, you must call each and not call". This is why
106
- # it inherits from BodyWrapper directly and not from EnumerableBodyWrapper
107
- #
108
- # @api private
109
- class CallableBodyWrapper < BodyWrapper
110
- def call(stream)
111
- # `stream` will be closed by the app we are calling, no need for us
112
- # to close it ourselves
113
- Appsignal.instrument("process_response_body.rack", "Process Rack response body (#call)") do
114
- @body.call(stream)
115
- end
116
- rescue Exception => error # rubocop:disable Lint/RescueException
117
- @transaction.set_error(error)
118
- raise error
119
- end
120
- end
121
-
122
- # "to_ary" takes precedence over "each" and allows the response body
123
- # to be read eagerly. If the body supports that method, it takes precedence
124
- # over "each":
125
- # "Middleware may call to_ary directly on the Body and return a new Body in its place"
126
- # One could "fold" both the to_ary API and the each() API into one Body object, but
127
- # to_ary must also call "close" after it executes - and in the Rails implementation
128
- # this pecularity was not handled properly.
129
- #
130
- # @api private
131
- class ArrayableBodyWrapper < EnumerableBodyWrapper
132
- def to_ary
133
- @body_already_closed = true
134
- Appsignal.instrument(
135
- "process_response_body.rack",
136
- "Process Rack response body (#to_ary)"
137
- ) do
138
- @body.to_ary
139
- end
140
- rescue Exception => error # rubocop:disable Lint/RescueException
141
- @transaction.set_error(error)
142
- raise error
143
- ensure
144
- # We do not call "close" on ourselves as the only action
145
- # we need to complete is completing the transaction.
146
- complete_transaction!
147
- end
148
- end
149
-
150
- # Having "to_path" on a body allows Rack to serve out a static file, or to
151
- # pass that file to the downstream webserver for sending using X-Sendfile
152
- class PathableBodyWrapper < EnumerableBodyWrapper
153
- def to_path
154
- Appsignal.instrument("response_body_to_path.rack") { @body.to_path }
155
- rescue Exception => error # rubocop:disable Lint/RescueException
156
- @transaction.set_error(error)
157
- raise error
158
- end
159
- end
160
- end
161
- end