appsignal 4.0.3-java → 4.0.5-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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/ext/agent.rb +27 -27
  4. data/lib/appsignal/check_in/cron.rb +2 -34
  5. data/lib/appsignal/check_in/scheduler.rb +192 -0
  6. data/lib/appsignal/check_in.rb +18 -0
  7. data/lib/appsignal/cli/diagnose.rb +1 -1
  8. data/lib/appsignal/config.rb +7 -0
  9. data/lib/appsignal/hooks/at_exit.rb +3 -1
  10. data/lib/appsignal/hooks/puma.rb +5 -1
  11. data/lib/appsignal/integrations/puma.rb +45 -0
  12. data/lib/appsignal/rack/abstract_middleware.rb +3 -47
  13. data/lib/appsignal/rack/body_wrapper.rb +15 -0
  14. data/lib/appsignal/rack/event_handler.rb +2 -0
  15. data/lib/appsignal/rack/hanami_middleware.rb +5 -1
  16. data/lib/appsignal/rack.rb +68 -0
  17. data/lib/appsignal/transmitter.rb +30 -7
  18. data/lib/appsignal/utils/ndjson.rb +15 -0
  19. data/lib/appsignal/utils.rb +1 -0
  20. data/lib/appsignal/version.rb +1 -1
  21. data/lib/appsignal.rb +1 -0
  22. data/spec/lib/appsignal/check_in/cron_spec.rb +202 -0
  23. data/spec/lib/appsignal/check_in/scheduler_spec.rb +443 -0
  24. data/spec/lib/appsignal/config_spec.rb +13 -0
  25. data/spec/lib/appsignal/environment_spec.rb +1 -1
  26. data/spec/lib/appsignal/hooks/at_exit_spec.rb +22 -0
  27. data/spec/lib/appsignal/hooks/puma_spec.rb +31 -23
  28. data/spec/lib/appsignal/integrations/puma_spec.rb +150 -0
  29. data/spec/lib/appsignal/probes_spec.rb +1 -6
  30. data/spec/lib/appsignal/rack/abstract_middleware_spec.rb +41 -122
  31. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +29 -21
  32. data/spec/lib/appsignal/rack_spec.rb +180 -0
  33. data/spec/lib/appsignal/transmitter_spec.rb +48 -2
  34. data/spec/lib/appsignal_spec.rb +5 -0
  35. data/spec/spec_helper.rb +0 -7
  36. data/spec/support/helpers/config_helpers.rb +2 -1
  37. data/spec/support/helpers/take_at_most_helper.rb +21 -0
  38. data/spec/support/matchers/contains_log.rb +10 -3
  39. data/spec/support/mocks/hash_like.rb +10 -0
  40. data/spec/support/mocks/puma_mock.rb +43 -0
  41. metadata +11 -3
  42. data/spec/lib/appsignal/check_in_spec.rb +0 -136
