appsignal 3.6.1-java → 3.6.3-java

Sign up to get free protection for your applications and to get access to all the features.
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