appsignal 4.0.4 → 4.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +151 -16
  3. data/CHANGELOG.md +42 -0
  4. data/build_matrix.yml +2 -1
  5. data/ext/agent.rb +27 -27
  6. data/gemfiles/que-1.gemfile +5 -0
  7. data/gemfiles/que-2.gemfile +5 -0
  8. data/lib/appsignal/check_in/scheduler.rb +3 -4
  9. data/lib/appsignal/config.rb +7 -0
  10. data/lib/appsignal/hooks/at_exit.rb +1 -0
  11. data/lib/appsignal/hooks/puma.rb +5 -1
  12. data/lib/appsignal/integrations/puma.rb +45 -0
  13. data/lib/appsignal/integrations/que.rb +8 -2
  14. data/lib/appsignal/rack/abstract_middleware.rb +3 -47
  15. data/lib/appsignal/rack/event_handler.rb +2 -0
  16. data/lib/appsignal/rack/hanami_middleware.rb +5 -1
  17. data/lib/appsignal/rack.rb +68 -0
  18. data/lib/appsignal/version.rb +1 -1
  19. data/spec/lib/appsignal/check_in/cron_spec.rb +134 -134
  20. data/spec/lib/appsignal/check_in/scheduler_spec.rb +297 -224
  21. data/spec/lib/appsignal/config_spec.rb +13 -0
  22. data/spec/lib/appsignal/hooks/at_exit_spec.rb +11 -0
  23. data/spec/lib/appsignal/hooks/puma_spec.rb +31 -23
  24. data/spec/lib/appsignal/integrations/puma_spec.rb +150 -0
  25. data/spec/lib/appsignal/integrations/que_spec.rb +56 -21
  26. data/spec/lib/appsignal/probes_spec.rb +4 -6
  27. data/spec/lib/appsignal/rack/abstract_middleware_spec.rb +41 -122
  28. data/spec/lib/appsignal/rack_spec.rb +180 -0
  29. data/spec/lib/appsignal/transaction_spec.rb +96 -92
  30. data/spec/spec_helper.rb +6 -7
  31. data/spec/support/helpers/api_request_helper.rb +40 -0
  32. data/spec/support/helpers/config_helpers.rb +2 -1
  33. data/spec/support/helpers/dependency_helper.rb +5 -0
  34. data/spec/support/matchers/contains_log.rb +10 -3
  35. data/spec/support/mocks/hash_like.rb +10 -0
  36. data/spec/support/mocks/puma_mock.rb +43 -0
  37. data/spec/support/testing.rb +9 -0
  38. metadata +8 -3
  39. data/gemfiles/que.gemfile +0 -5
@@ -1,5 +1,18 @@
1
1
  describe Appsignal::Config do
2
2
  describe ".add_loader_defaults" do
3
+ context "when the config is initialized" do
4
+ before { Appsignal.configure(:test) }
5
+
6
+ it "logs a warning" do
7
+ logs = capture_logs { described_class.add_loader_defaults(:loader1) }
8
+
9
+ expect(logs).to contains_log(
10
+ :warn,
11
+ "The config defaults from the 'loader1' loader are ignored"
12
+ )
13
+ end
14
+ end
15
+
3
16
  it "adds loader defaults to the list" do
4
17
  described_class.add_loader_defaults(:loader1)
5
18
 
@@ -40,6 +40,17 @@ describe Appsignal::Hooks::AtExit::AtExitCallback do
40
40
  Appsignal::Hooks::AtExit::AtExitCallback.call
41
41
  end
42
42
 
43
+ it "reports no transaction if the process didn't exit with an error" do
44
+ logs =
45
+ capture_logs do
46
+ expect do
47
+ call_callback
48
+ end.to_not(change { created_transactions.count })
49
+ end
50
+
51
+ expect(logs).to_not contains_log(:error, "Appsignal.report_error: Cannot add error.")
52
+ end
53
+
43
54
  it "reports an error if there's an unhandled error" do
44
55
  expect do
45
56
  with_error(ExampleException, "error message") do