@@ -0,0 +1,150 @@
1
+ require "appsignal/integrations/puma"
2
+
3
+ describe Appsignal::Integrations::PumaServer do
4
+ describe "#lowlevel_error" do
5
+ before do
6
+ stub_const("Puma", PumaMock)
7
+ stub_const("Puma::Server", puma_server)
8
+ start_agent
9
+ end
10
+ let(:queue_start_time) { fixed_time * 1_000 }
11
+ let(:env) do
12
+ Rack::MockRequest.env_for(
13
+ "/some/path",
14
+ "REQUEST_METHOD" => "GET",
15
+ :params => { "page" => 2, "query" => "lorem" },
16
+ "rack.session" => { "session" => "data", "user_id" => 123 },
17
+ "HTTP_X_REQUEST_START" => "t=#{queue_start_time.to_i}" # in milliseconds
18
+ )
19
+ end
20
+ let(:server) { Puma::Server.new }
21
+ let(:error) { ExampleException.new("error message") }
22
+ around { |example| keep_transactions { example.run } }
23
+ before { Appsignal::Hooks::PumaHook.new.install }
24
+
25
+ def lowlevel_error(error, env, status = nil)
26
+ result =
27
+ if status
28
+ server.lowlevel_error(error, env, status)
29
+ else
30
+ server.lowlevel_error(error, env)
31
+ end
32
+ # Transaction is normally closed by the EventHandler's RACK_AFTER_REPLY hook
33
+ last_transaction&.complete
34
+ result
35
+ end
36
+
37
+ describe "error reporting" do
38
+ let(:puma_server) { default_puma_server_mock }
39
+
40
+ context "with active transaction" do
41
+ before { create_transaction }
42
+
43
+ it "reports the error to the transaction" do
44
+ expect do
45
+ lowlevel_error(error, env)
46
+ end.to_not(change { created_transactions.count })
47
+
48
+ expect(last_transaction).to have_error("ExampleException", "error message")
49
+ expect(last_transaction).to include_tags("reported_by" => "puma_lowlevel_error")
50
+ end
51
+ end
52
+
53
+ # This shouldn't happen if the EventHandler is set up correctly, but if
54
+ # it's not it will create a new transaction.
55
+ context "without active transaction" do
56
+ it "creates a new transaction with the error" do
57
+ expect do
58
+ lowlevel_error(error, env)
59
+ end.to change { created_transactions.count }.by(1)
60
+
61
+ expect(last_transaction).to have_error("ExampleException", "error message")
62
+ expect(last_transaction).to include_tags("reported_by" => "puma_lowlevel_error")
63
+ end
64
+ end
65
+
66
+ it "doesn't report internal Puma errors" do
67
+ expect do
68
+ lowlevel_error(Puma::MiniSSL::SSLError.new("error message"), env)
69
+ lowlevel_error(Puma::HttpParserError.new("error message"), env)
70
+ lowlevel_error(Puma::HttpParserError501.new("error message"), env)
71
+ end.to_not(change { created_transactions.count })
72
+ end
73
+
74
+ describe "request metadata" do
75
+ it "sets request metadata" do
76
+ lowlevel_error(error, env)
77
+
78
+ expect(last_transaction).to include_metadata(
79
+ "request_method" => "GET",
80
+ "method" => "GET",
81
+ "request_path" => "/some/path",
82
+ "path" => "/some/path"
83
+ )
84
+ expect(last_transaction).to include_environment(
85
+ "REQUEST_METHOD" => "GET",
86
+ "PATH_INFO" => "/some/path"
87
+ # and more, but we don't need to test Rack mock defaults
88
+ )
89
+ end
90
+
91
+ it "sets request parameters" do
92
+ lowlevel_error(error, env)
93
+
94
+ expect(last_transaction).to include_params(
95
+ "page" => "2",
96
+ "query" => "lorem"
97
+ )
98
+ end
99
+
100
+ it "sets session data" do
101
+ lowlevel_error(error, env)
102
+
103
+ expect(last_transaction).to include_session_data("session" => "data", "user_id" => 123)
104
+ end
105
+
106
+ it "sets the queue start" do
107
+ lowlevel_error(error, env)
108
+
109
+ expect(last_transaction).to have_queue_start(queue_start_time)
110
+ end
111
+ end
112
+ end
113
+
114
+ context "with Puma::Server#lowlevel_error accepting 3 arguments" do
115
+ let(:puma_server) { default_puma_server_mock }
116
+
117
+ it "calls the super class with 3 arguments" do
118
+ result = lowlevel_error(error, env, 501)
119
+ expect(result).to eq([501, {}, ""])
120
+
121
+ expect(last_transaction).to include_tags("response_status" => 501)
122
+ end
123
+ end
124
+
125
+ context "with Puma::Server#lowlevel_error accepting 2 arguments" do
126
+ let(:puma_server) do
127
+ Class.new do
128
+ def lowlevel_error(_error, _env)
129
+ [500, {}, ""]
130
+ end
131
+ end
132
+ end
133
+
134
+ it "calls the super class with 3 arguments" do
135
+ result = lowlevel_error(error, env)
136
+ expect(result).to eq([500, {}, ""])
137
+
138
+ expect(last_transaction).to include_tags("response_status" => 500)
139
+ end
140
+ end
141
+ end
142
+
143
+ def default_puma_server_mock
144
+ Class.new do
145
+ def lowlevel_error(_error, _env, status = 500)
146
+ [status, {}, ""]
147
+ end
148
+ end
149
+ end
150
+ end
@@ -244,12 +244,7 @@ describe Appsignal::Probes do
244
244
  end
