appsignal 4.0.4 → 4.0.6

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 (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