appsignal 3.6.1-java → 3.6.2-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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/appsignal/rack/generic_instrumentation.rb +4 -17
- data/lib/appsignal/rack/rails_instrumentation.rb +3 -15
- data/lib/appsignal/rack/sinatra_instrumentation.rb +3 -15
- data/lib/appsignal/rack/streaming_listener.rb +35 -26
- data/lib/appsignal/version.rb +1 -1
- data/lib/appsignal.rb +0 -1
- data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +2 -3
- data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +2 -4
- data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +1 -3
- data/spec/lib/appsignal/rack/streaming_listener_spec.rb +53 -9
- metadata +3 -5
- data/lib/appsignal/rack/body_wrapper.rb +0 -161
- data/spec/lib/appsignal/rack/body_wrapper_spec.rb +0 -220
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3a301e6247ef3ffd84430308f44d847483807a96c04356346fb0a281ad9bea0f
|
4
|
+
data.tar.gz: ae2bd9370beec37c374885e5bf10b5316394243ed73038f5d377553734789597
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1ffcccbd922782d969fabd46828571c9137905674943f0aa790df83fcae7c71cfcc5ac26ab5a6d5227f1e461061ff00da8ccf38ade23bb86fe7c41137774251
|
7
|
+
data.tar.gz: 9e732c7b94dfa76013a8def22d31202feb6c7e4ecffb74a74408e2787ed8ae7b98fd8477b9a2af7242edc35d912d65f2c60a9228ef2ce019574082919ae4d622
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
# AppSignal for Ruby gem Changelog
|
2
2
|
|
3
|
+
## 3.6.2
|
4
|
+
|
5
|
+
_Published on 2024-03-08._
|
6
|
+
|
7
|
+
### Fixed
|
8
|
+
|
9
|
+
- [c3921865](https://github.com/appsignal/appsignal-ruby/commit/c392186573a72fd9afe22299fabcd14dcfe96139) patch - Revert Rack middleware changes (see [changelog](https://github.com/appsignal/appsignal-ruby/blob/main/CHANGELOG.md#360)) to fix issues relating to Unicorn broken pipe errors and multiple requests merging into a single sample.
|
10
|
+
|
3
11
|
## 3.6.1
|
4
12
|
|
5
13
|
_Published on 2024-03-05._
|
@@ -16,9 +16,7 @@ module Appsignal
|
|
16
16
|
if Appsignal.active?
|
17
17
|
call_with_appsignal_monitoring(env)
|
18
18
|
else
|
19
|
-
|
20
|
-
status, headers, obody = @app.call(env)
|
21
|
-
[status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
|
19
|
+
@app.call(env)
|
22
20
|
end
|
23
21
|
end
|
24
22
|
|
@@ -29,30 +27,19 @@ module Appsignal
|
|
29
27
|
Appsignal::Transaction::HTTP_REQUEST,
|
30
28
|
request
|
31
29
|
)
|
32
|
-
# We need to complete the transaction if there is an exception inside the `call`
|
33
|
-
# of the app. If there isn't one and the app returns us a Rack response triplet, we let
|
34
|
-
# the BodyWrapper complete the transaction when #close gets called on it
|
35
|
-
# (guaranteed by the webserver)
|
36
|
-
complete_transaction_without_body = false
|
37
30
|
begin
|
38
31
|
Appsignal.instrument("process_action.generic") do
|
39
|
-
|
40
|
-
[status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
|
32
|
+
@app.call(env)
|
41
33
|
end
|
42
34
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
43
35
|
transaction.set_error(error)
|
44
|
-
complete_transaction_without_body = true
|
45
36
|
raise error
|
46
37
|
ensure
|
47
|
-
|
48
|
-
transaction.set_action_if_nil(default_action)
|
38
|
+
transaction.set_action_if_nil(env["appsignal.route"] || "unknown")
|
49
39
|
transaction.set_metadata("path", request.path)
|
50
40
|
transaction.set_metadata("method", request.request_method)
|
51
41
|
transaction.set_http_or_background_queue_start
|
52
|
-
|
53
|
-
# Transaction gets completed when the body gets read out, except in cases when
|
54
|
-
# the app failed before returning us the Rack response triplet.
|
55
|
-
Appsignal::Transaction.complete_current! if complete_transaction_without_body
|
42
|
+
Appsignal::Transaction.complete_current!
|
56
43
|
end
|
57
44
|
end
|
58
45
|
end
|
@@ -16,9 +16,7 @@ module Appsignal
|
|
16
16
|
if Appsignal.active?
|
17
17
|
call_with_appsignal_monitoring(env)
|
18
18
|
else
|
19
|
-
|
20
|
-
status, headers, obody = @app.call(env)
|
21
|
-
[status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
|
19
|
+
@app.call(env)
|
22
20
|
end
|
23
21
|
end
|
24
22
|
|
@@ -30,17 +28,10 @@ module Appsignal
|
|
30
28
|
request,
|
31
29
|
:params_method => :filtered_parameters
|
32
30
|
)
|
33
|
-
# We need to complete the transaction if there is an exception exception inside the `call`
|
34
|
-
# of the app. If there isn't one and the app returns us a Rack response triplet, we let
|
35
|
-
# the BodyWrapper complete the transaction when #close gets called on it
|
36
|
-
# (guaranteed by the webserver)
|
37
|
-
complete_transaction_without_body = false
|
38
31
|
begin
|
39
|
-
|
40
|
-
[status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
|
32
|
+
@app.call(env)
|
41
33
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
42
34
|
transaction.set_error(error)
|
43
|
-
complete_transaction_without_body = true
|
44
35
|
raise error
|
45
36
|
ensure
|
46
37
|
controller = env["action_controller.instance"]
|
@@ -54,10 +45,7 @@ module Appsignal
|
|
54
45
|
rescue => error
|
55
46
|
Appsignal.internal_logger.error("Unable to report HTTP request method: '#{error}'")
|
56
47
|
end
|
57
|
-
|
58
|
-
# Transaction gets completed when the body gets read out, except in cases when
|
59
|
-
# the app failed before returning us the Rack response triplet.
|
60
|
-
Appsignal::Transaction.complete_current! if complete_transaction_without_body
|
48
|
+
Appsignal::Transaction.complete_current!
|
61
49
|
end
|
62
50
|
end
|
63
51
|
|
@@ -42,9 +42,7 @@ module Appsignal
|
|
42
42
|
if Appsignal.active?
|
43
43
|
call_with_appsignal_monitoring(env)
|
44
44
|
else
|
45
|
-
|
46
|
-
status, headers, obody = @app.call(env)
|
47
|
-
[status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
|
45
|
+
@app.call(env)
|
48
46
|
end
|
49
47
|
end
|
50
48
|
|
@@ -58,19 +56,12 @@ module Appsignal
|
|
58
56
|
request,
|
59
57
|
options
|
60
58
|
)
|
61
|
-
# We need to complete the transaction if there is an exception exception inside the `call`
|
62
|
-
# of the app. If there isn't one and the app returns us a Rack response triplet, we let
|
63
|
-
# the BodyWrapper complete the transaction when #close gets called on it
|
64
|
-
# (guaranteed by the webserver)
|
65
|
-
complete_transaction_without_body = false
|
66
59
|
begin
|
67
60
|
Appsignal.instrument("process_action.sinatra") do
|
68
|
-
|
69
|
-
[status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, transaction)]
|
61
|
+
@app.call(env)
|
70
62
|
end
|
71
63
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
72
64
|
transaction.set_error(error)
|
73
|
-
complete_transaction_without_body = true
|
74
65
|
raise error
|
75
66
|
ensure
|
76
67
|
# If raise_error is off versions of Sinatra don't raise errors, but store
|
@@ -82,10 +73,7 @@ module Appsignal
|
|
82
73
|
transaction.set_metadata("path", request.path)
|
83
74
|
transaction.set_metadata("method", request.request_method)
|
84
75
|
transaction.set_http_or_background_queue_start
|
85
|
-
|
86
|
-
# Transaction gets completed when the body gets read out, except in cases when
|
87
|
-
# the app failed before returning us the Rack response triplet.
|
88
|
-
Appsignal::Transaction.complete_current! if complete_transaction_without_body
|
76
|
+
Appsignal::Transaction.complete_current!
|
89
77
|
end
|
90
78
|
end
|
91
79
|
|
@@ -16,9 +16,7 @@ module Appsignal
|
|
16
16
|
if Appsignal.active?
|
17
17
|
call_with_appsignal_monitoring(env)
|
18
18
|
else
|
19
|
-
|
20
|
-
status, headers, obody = @app.call(env)
|
21
|
-
[status, headers, Appsignal::Rack::BodyWrapper.wrap(obody, nil_transaction)]
|
19
|
+
@app.call(env)
|
22
20
|
end
|
23
21
|
end
|
24
22
|
|
@@ -30,35 +28,46 @@ module Appsignal
|
|
30
28
|
request
|
31
29
|
)
|
32
30
|
|
33
|
-
# We need to complete the transaction if there is an exception exception inside the `call`
|
34
|
-
# of the app. If there isn't one and the app returns us a Rack response triplet, we let
|
35
|
-
# the BodyWrapper complete the transaction when #close gets called on it
|
36
|
-
# (guaranteed by the webserver)
|
37
|
-
complete_transaction_without_body = false
|
38
|
-
|
39
31
|
# Instrument a `process_action`, to set params/action name
|
40
|
-
|
32
|
+
status, headers, body =
|
41
33
|
Appsignal.instrument("process_action.rack") do
|
42
|
-
|
43
|
-
|
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
|
44
43
|
end
|
45
|
-
rescue Exception => error # rubocop:disable Lint/RescueException
|
46
|
-
transaction.set_error(error)
|
47
|
-
complete_transaction_without_body = true
|
48
|
-
raise error
|
49
|
-
ensure
|
50
|
-
transaction.set_action_if_nil(env["appsignal.action"])
|
51
|
-
transaction.set_metadata("path", request.path)
|
52
|
-
transaction.set_metadata("method", request.request_method)
|
53
|
-
transaction.set_http_or_background_queue_start
|
54
44
|
|
55
|
-
|
56
|
-
|
57
|
-
Appsignal::Transaction.complete_current! if complete_transaction_without_body
|
58
|
-
end
|
45
|
+
# Wrap the result body with our StreamWrapper
|
46
|
+
[status, headers, StreamWrapper.new(body, transaction)]
|
59
47
|
end
|
60
48
|
end
|
61
49
|
end
|
62
50
|
|
63
|
-
StreamWrapper
|
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
|
64
73
|
end
|
data/lib/appsignal/version.rb
CHANGED
data/lib/appsignal.rb
CHANGED
@@ -305,6 +305,5 @@ require "appsignal/garbage_collection"
|
|
305
305
|
require "appsignal/integrations/railtie" if defined?(::Rails)
|
306
306
|
require "appsignal/transaction"
|
307
307
|
require "appsignal/version"
|
308
|
-
require "appsignal/rack/body_wrapper"
|
309
308
|
require "appsignal/rack/generic_instrumentation"
|
310
309
|
require "appsignal/transmitter"
|
@@ -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
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
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.
|
4
|
+
version: 3.6.2
|
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-
|
13
|
+
date: 2024-03-08 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.
|
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
|
@@ -1,220 +0,0 @@
|
|
1
|
-
describe Appsignal::Rack::BodyWrapper do
|
2
|
-
let(:nil_txn) { Appsignal::Transaction::NilTransaction.new }
|
3
|
-
|
4
|
-
describe "with a body that supports all possible features" do
|
5
|
-
it "reduces the supported methods to just each()" do
|
6
|
-
# which is the safest thing to do, since the body is likely broken
|
7
|
-
fake_body = double(:each => nil, :call => nil, :to_ary => [], :to_path => "/tmp/foo.bin",
|
8
|
-
:close => nil)
|
9
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
10
|
-
expect(wrapped).to respond_to(:each)
|
11
|
-
expect(wrapped).not_to respond_to(:to_ary)
|
12
|
-
expect(wrapped).not_to respond_to(:call)
|
13
|
-
expect(wrapped).to respond_to(:close)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
describe "with a body only supporting each()" do
|
18
|
-
it "wraps with appropriate class" do
|
19
|
-
fake_body = double
|
20
|
-
allow(fake_body).to receive(:each)
|
21
|
-
|
22
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
23
|
-
expect(wrapped).to respond_to(:each)
|
24
|
-
expect(wrapped).not_to respond_to(:to_ary)
|
25
|
-
expect(wrapped).not_to respond_to(:call)
|
26
|
-
expect(wrapped).to respond_to(:close)
|
27
|
-
end
|
28
|
-
|
29
|
-
it "reads out the body in full using each" do
|
30
|
-
fake_body = double
|
31
|
-
expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
|
32
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
33
|
-
expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
|
34
|
-
end
|
35
|
-
|
36
|
-
it "returns an Enumerator if each() gets called without a block" do
|
37
|
-
fake_body = double
|
38
|
-
expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
|
39
|
-
|
40
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
41
|
-
enum = wrapped.each
|
42
|
-
expect(enum).to be_kind_of(Enumerator)
|
43
|
-
expect { |b| enum.each(&b) }.to yield_successive_args("a", "b", "c")
|
44
|
-
end
|
45
|
-
|
46
|
-
it "sets the exception raised inside each() into the Appsignal transaction" do
|
47
|
-
fake_body = double
|
48
|
-
expect(fake_body).to receive(:each).once.and_raise(Exception.new("Oops"))
|
49
|
-
|
50
|
-
txn = double("Appsignal transaction", "nil_transaction?" => false)
|
51
|
-
expect(txn).to receive(:set_error).once.with(instance_of(Exception))
|
52
|
-
|
53
|
-
wrapped = described_class.wrap(fake_body, txn)
|
54
|
-
expect do
|
55
|
-
expect { |b| wrapped.each(&b) }.to yield_control
|
56
|
-
end.to raise_error(/Oops/)
|
57
|
-
end
|
58
|
-
|
59
|
-
it "closes the body and the transaction when it gets closed" do
|
60
|
-
fake_body = double
|
61
|
-
expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
|
62
|
-
|
63
|
-
txn = double("Appsignal transaction", "nil_transaction?" => false)
|
64
|
-
expect(Appsignal::Transaction).to receive(:complete_current!).once
|
65
|
-
|
66
|
-
wrapped = described_class.wrap(fake_body, txn)
|
67
|
-
expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
|
68
|
-
expect { wrapped.close }.not_to raise_error
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
describe "with a body supporting both each() and call" do
|
73
|
-
it "wraps with the wrapper that conceals call() and exposes each" do
|
74
|
-
fake_body = double
|
75
|
-
allow(fake_body).to receive(:each)
|
76
|
-
allow(fake_body).to receive(:call)
|
77
|
-
|
78
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
79
|
-
expect(wrapped).to respond_to(:each)
|
80
|
-
expect(wrapped).not_to respond_to(:to_ary)
|
81
|
-
expect(wrapped).not_to respond_to(:call)
|
82
|
-
expect(wrapped).not_to respond_to(:to_path)
|
83
|
-
expect(wrapped).to respond_to(:close)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
describe "with a body supporting both to_ary and each" do
|
88
|
-
let(:fake_body) { double(:each => nil, :to_ary => []) }
|
89
|
-
it "wraps with appropriate class" do
|
90
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
91
|
-
expect(wrapped).to respond_to(:each)
|
92
|
-
expect(wrapped).to respond_to(:to_ary)
|
93
|
-
expect(wrapped).not_to respond_to(:call)
|
94
|
-
expect(wrapped).not_to respond_to(:to_path)
|
95
|
-
expect(wrapped).to respond_to(:close)
|
96
|
-
end
|
97
|
-
|
98
|
-
it "reads out the body in full using each" do
|
99
|
-
expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
|
100
|
-
|
101
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
102
|
-
expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
|
103
|
-
end
|
104
|
-
|
105
|
-
it "sets the exception raised inside each() into the Appsignal transaction" do
|
106
|
-
expect(fake_body).to receive(:each).once.and_raise(Exception.new("Oops"))
|
107
|
-
|
108
|
-
txn = double("Appsignal transaction", "nil_transaction?" => false)
|
109
|
-
expect(txn).to receive(:set_error).once.with(instance_of(Exception))
|
110
|
-
|
111
|
-
wrapped = described_class.wrap(fake_body, txn)
|
112
|
-
expect do
|
113
|
-
expect { |b| wrapped.each(&b) }.to yield_control
|
114
|
-
end.to raise_error(/Oops/)
|
115
|
-
end
|
116
|
-
|
117
|
-
it "reads out the body in full using to_ary" do
|
118
|
-
expect(fake_body).to receive(:to_ary).and_return(["one", "two", "three"])
|
119
|
-
|
120
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
121
|
-
expect(wrapped.to_ary).to eq(["one", "two", "three"])
|
122
|
-
end
|
123
|
-
|
124
|
-
it "sends the exception raised inside to_ary() into the Appsignal and closes txn" do
|
125
|
-
fake_body = double
|
126
|
-
allow(fake_body).to receive(:each)
|
127
|
-
expect(fake_body).to receive(:to_ary).once.and_raise(Exception.new("Oops"))
|
128
|
-
expect(fake_body).not_to receive(:close) # Per spec we expect the body has closed itself
|
129
|
-
|
130
|
-
txn = double("Appsignal transaction", "nil_transaction?" => false)
|
131
|
-
expect(txn).to receive(:set_error).once.with(instance_of(Exception))
|
132
|
-
expect(Appsignal::Transaction).to receive(:complete_current!).once
|
133
|
-
|
134
|
-
wrapped = described_class.wrap(fake_body, txn)
|
135
|
-
expect { wrapped.to_ary }.to raise_error(/Oops/)
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
describe "with a body supporting both to_path and each" do
|
140
|
-
let(:fake_body) { double(:each => nil, :to_path => nil) }
|
141
|
-
|
142
|
-
it "wraps with appropriate class" do
|
143
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
144
|
-
expect(wrapped).to respond_to(:each)
|
145
|
-
expect(wrapped).not_to respond_to(:to_ary)
|
146
|
-
expect(wrapped).not_to respond_to(:call)
|
147
|
-
expect(wrapped).to respond_to(:to_path)
|
148
|
-
expect(wrapped).to respond_to(:close)
|
149
|
-
end
|
150
|
-
|
151
|
-
it "reads out the body in full using each()" do
|
152
|
-
expect(fake_body).to receive(:each).once.and_yield("a").and_yield("b").and_yield("c")
|
153
|
-
|
154
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
155
|
-
expect { |b| wrapped.each(&b) }.to yield_successive_args("a", "b", "c")
|
156
|
-
end
|
157
|
-
|
158
|
-
it "sets the exception raised inside each() into the Appsignal transaction" do
|
159
|
-
expect(fake_body).to receive(:each).once.and_raise(Exception.new("Oops"))
|
160
|
-
|
161
|
-
txn = double("Appsignal transaction", "nil_transaction?" => false)
|
162
|
-
expect(txn).to receive(:set_error).once.with(instance_of(Exception))
|
163
|
-
|
164
|
-
wrapped = described_class.wrap(fake_body, txn)
|
165
|
-
expect do
|
166
|
-
expect { |b| wrapped.each(&b) }.to yield_control
|
167
|
-
end.to raise_error(/Oops/)
|
168
|
-
end
|
169
|
-
|
170
|
-
it "sets the exception raised inside to_path() into the Appsignal transaction" do
|
171
|
-
allow(fake_body).to receive(:to_path).once.and_raise(Exception.new("Oops"))
|
172
|
-
|
173
|
-
txn = double("Appsignal transaction", "nil_transaction?" => false)
|
174
|
-
expect(txn).to receive(:set_error).once.with(instance_of(Exception))
|
175
|
-
expect(txn).not_to receive(:complete) # gets called by the caller via close()
|
176
|
-
|
177
|
-
wrapped = described_class.wrap(fake_body, txn)
|
178
|
-
expect { wrapped.to_path }.to raise_error(/Oops/)
|
179
|
-
end
|
180
|
-
|
181
|
-
it "exposes to_path to the sender" do
|
182
|
-
allow(fake_body).to receive(:to_path).and_return("/tmp/file.bin")
|
183
|
-
|
184
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
185
|
-
expect(wrapped.to_path).to eq("/tmp/file.bin")
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
|
-
describe "with a body only supporting call()" do
|
190
|
-
let(:fake_body) { double(:call => nil) }
|
191
|
-
it "wraps with appropriate class" do
|
192
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
193
|
-
expect(wrapped).not_to respond_to(:each)
|
194
|
-
expect(wrapped).not_to respond_to(:to_ary)
|
195
|
-
expect(wrapped).to respond_to(:call)
|
196
|
-
expect(wrapped).not_to respond_to(:to_path)
|
197
|
-
expect(wrapped).to respond_to(:close)
|
198
|
-
end
|
199
|
-
|
200
|
-
it "passes the stream into the call() of the body" do
|
201
|
-
fake_rack_stream = double("stream")
|
202
|
-
expect(fake_body).to receive(:call).with(fake_rack_stream)
|
203
|
-
|
204
|
-
wrapped = described_class.wrap(fake_body, nil_txn)
|
205
|
-
expect { wrapped.call(fake_rack_stream) }.not_to raise_error
|
206
|
-
end
|
207
|
-
|
208
|
-
it "sets the exception raised inside call() into the Appsignal transaction" do
|
209
|
-
fake_rack_stream = double
|
210
|
-
allow(fake_body).to receive(:call).with(fake_rack_stream).and_raise(Exception.new("Oopsie"))
|
211
|
-
|
212
|
-
txn = double("Appsignal transaction", "nil_transaction?" => false)
|
213
|
-
expect(txn).to receive(:set_error).once.with(instance_of(Exception))
|
214
|
-
expect(txn).not_to receive(:complete) # gets called by the caller via close()
|
215
|
-
wrapped = described_class.wrap(fake_body, txn)
|
216
|
-
|
217
|
-
expect { wrapped.call(fake_rack_stream) }.to raise_error(/Oopsie/)
|
218
|
-
end
|
219
|
-
end
|
220
|
-
end
|