245
245
 
246
246
  describe ".unregister" do
247
- let(:log_stream) { StringIO.new }
248
- let(:log) { log_contents(log_stream) }
249
- before do
250
- Appsignal.internal_logger = test_logger(log_stream)
251
- speed_up_tests!
252
- end
247
+ before { speed_up_tests! }
253
248
 
254
249
  it "does not call the initialized probe after unregistering" do
255
250
  probe1_calls = 0
@@ -1,19 +1,8 @@
1
1
  describe Appsignal::Rack::AbstractMiddleware do
2
- class HashLike < Hash
3
- def initialize(value)
4
- @value = value
5
- end
6
-
7
- def to_h
8
- @value
9
- end
10
- end
11
-
12
2
  let(:app) { DummyApp.new }
13
- let(:request_path) { "/some/path" }
14
3
  let(:env) do
15
4
  Rack::MockRequest.env_for(
16
- request_path,
5
+ "/some/path",
17
6
  "REQUEST_METHOD" => "GET",
18
7
  :params => { "page" => 2, "query" => "lorem" },
19
8
  "rack.session" => { "session" => "data", "user_id" => 123 }
@@ -174,13 +163,17 @@ describe Appsignal::Rack::AbstractMiddleware do
174
163
  end
175
164
  end
176
165
 
166
+ # Partial duplicate tests from Appsignal::Rack::ApplyRackRequest that
167
+ # ensure the request metadata is set on via the AbstractMiddleware.
177
168
  describe "request metadata" do
178
169
  it "sets request metadata" do
179
170
  env.merge!("PATH_INFO" => "/some/path", "REQUEST_METHOD" => "GET")
180
171
  make_request
181
172
 
182
173
  expect(last_transaction).to include_metadata(
174
+ "request_method" => "GET",
183
175
  "method" => "GET",
176
+ "request_path" => "/some/path",
184
177
  "path" => "/some/path"
185
178
  )
186
179
  expect(last_transaction).to include_environment(
@@ -190,36 +183,6 @@ describe Appsignal::Rack::AbstractMiddleware do
190
183
  )
191
184
  end
192
185
 
193
- context "with an invalid HTTP request method" do
194
- it "stores the invalid HTTP request method" do
195
- env["REQUEST_METHOD"] = "FOO"
196
- make_request
197
-
198
- expect(last_transaction).to include_metadata("method" => "FOO")
199
- end
200
- end
201
-
202
- context "when fetching the request method raises an error" do
203
- class BrokenRequestMethodRequest < Rack::Request
204
- def request_method
205
- raise "uh oh!"
206
- end
207
- end
208
-
209
- let(:options) { { :request_class => BrokenRequestMethodRequest } }
210
-
211
- it "does not store the invalid HTTP request method" do
212
- env["REQUEST_METHOD"] = "FOO"
213
- logs = capture_logs { make_request }
214
-
215
- expect(last_transaction).to_not include_metadata("method" => anything)
216
- expect(logs).to contains_log(
217
- :error,
218
- "Exception while fetching the HTTP request method: RuntimeError: uh oh"
219
- )
220
- end
221
- end
222
-
223
186
  it "sets request parameters" do
224
187
  make_request
225
188
 
@@ -229,107 +192,63 @@ describe Appsignal::Rack::AbstractMiddleware do
229
192
  )
230
193
  end
231
194
 
232
- context "when setting custom params" do
233
- let(:app) do
234
- DummyApp.new do |_env|
235
- Appsignal::Transaction.current.set_params("custom" => "param")
236
- end
237
- end
238
-
239
- it "allow custom request parameters to be set" do
240
- make_request
241
-
242
- expect(last_transaction).to include_params("custom" => "param")
243
- end
244
- end
245
-
246
- context "when fetching the request method raises an error" do
247
- class BrokenRequestParamsRequest < Rack::Request
248
- def params
249
- raise "uh oh!"
250
- end
251
- end
252
-
253
- let(:options) do
254
- { :request_class => BrokenRequestParamsRequest, :params_method => :params }
255
- end
256
-
257
- it "does not store the invalid HTTP request method" do
258
- logs = capture_logs { make_request }
259
-
260
- expect(last_transaction).to_not include_params
261
- expect(logs).to contains_log(
262
- :error,
263
- "Exception while fetching params " \
264
- "from 'BrokenRequestParamsRequest#params': RuntimeError uh oh!"
265
- )
266
- end
267
- end
268
-
269
195
  it "sets session data" do