@@ -1,34 +1,45 @@
1
1
  describe Appsignal::Hooks::PumaHook do
2
2
  context "with puma" do
3
- before(:context) do
4
- class Puma
5
- def self.stats
6
- end
3
+ let(:puma_version) { "6.0.0" }
4
+ before do
5
+ stub_const("Puma", PumaMock)
6
+ stub_const("Puma::Const::VERSION", puma_version)
7
+ end
8
+
9
+ describe "#dependencies_present?" do
10
+ subject { described_class.new.dependencies_present? }
11
+
12
+ context "when Puma present" do
13
+ context "when Puma is newer than version 3.0.0" do
14
+ let(:puma_version) { "3.0.0" }
7
15
 
8
- def self.cli_config
9
- @cli_config ||= CliConfig.new
16
+ it { is_expected.to be_truthy }
10
17
  end
11
- end
12
18
 
13
- class CliConfig
14
- attr_accessor :options
19
+ context "when Puma is older than version 3.0.0" do
20
+ let(:puma_version) { "2.9.9" }
15
21
 
16
- def initialize
17
- @options = {}
22
+ it { is_expected.to be_falsey }
18
23
  end
19
24
  end
20
- end
21
- after(:context) { Object.send(:remove_const, :Puma) }
22
25
 
23
- describe "#dependencies_present?" do
24
- subject { described_class.new.dependencies_present? }
26
+ context "when Puma is not present" do
27
+ before do
28
+ hide_const("Puma")
29
+ end
25
30
 
26
- it { is_expected.to be_truthy }
31
+ it { is_expected.to be_falsey }
32
+ end
27
33
  end
28
34
 
29
35
  describe "installation" do
30
36
  before { Appsignal::Probes.probes.clear }
31
37
 
38
+ it "adds the Puma::Server patch" do
39
+ Appsignal::Hooks::PumaHook.new.install
40
+ expect(::Puma::Server.included_modules).to include(Appsignal::Integrations::PumaServer)
41
+ end
42
+
32
43
  context "when not clustered mode" do
33
44
  it "does not add AppSignal stop behavior Puma::Cluster" do
34
45
  expect(defined?(::Puma::Cluster)).to be_falsy
@@ -39,15 +50,12 @@ describe Appsignal::Hooks::PumaHook do
39
50
 
40
51
  context "when in clustered mode" do
41
52
  before do
42
- class Puma
43
- class Cluster
44
- def stop_workers
45
- @called = true
46
- end
53
+ stub_const("Puma::Cluster", Class.new do
54
+ def stop_workers
55
+ @called = true
47
56
  end
48
- end
57
+ end)
49
58
  end
50
- after { Puma.send(:remove_const, :Cluster) }
51
59
 
52
60
  it "adds behavior to Puma::Cluster.stop_workers" do
53
61
  Appsignal::Hooks::PumaHook.new.install
@@ -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
@@ -9,28 +9,16 @@ if DependencyHelper.que_present?
9
9
  :queue => "dfl",
10
10
  :job_class => "MyQueJob",
11
11
  :priority => 100,
12
- :args => %w[1 birds],
12
+ :args => %w[post_id_123 user_id_123],
13
13
  :run_at => fixed_time,
14
14
  :error_count => 0
15
- }
16
- end
17
- let(:env) do
18
- {
19
- :class => "MyQueJob",
20
- :method => "run",
21
- :metadata => {
22
- :id => 123,
23
- :queue => "dfl",
24
- :priority => 100,
25
- :run_at => fixed_time.to_s,
26
- :attempts => 0
27
- },
28
- :params => %w[1 birds]
29
- }
15
+ }.tap do |hash|
16
+ hash[:kwargs] = {} if DependencyHelper.que2_present?
17
+ end
30
18
  end
31
19
  let(:job) do
32
20
  Class.new(::Que::Job) do
33
- def run(*args)
21
+ def run(post_id, user_id)
34
22
  end
35
23
  end
36
24
  end
@@ -46,7 +34,7 @@ if DependencyHelper.que_present?
46
34
  job._run
47
35
  end
48
36
 
49
- context "success" do
37
+ context "without exception" do
50
38
  it "creates a transaction for a job" do
51
39
  expect do
52
40
  perform_que_job(instance)
@@ -64,7 +52,18 @@ if DependencyHelper.que_present?
64
52
  "name" => "perform_job.que",
65
53
  "title" => ""
66
54
  )
67
- expect(transaction).to include_params(%w[1 birds])
55
+ expect(transaction).to include_params(
56
+ "arguments" => %w[post_id_123 user_id_123]
57
+ )
58
+ if DependencyHelper.que2_present?
59
+ expect(transaction).to include_params(
60
+ "keyword_arguments" => {}
61
+ )
62
+ else
63
+ expect(transaction).to_not include_params(
64
+ "keyword_arguments" => anything
65
+ )
66
+ end
68
67
  expect(transaction).to include_tags(
69
68
  "attempts" => 0,
70
69
  "id" => 123,
@@ -93,7 +92,9 @@ if DependencyHelper.que_present?
93
92
  expect(transaction).to have_action("MyQueJob#run")
94
93
  expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
95
94
  expect(transaction).to have_error(error.class.name, error.message)
96
- expect(transaction).to include_params(%w[1 birds])
95
+ expect(transaction).to include_params(
96
+ "arguments" => %w[post_id_123 user_id_123]
97
+ )
97
98
  expect(transaction).to include_tags(
98
99
  "attempts" => 0,
99
100
  "id" => 123,
@@ -118,7 +119,9 @@ if DependencyHelper.que_present?
118
119
  expect(transaction).to have_action("MyQueJob#run")
119
120
  expect(transaction).to have_namespace(Appsignal::Transaction::BACKGROUND_JOB)
120
121
  expect(transaction).to have_error(error.class.name, error.message)
121
- expect(transaction).to include_params(%w[1 birds])
122
+ expect(transaction).to include_params(
123
+ "arguments" => %w[post_id_123 user_id_123]
124
+ )
122
125
  expect(transaction).to include_tags(
123
126
  "attempts" => 0,
124
127
  "id" => 123,
@@ -130,6 +133,38 @@ if DependencyHelper.que_present?
130
133
  end
131
134
  end
132
135
 
136
+ if DependencyHelper.que2_present?
137
+ context "with keyword argument" do
138
+ let(:job_attrs) do
139
+ {
140
+ :job_id => 123,
141
+ :queue => "dfl",
142
+ :job_class => "MyQueJob",
143
+ :priority => 100,
144
+ :args => %w[post_id_123],
145
+ :kwargs => { :user_id => "user_id_123" },
146
+ :run_at => fixed_time,
147
+ :error_count => 0
148
+ }
149
+ end
150
+ let(:job) do
151
+ Class.new(::Que::Job) do
152
+ def run(post_id, user_id: nil)
153
+ end
154
+ end
155
+ end
156
+
157
+ it "reports keyword arguments as parameters" do
158
+ perform_que_job(instance)
159
+
160
+ expect(last_transaction).to include_params(
161
+ "arguments" => %w[post_id_123],
162
+ "keyword_arguments" => { "user_id" => "user_id_123" }
163
+ )
164
+ end
165
+ end
166
+ end
167
+
133
168
  context "when action set in job" do
134
169
  let(:job) do
135
170
  Class.new(::Que::Job) do
@@ -43,6 +43,9 @@ describe Appsignal::Probes do
43
43
  let(:log) { log_contents(log_stream) }
44
44
  before do
45
45
  Appsignal.internal_logger = test_logger(log_stream)
46
+ # TODO: These logs are here to debug an issue on CI
47
+ Appsignal.internal_logger.info("a" * 100)
48
+ Appsignal.internal_logger.info("b" * 100)
46
49
  speed_up_tests!
47
50
  end
48
51
 
@@ -244,12 +247,7 @@ describe Appsignal::Probes do
244
247
  end
245
248
 
246
249
  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
250
+ before { speed_up_tests! }
253
251
 
254
252
  it "does not call the initialized probe after unregistering" do
255
253
  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