270
196
  make_request
271
197
 
272
198
  expect(last_transaction).to include_session_data("session" => "data", "user_id" => 123)
273
199
  end
274
200
 
275
- it "sets session data if the session is a Hash-like type" do
276
- env["rack.session"] = HashLike.new("hash-like" => "value", "user_id" => 123)
277
- make_request
278
-
279
- expect(last_transaction).to include_session_data("hash-like" => "value", "user_id" => 123)
280
- end
281
- end
282
-
283
- context "with queue start header" do
284
- let(:queue_start_time) { fixed_time * 1_000 }
201
+ context "with queue start header" do
202
+ let(:queue_start_time) { fixed_time * 1_000 }
285
203
 
286
- it "sets the queue start" do
287
- env["HTTP_X_REQUEST_START"] = "t=#{queue_start_time.to_i}" # in milliseconds
288
- make_request
204
+ it "sets the queue start" do
205
+ env["HTTP_X_REQUEST_START"] = "t=#{queue_start_time.to_i}" # in milliseconds
206
+ make_request
289
207
 
290
- expect(last_transaction).to have_queue_start(queue_start_time)
208
+ expect(last_transaction).to have_queue_start(queue_start_time)
209
+ end
291
210
  end
292
- end
293
211
 
294
- class FilteredRequest
295
- attr_reader :env
212
+ class SomeFilteredRequest
213
+ attr_reader :env
296
214
 
297
- def initialize(env)
298
- @env = env
299
- end
215
+ def initialize(env)
216
+ @env = env
217
+ end
300
218
 
301
- def path
302
- "/static/path"
303
- end
219
+ def path
220
+ "/static/path"
221
+ end
304
222
 
305
- def request_method
306
- "GET"
307
- end
223
+ def request_method
224
+ "GET"
225
+ end
308
226
 
309
- def filtered_params
310
- { "abc" => "123" }
311
- end
227
+ def filtered_params
228
+ { "abc" => "123" }
229
+ end
312
230
 
313
- def session
314
- { "data" => "value" }
231
+ def session
232
+ { "data" => "value" }
233
+ end
315
234
  end
316
- end
317
235
 
318
- context "with overridden request class and params method" do
319
- let(:options) do
320
- { :request_class => FilteredRequest, :params_method => :filtered_params }
321
- end
236
+ context "with overridden request class and params method" do
237
+ let(:options) do
238
+ { :request_class => SomeFilteredRequest, :params_method => :filtered_params }
239
+ end
322
240
 
323
- it "uses the overridden request class and params method to fetch params" do
324
- make_request
241
+ it "uses the overridden request class and params method to fetch params" do
242
+ make_request
325
243
 
326
- expect(last_transaction).to include_params("abc" => "123")
327
- end
244
+ expect(last_transaction).to include_params("abc" => "123")
245
+ end
328
246
 
329
- it "uses the overridden request class to fetch session data" do
330
- make_request
247
+ it "uses the overridden request class to fetch session data" do
248
+ make_request
331
249
 
332
- expect(last_transaction).to include_session_data("data" => "value")
250
+ expect(last_transaction).to include_session_data("data" => "value")
251
+ end
333
252
  end
334
253
  end
335
254
 
@@ -5,23 +5,29 @@ describe Appsignal::Rack::BodyWrapper do
5
5
  set_current_transaction(transaction)
6
6
  end
7
7
 
8
- describe "with a body that supports all possible features" do
9
- it "reduces the supported methods to just each()" do
10
- # which is the safest thing to do, since the body is likely broken
11
- fake_body = double(
12
- :each => nil,
13
- :call => nil,
14
- :to_ary => [],
15
- :to_path => "/tmp/foo.bin",
16
- :close => nil
17
- )
8
+ it "forwards method calls to the body if the method doesn't exist" do
9
+ fake_body = double(
10
+ :body => ["some body"],
11
+ :some_method => :some_value
12
+ )
13
+
14
+ wrapped = described_class.wrap(fake_body, transaction)
15
+ expect(wrapped).to respond_to(:body)
16
+ expect(wrapped.body).to eq(["some body"])
17
+
18
+ expect(wrapped).to respond_to(:some_method)
19
+ expect(wrapped.some_method).to eq(:some_value)
20
+ end
18
21
 
19
- wrapped = described_class.wrap(fake_body, transaction)
20
- expect(wrapped).to respond_to(:each)
21
- expect(wrapped).to_not respond_to(:to_ary)
22
- expect(wrapped).to_not respond_to(:call)
23
- expect(wrapped).to respond_to(:close)
24
- end
22
+ it "doesn't respond to methods the Rack::BodyProxy doesn't respond to" do
23
+ body = Rack::BodyProxy.new(["body"])
24
+ wrapped = described_class.wrap(body, transaction)
25
+
26
+ expect(wrapped).to_not respond_to(:to_str)
27
+ expect { wrapped.to_str }.to raise_error(NoMethodError)
28
+
29
+ expect(wrapped).to_not respond_to(:body)
30
+ expect { wrapped.body }.to raise_error(NoMethodError)
25
31
  end
26
32
 
27
33
  describe "with a body only supporting each()" do
@@ -97,15 +103,17 @@ describe Appsignal::Rack::BodyWrapper do
97
103
  end
98
104
 
99
105
  describe "with a body supporting both each() and call" do
100
- it "wraps with the wrapper that conceals call() and exposes each" do
101
- fake_body = double
102
- allow(fake_body).to receive(:each)
103
- allow(fake_body).to receive(:call)
106
+ it "wraps with the wrapper that exposes each" do
107
+ fake_body = double(
108
+ :each => true,
109
+ :call => "original call"
110
+ )
104
111
 
105
112
  wrapped = described_class.wrap(fake_body, transaction)
106
113
  expect(wrapped).to respond_to(:each)
107
114
  expect(wrapped).to_not respond_to(:to_ary)
108
- expect(wrapped).to_not respond_to(:call)
115
+ expect(wrapped).to respond_to(:call)
116
+ expect(wrapped.call).to eq("original call")
109
117
  expect(wrapped).to_not respond_to(:to_path)
110
118
  expect(wrapped).to respond_to(:close)
111
119
  end
@@ -61,3 +61,183 @@ describe Appsignal::Rack::Utils do
61
61
  end
62
62
  end
63
63
  end
64
+
65
+ describe Appsignal::Rack::ApplyRackRequest do
66
+ describe "#apply_to" do
67
+ let(:merged_env) do
68
+ Rack::MockRequest.env_for(
69
+ "/some/path",
70
+ {
71
+ "REQUEST_METHOD" => "GET",
72
+ :params => { "page" => 2, "query" => "lorem" },
73
+ "rack.session" => { "session" => "data", "user_id" => 123 }
74
+ }.merge(env)
75
+ )
76
+ end
77
+ let(:env) { {} }
78
+ let(:request) { ::Rack::Request.new(merged_env) }
79
+ let(:options) { {} }
80
+ let(:helper) { described_class.new(request, options) }
81
+ let(:transaction) { http_request_transaction }
82
+ before { start_agent }
83
+
84
+ def apply_to(transaction)
85
+ helper.apply_to(transaction)
86
+ transaction._sample
87
+ end
88
+
89
+ it "sets request metadata" do
90
+ apply_to(transaction)
91
+
92
+ expect(transaction).to include_metadata(
93
+ "method" => "GET",
94
+ "path" => "/some/path"
95
+ )
96
+ expect(transaction).to include_environment(
97
+ "REQUEST_METHOD" => "GET",
98
+ "PATH_INFO" => "/some/path"
99
+ # and more, but we don't need to test Rack mock defaults
100
+ )
101
+ end
102
+
103
+ context "with an invalid HTTP request method" do
104
+ let(:env) { { "REQUEST_METHOD" => "FOO" } }
105
+
106
+ it "stores the invalid HTTP request method" do
107
+ apply_to(transaction)
108
+
109
+ expect(transaction).to include_metadata("method" => "FOO")
110
+ end
111
+ end
112
+
113
+ context "when fetching the request method raises an error" do
114
+ class BrokenRequestMethodRequest < Rack::Request
115
+ def request_method
116
+ raise "uh oh!"
117
+ end
118
+ end
119
+
120
+ let(:env) { { "REQUEST_METHOD" => "FOO" } }
121
+ let(:request) { BrokenRequestMethodRequest.new(merged_env) }
122
+
123
+ it "does not store the invalid HTTP request method" do
124
+ logs = capture_logs { apply_to(transaction) }
125
+
126
+ expect(transaction).to_not include_metadata("method" => anything)
127
+ expect(logs).to contains_log(
128
+ :error,
129
+ "Exception while fetching the HTTP request method: RuntimeError: uh oh"
130
+ )
131
+ end
132
+ end
133
+
134
+ it "sets request parameters" do
135
+ apply_to(transaction)
136
+
137
+ expect(transaction).to include_params(
138
+ "page" => "2",
139
+ "query" => "lorem"
140
+ )
141
+ end
142
+
143
+ context "when params_method isn't set" do
144
+ let(:options) { { :params_method => nil } }
145
+
146
+ it "reports no params" do
147
+ apply_to(transaction)
148
+
149
+ expect(transaction).to_not include_params
150
+ end
151
+ end
152
+
153
+ context "when fetching the request method raises an error" do
154
+ class BrokenRequestParamsRequest < Rack::Request
155
+ def params
156
+ raise "uh oh!"
157
+ end
158
+ end
159
+
160
+ let(:request) { BrokenRequestParamsRequest.new(merged_env) }
161
+ let(:options) { { :params_method => :params } }
162
+
163
+ it "does not store the invalid HTTP request method" do
164
+ logs = capture_logs { apply_to(transaction) }
165
+
166
+ expect(transaction).to_not include_params
167
+ expect(logs).to contains_log(
168
+ :error,
169
+ "Exception while fetching params " \
170
+ "from 'BrokenRequestParamsRequest#params': RuntimeError uh oh!"
171
+ )
172
+ end
173
+ end
174
+
175
+ it "sets session data" do
176
+ apply_to(transaction)
177
+
178
+ expect(transaction).to include_session_data("session" => "data", "user_id" => 123)
179
+ end
180
+
181
+ context "with Hash-like session data" do
182
+ let(:env) { { "rack.session" => HashLike.new("hash-like" => "value", "user_id" => 123) } }
183
+
184
+ it "sets session data" do
185
+ apply_to(transaction)
186
+
187
+ expect(transaction).to include_session_data("hash-like" => "value", "user_id" => 123)
188
+ end
189
+ end
190
+
191
+ context "with queue start header" do
192
+ let(:queue_start_time) { fixed_time * 1_000 }
193
+ let(:env) { { "HTTP_X_REQUEST_START" => "t=#{queue_start_time.to_i}" } } # in milliseconds
194
+
195
+ it "sets the queue start" do
196
+ apply_to(transaction)
197
+
198
+ expect(transaction).to have_queue_start(queue_start_time)
199
+ end
200
+ end
201
+
202
+ class RackFilteredRequest
203
+ attr_reader :env
204
+
205
+ def initialize(env)
206
+ @env = env
207
+ end
208
+
209
+ def path
210
+ "/static/path"
211
+ end
212
+
213
+ def request_method
214
+ "GET"
215
+ end
216
+
217
+ def filtered_params
218
+ { "abc" => "123" }
219
+ end
220
+
221
+ def session
222
+ { "data" => "value" }
223
+ end
224
+ end
225
+
226
+ context "with overridden request class and params method" do
227
+ let(:request) { RackFilteredRequest.new(env) }
228
+ let(:options) { { :params_method => :filtered_params } }
229
+
230
+ it "uses the overridden request class and params method to fetch params" do
231
+ apply_to(transaction)
232
+
233
+ expect(transaction).to include_params("abc" => "123")
234
+ end
235
+
236
+ it "uses the overridden request class to fetch session data" do
237
+ apply_to(transaction)
238
+
239
+ expect(transaction).to include_session_data("data" => "value")
240
+ end
241
+ end
242
+ end
243
+ end