appsignal 2.10.8 → 2.11.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +75 -61
  4. data/CHANGELOG.md +21 -0
  5. data/build_matrix.yml +13 -7
  6. data/ext/agent.yml +19 -19
  7. data/ext/appsignal_extension.c +10 -1
  8. data/ext/base.rb +11 -2
  9. data/gemfiles/padrino.gemfile +2 -2
  10. data/gemfiles/rails-4.2.gemfile +9 -2
  11. data/gemfiles/rails-5.0.gemfile +1 -0
  12. data/gemfiles/rails-5.1.gemfile +1 -0
  13. data/gemfiles/rails-5.2.gemfile +1 -0
  14. data/gemfiles/rails-6.0.gemfile +1 -0
  15. data/gemfiles/resque-1.gemfile +7 -0
  16. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  17. data/lib/appsignal.rb +21 -1
  18. data/lib/appsignal/capistrano.rb +2 -0
  19. data/lib/appsignal/config.rb +6 -2
  20. data/lib/appsignal/environment.rb +126 -0
  21. data/lib/appsignal/extension/jruby.rb +10 -0
  22. data/lib/appsignal/hooks.rb +2 -0
  23. data/lib/appsignal/hooks/active_job.rb +89 -0
  24. data/lib/appsignal/hooks/net_http.rb +2 -0
  25. data/lib/appsignal/hooks/puma.rb +2 -58
  26. data/lib/appsignal/hooks/redis.rb +2 -0
  27. data/lib/appsignal/hooks/resque.rb +60 -0
  28. data/lib/appsignal/hooks/sequel.rb +2 -0
  29. data/lib/appsignal/hooks/sidekiq.rb +18 -191
  30. data/lib/appsignal/integrations/object.rb +4 -0
  31. data/lib/appsignal/integrations/que.rb +1 -1
  32. data/lib/appsignal/integrations/resque.rb +9 -12
  33. data/lib/appsignal/integrations/resque_active_job.rb +9 -24
  34. data/lib/appsignal/probes/puma.rb +61 -0
  35. data/lib/appsignal/probes/sidekiq.rb +102 -0
  36. data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
  37. data/lib/appsignal/transaction.rb +32 -7
  38. data/lib/appsignal/utils/deprecation_message.rb +5 -1
  39. data/lib/appsignal/version.rb +1 -1
  40. data/lib/puma/plugin/appsignal.rb +2 -1
  41. data/spec/lib/appsignal/cli/diagnose_spec.rb +2 -1
  42. data/spec/lib/appsignal/config_spec.rb +6 -1
  43. data/spec/lib/appsignal/environment_spec.rb +167 -0
  44. data/spec/lib/appsignal/hooks/activejob_spec.rb +458 -0
  45. data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
  46. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  47. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +292 -546
  48. data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
  49. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  50. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -137
  51. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  52. data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
  53. data/spec/lib/appsignal/probes/sidekiq_spec.rb +204 -0
  54. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
  55. data/spec/lib/appsignal/transaction_spec.rb +35 -20
  56. data/spec/lib/appsignal_spec.rb +22 -0
  57. data/spec/lib/puma/appsignal_spec.rb +1 -1
  58. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  59. data/spec/support/helpers/dependency_helper.rb +12 -0
  60. data/spec/support/helpers/env_helpers.rb +1 -1
  61. data/spec/support/helpers/environment_metdata_helper.rb +16 -0
  62. data/spec/support/helpers/transaction_helpers.rb +6 -0
  63. data/spec/support/stubs/sidekiq/api.rb +2 -2
  64. metadata +25 -5
@@ -55,7 +55,7 @@ describe Appsignal::Hooks::PumaHook do
55
55
 
56
56
  Appsignal::Hooks::PumaHook.new.install
57
57
  probe = Appsignal::Minutely.probes[:puma]
58
- expect(probe).to eql(Appsignal::Hooks::PumaProbe)
58
+ expect(probe).to eql(Appsignal::Probes::PumaProbe)
59
59
  end
60
60
  end
61
61
  end
@@ -101,7 +101,7 @@ describe Appsignal::Hooks::PumaHook do
101
101
 
102
102
  Appsignal::Hooks::PumaHook.new.install
103
103
  probe = Appsignal::Minutely.probes[:puma]
104
- expect(probe).to eql(Appsignal::Hooks::PumaProbe)
104
+ expect(probe).to eql(Appsignal::Probes::PumaProbe)
105
105
  end
106
106
  end
107
107
  end
@@ -116,182 +116,3 @@ describe Appsignal::Hooks::PumaHook do
116
116
  end
117
117
  end
118
118
  end
119
-
120
- describe Appsignal::Hooks::PumaProbe do
121
- before(:context) do
122
- Appsignal.config = project_fixture_config
123
- end
124
- after(:context) do
125
- Appsignal.config = nil
126
- end
127
-
128
- let(:probe) { Appsignal::Hooks::PumaProbe.new }
129
-
130
- describe "hostname" do
131
- it "returns the socket hostname" do
132
- expect(probe.send(:hostname)).to eql(Socket.gethostname)
133
- end
134
-
135
- context "with overridden hostname" do
136
- around do |sample|
137
- Appsignal.config[:hostname] = "frontend1"
138
- sample.run
139
- Appsignal.config[:hostname] = nil
140
- end
141
- it "returns the configured host" do
142
- expect(probe.send(:hostname)).to eql("frontend1")
143
- end
144
- end
145
- end
146
-
147
- describe "#call" do
148
- let(:expected_default_tags) { { :hostname => Socket.gethostname } }
149
-
150
- context "with multiple worker stats" do
151
- before(:context) do
152
- class Puma
153
- def self.stats
154
- {
155
- "workers" => 2,
156
- "booted_workers" => 2,
157
- "old_workers" => 0,
158
- "worker_status" => [
159
- {
160
- "last_status" => {
161
- "backlog" => 0,
162
- "running" => 5,
163
- "pool_capacity" => 5,
164
- "max_threads" => 5
165
- }
166
- },
167
- {
168
- "last_status" => {
169
- "backlog" => 0,
170
- "running" => 5,
171
- "pool_capacity" => 5,
172
- "max_threads" => 5
173
- }
174
- }
175
- ]
176
- }.to_json
177
- end
178
- end
179
- end
180
- after(:context) { Object.send(:remove_const, :Puma) }
181
-
182
- it "calls `puma_gauge` with the (summed) worker metrics" do
183
- expect_gauge(:workers, 2, :type => :count)
184
- expect_gauge(:workers, 2, :type => :booted)
185
- expect_gauge(:workers, 0, :type => :old)
186
-
187
- expect_gauge(:connection_backlog, 0)
188
- expect_gauge(:pool_capacity, 10)
189
- expect_gauge(:threads, 10, :type => :running)
190
- expect_gauge(:threads, 10, :type => :max)
191
-
192
- probe.call
193
- end
194
- end
195
-
196
- context "with single worker stats" do
197
- before(:context) do
198
- class Puma
199
- def self.stats
200
- {
201
- "backlog" => 0,
202
- "running" => 5,
203
- "pool_capacity" => 5,
204
- "max_threads" => 5
205
- }.to_json
206
- end
207
- end
208
- end
209
- after(:context) { Object.send(:remove_const, :Puma) }
210
-
211
- it "calls `puma_gauge` with the (summed) worker metrics" do
212
- expect_gauge(:connection_backlog, 0)
213
- expect_gauge(:pool_capacity, 5)
214
- expect_gauge(:threads, 5, :type => :running)
215
- expect_gauge(:threads, 5, :type => :max)
216
- probe.call
217
- end
218
- end
219
-
220
- context "without stats" do
221
- before(:context) do
222
- class Puma
223
- def self.stats
224
- end
225
- end
226
- end
227
- after(:context) { Object.send(:remove_const, :Puma) }
228
-
229
- context "when it returns nil" do
230
- it "does not track metrics" do
231
- expect(probe).to_not receive(:puma_gauge)
232
- probe.call
233
- end
234
- end
235
-
236
- # Puma.stats raises a NoMethodError on a nil object on the first call.
237
- context "when it returns a NoMethodError on the first call" do
238
- let(:log) { StringIO.new }
239
-
240
- it "ignores the first call and tracks the second call" do
241
- use_logger_with log do
242
- expect(Puma).to receive(:stats)
243
- .and_raise(NoMethodError.new("undefined method `stats' for nil:NilClass"))
244
- probe.call
245
-
246
- expect(Puma).to receive(:stats).and_return({
247
- "backlog" => 1,
248
- "running" => 5,
249
- "pool_capacity" => 4,
250
- "max_threads" => 6
251
- }.to_json)
252
-
253
- expect_gauge(:connection_backlog, 1)
254
- expect_gauge(:pool_capacity, 4)
255
- expect_gauge(:threads, 5, :type => :running)
256
- expect_gauge(:threads, 6, :type => :max)
257
- probe.call
258
- end
259
-
260
- expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
261
- end
262
- end
263
-
264
- context "when it does not have a complete stats payload" do
265
- let(:log) { StringIO.new }
266
-
267
- it "tracks whatever metrics we do have" do
268
- use_logger_with log do
269
- expect(Puma).to receive(:stats).and_return({
270
- "backlog" => 1,
271
- "running" => 5
272
- }.to_json)
273
-
274
- expect_gauge(:connection_backlog, 1)
275
- expect_no_gauge(:pool_capacity)
276
- expect_gauge(:threads, 5, :type => :running)
277
- expect_no_gauge(:threads, :type => :max)
278
- probe.call
279
- end
280
-
281
- expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
282
- end
283
- end
284
- end
285
-
286
- def expect_gauge(key, value, tags = {})
287
- expect(Appsignal).to receive(:set_gauge)
288
- .with("puma_#{key}", value, expected_default_tags.merge(tags))
289
- .and_call_original
290
- end
291
-
292
- def expect_no_gauge(key, tags = {})
293
- expect(Appsignal).to_not receive(:set_gauge)
294
- .with("puma_#{key}", anything, tags)
295
- end
296
- end
297
- end
@@ -0,0 +1,185 @@
1
+ describe Appsignal::Hooks::ResqueHook do
2
+ describe "#dependency_present?" do
3
+ subject { described_class.new.dependencies_present? }
4
+
5
+ context "when Resque is loaded" do
6
+ before { stub_const "Resque", 1 }
7
+
8
+ it { is_expected.to be_truthy }
9
+ end
10
+
11
+ context "when Resque is not loaded" do
12
+ before { hide_const "Resque" }
13
+
14
+ it { is_expected.to be_falsy }
15
+ end
16
+ end
17
+
18
+ if DependencyHelper.resque_present?
19
+ describe "#install" do
20
+ def perform_job(klass, options = {})
21
+ payload = { "class" => klass.to_s }.merge(options)
22
+ job = ::Resque::Job.new(queue, payload)
23
+ keep_transactions { job.perform }
24
+ end
25
+
26
+ let(:queue) { "default" }
27
+ let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
28
+ before do
29
+ start_agent
30
+
31
+ class ResqueTestJob
32
+ def self.perform(*_args)
33
+ end
34
+ end
35
+
36
+ class ResqueErrorTestJob
37
+ def self.perform
38
+ raise "resque job error"
39
+ end
40
+ end
41
+
42
+ expect(Appsignal).to receive(:stop) # Resque calls stop after every job
43
+ end
44
+ around do |example|
45
+ keep_transactions { example.run }
46
+ end
47
+ after do
48
+ Object.send(:remove_const, :ResqueTestJob)
49
+ Object.send(:remove_const, :ResqueErrorTestJob)
50
+ end
51
+
52
+ it "tracks a transaction on perform" do
53
+ perform_job(ResqueTestJob)
54
+
55
+ transaction = last_transaction
56
+ transaction_hash = transaction.to_h
57
+ expect(transaction_hash).to include(
58
+ "id" => kind_of(String),
59
+ "action" => "ResqueTestJob#perform",
60
+ "error" => nil,
61
+ "namespace" => namespace,
62
+ "metadata" => {},
63
+ "sample_data" => { "tags" => { "queue" => queue } }
64
+ )
65
+ expect(transaction_hash["events"].map { |e| e["name"] })
66
+ .to eql(["perform.resque"])
67
+ end
68
+
69
+ context "with error" do
70
+ it "tracks the error on the transaction" do
71
+ expect do
72
+ perform_job(ResqueErrorTestJob)
73
+ end.to raise_error(RuntimeError, "resque job error")
74
+
75
+ transaction = last_transaction
76
+ transaction_hash = transaction.to_h
77
+ expect(transaction_hash).to include(
78
+ "id" => kind_of(String),
79
+ "action" => "ResqueErrorTestJob#perform",
80
+ "error" => {
81
+ "name" => "RuntimeError",
82
+ "message" => "resque job error",
83
+ "backtrace" => kind_of(String)
84
+ },
85
+ "namespace" => namespace,
86
+ "metadata" => {},
87
+ "sample_data" => { "tags" => { "queue" => queue } }
88
+ )
89
+ end
90
+ end
91
+
92
+ context "with arguments" do
93
+ before do
94
+ Appsignal.config = project_fixture_config("production")
95
+ Appsignal.config[:filter_parameters] = ["foo"]
96
+ end
97
+
98
+ it "filters out configured arguments" do
99
+ perform_job(
100
+ ResqueTestJob,
101
+ "args" => [
102
+ "foo",
103
+ {
104
+ "foo" => "secret",
105
+ "bar" => "Bar",
106
+ "baz" => { "1" => "foo" }
107
+ }
108
+ ]
109
+ )
110
+
111
+ transaction = last_transaction
112
+ transaction_hash = transaction.to_h
113
+ expect(transaction_hash).to include(
114
+ "id" => kind_of(String),
115
+ "action" => "ResqueTestJob#perform",
116
+ "error" => nil,
117
+ "namespace" => namespace,
118
+ "metadata" => {},
119
+ "sample_data" => {
120
+ "tags" => { "queue" => queue },
121
+ "params" => [
122
+ "foo",
123
+ {
124
+ "foo" => "[FILTERED]",
125
+ "bar" => "Bar",
126
+ "baz" => { "1" => "foo" }
127
+ }
128
+ ]
129
+ }
130
+ )
131
+ end
132
+ end
133
+
134
+ context "with active job" do
135
+ before do
136
+ module ActiveJobMock
137
+ module QueueAdapters
138
+ module ResqueAdapter
139
+ module JobWrapper
140
+ class << self
141
+ def perform(job_data)
142
+ # Basic ActiveJob stub for this test.
143
+ # I haven't found a way to run Resque in a testing mode.
144
+ Appsignal.set_action(job_data["job_class"])
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ stub_const "ActiveJob", ActiveJobMock
153
+ end
154
+ after { Object.send(:remove_const, :ActiveJobMock) }
155
+
156
+ it "does not set arguments but lets the ActiveJob intergration handle it" do
157
+ perform_job(
158
+ ResqueTestJob,
159
+ "class" => "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper",
160
+ "args" => [
161
+ {
162
+ "job_class" => "ResqueTestJobByActiveJob#perform",
163
+ "arguments" => ["an argument", "second argument"]
164
+ }
165
+ ]
166
+ )
167
+
168
+ transaction = last_transaction
169
+ transaction_hash = transaction.to_h
170
+ expect(transaction_hash).to include(
171
+ "id" => kind_of(String),
172
+ "action" => "ResqueTestJobByActiveJob#perform",
173
+ "error" => nil,
174
+ "namespace" => namespace,
175
+ "metadata" => {},
176
+ "sample_data" => {
177
+ "tags" => { "queue" => queue }
178
+ # Params will be set by the ActiveJob integration
179
+ }
180
+ )
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -1,3 +1,54 @@
1
+ describe Appsignal::Hooks::SidekiqHook do
2
+ describe "#dependencies_present?" do
3
+ subject { described_class.new.dependencies_present? }
4
+
5
+ context "when Sidekiq constant is found" do
6
+ before { stub_const "Sidekiq", Class.new }
7
+
8
+ it { is_expected.to be_truthy }
9
+ end
10
+
11
+ context "when Sidekiq constant is not found" do
12
+ before { hide_const "Sidekiq" }
13
+
14
+ it { is_expected.to be_falsy }
15
+ end
16
+ end
17
+
18
+ describe "#install" do
19
+ class SidekiqMiddlewareMock < Set
20
+ def exists?(middleware)
21
+ include?(middleware)
22
+ end
23
+ end
24
+ module SidekiqMock
25
+ def self.middlewares
26
+ @middlewares ||= SidekiqMiddlewareMock.new
27
+ end
28
+
29
+ def self.configure_server
30
+ yield self
31
+ end
32
+
33
+ def self.server_middleware
34
+ yield middlewares if block_given?
35
+ middlewares
36
+ end
37
+ end
38
+
39
+ before do
40
+ Appsignal.config = project_fixture_config
41
+ stub_const "Sidekiq", SidekiqMock
42
+ end
43
+
44
+ it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain" do
45
+ described_class.new.install
46
+
47
+ expect(Sidekiq.server_middleware.exists?(Appsignal::Hooks::SidekiqPlugin)).to be(true)
48
+ end
49
+ end
50
+ end
51
+
1
52
  describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
2
53
  class DelayedTestClass; end
3
54
 
@@ -48,369 +99,132 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
48
99
  expect(log_contents(log)).to_not contains_log(:warn, "Unable to load YAML")
49
100
  end
50
101
 
51
- shared_examples "sidekiq metadata" do
52
- describe "internal Sidekiq job values" do
53
- it "does not save internal Sidekiq values as metadata on transaction" do
54
- perform_job
102
+ describe "internal Sidekiq job values" do
103
+ it "does not save internal Sidekiq values as metadata on transaction" do
104
+ perform_job
55
105
 
56
- transaction_hash = transaction.to_h
57
- expect(transaction_hash["metadata"].keys)
58
- .to_not include(*Appsignal::Hooks::SidekiqPlugin::JOB_KEYS)
59
- end
106
+ transaction_hash = transaction.to_h
107
+ expect(transaction_hash["metadata"].keys)
108
+ .to_not include(*Appsignal::Hooks::SidekiqPlugin::EXCLUDED_JOB_KEYS)
60
109
  end
110
+ end
61
111
 
62
- context "with parameter filtering" do
63
- before do
64
- Appsignal.config = project_fixture_config("production")
65
- Appsignal.config[:filter_parameters] = ["foo"]
66
- end
67
-
68
- it "filters selected arguments" do
69
- perform_job
70
-
71
- transaction_hash = transaction.to_h
72
- expect(transaction_hash["sample_data"]).to include(
73
- "params" => [
74
- "foo",
75
- {
76
- "foo" => "[FILTERED]",
77
- "bar" => "Bar",
78
- "baz" => { "1" => "foo" }
79
- }
80
- ]
81
- )
82
- end
112
+ context "with parameter filtering" do
113
+ before do
114
+ Appsignal.config = project_fixture_config("production")
115
+ Appsignal.config[:filter_parameters] = ["foo"]
83
116
  end
84
117
 
85
- context "with encrypted arguments" do
86
- before do
87
- item["encrypt"] = true
88
- item["args"] << "super secret value" # Last argument will be replaced
89
- end
90
-
91
- it "replaces the last argument (the secret bag) with an [encrypted data] string" do
92
- perform_job
118
+ it "filters selected arguments" do
119
+ perform_job
93
120
 
94
- transaction_hash = transaction.to_h
95
- expect(transaction_hash["sample_data"]).to include(
96
- "params" => expected_args << "[encrypted data]"
97
- )
98
- end
121
+ transaction_hash = transaction.to_h
122
+ expect(transaction_hash["sample_data"]).to include(
123
+ "params" => [
124
+ "foo",
125
+ {
126
+ "foo" => "[FILTERED]",
127
+ "bar" => "Bar",
128
+ "baz" => { "1" => "foo" }
129
+ }
130
+ ]
131
+ )
99
132
  end
133
+ end
100
134
 
101
- context "when using the Sidekiq delayed extension" do
102
- let(:item) do
103
- {
104
- "jid" => "efb140489485999d32b5504c",
105
- "class" => "Sidekiq::Extensions::DelayedClass",
106
- "queue" => "default",
107
- "args" => [
108
- "---\n- !ruby/class 'DelayedTestClass'\n- :foo_method\n- - :bar: baz\n"
109
- ],
110
- "retry" => true,
111
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
112
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
113
- "extra" => "data"
114
- }
115
- end
116
-
117
- it "uses the delayed class and method name for the action" do
118
- perform_job
119
-
120
- transaction_hash = transaction.to_h
121
- expect(transaction_hash["action"]).to eq("DelayedTestClass.foo_method")
122
- expect(transaction_hash["sample_data"]).to include(
123
- "params" => ["bar" => "baz"]
124
- )
125
- end
126
-
127
- context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
128
- before { item["args"] = [] }
129
-
130
- it "logs a warning and uses the default argument" do
131
- perform_job
132
-
133
- transaction_hash = transaction.to_h
134
- expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedClass#perform")
135
- expect(transaction_hash["sample_data"]).to include("params" => [])
136
- expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
137
- end
138
- end
135
+ context "with encrypted arguments" do
136
+ before do
137
+ item["encrypt"] = true
138
+ item["args"] << "super secret value" # Last argument will be replaced
139
139
  end
140
140
 
141
- context "when using the Sidekiq ActiveRecord instance delayed extension" do
142
- let(:item) do
143
- {
144
- "jid" => "efb140489485999d32b5504c",
145
- "class" => "Sidekiq::Extensions::DelayedModel",
146
- "queue" => "default",
147
- "args" => [
148
- "---\n- !ruby/object:DelayedTestClass {}\n- :foo_method\n- - :bar: :baz\n"
149
- ],
150
- "retry" => true,
151
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
152
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
153
- "extra" => "data"
154
- }
155
- end
156
-
157
- it "uses the delayed class and method name for the action" do
158
- perform_job
141
+ it "replaces the last argument (the secret bag) with an [encrypted data] string" do
142
+ perform_job
159
143
 
160
- transaction_hash = transaction.to_h
161
- expect(transaction_hash["action"]).to eq("DelayedTestClass#foo_method")
162
- expect(transaction_hash["sample_data"]).to include(
163
- "params" => ["bar" => "baz"]
164
- )
165
- end
144
+ transaction_hash = transaction.to_h
145
+ expect(transaction_hash["sample_data"]).to include(
146
+ "params" => expected_args << "[encrypted data]"
147
+ )
148
+ end
149
+ end
166
150
 
167
- context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
168
- before { item["args"] = [] }
151
+ context "when using the Sidekiq delayed extension" do
152
+ let(:item) do
153
+ {
154
+ "jid" => "efb140489485999d32b5504c",
155
+ "class" => "Sidekiq::Extensions::DelayedClass",
156
+ "queue" => "default",
157
+ "args" => [
158
+ "---\n- !ruby/class 'DelayedTestClass'\n- :foo_method\n- - :bar: baz\n"
159
+ ],
160
+ "retry" => true,
161
+ "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
162
+ "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
163
+ "extra" => "data"
164
+ }
165
+ end
169
166
 
170
- it "logs a warning and uses the default argument" do
171
- perform_job
167
+ it "uses the delayed class and method name for the action" do
168
+ perform_job
172
169
 
173
- transaction_hash = transaction.to_h
174
- expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedModel#perform")
175
- expect(transaction_hash["sample_data"]).to include("params" => [])
176
- expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
177
- end
178
- end
170
+ transaction_hash = transaction.to_h
171
+ expect(transaction_hash["action"]).to eq("DelayedTestClass.foo_method")
172
+ expect(transaction_hash["sample_data"]).to include(
173
+ "params" => ["bar" => "baz"]
174
+ )
179
175
  end
180
176
 
181
- context "when using ActiveJob" do
182
- let(:item) do
183
- {
184
- "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
185
- "wrapped" => "ActiveJobTestClass",
186
- "queue" => "default",
187
- "args" => [{
188
- "job_class" => "ActiveJobTestJob",
189
- "job_id" => "23e79d48-6966-40d0-b2d4-f7938463a263",
190
- "queue_name" => "default",
191
- "arguments" => [
192
- "foo", { "foo" => "Foo", "bar" => "Bar", "baz" => { 1 => :bar } }
193
- ]
194
- }],
195
- "retry" => true,
196
- "jid" => "efb140489485999d32b5504c",
197
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
198
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f
199
- }
200
- end
177
+ context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
178
+ before { item["args"] = [] }
201
179
 
202
- it "creates a transaction with events" do
180
+ it "logs a warning and uses the default argument" do
203
181
  perform_job
204
182
 
205
183
  transaction_hash = transaction.to_h
206
- expect(transaction_hash).to include(
207
- "id" => kind_of(String),
208
- "action" => "ActiveJobTestClass#perform",
209
- "error" => nil,
210
- "namespace" => namespace,
211
- "metadata" => {
212
- "queue" => "default"
213
- },
214
- "sample_data" => {
215
- "environment" => {},
216
- "params" => [
217
- "foo",
218
- {
219
- "foo" => "Foo",
220
- "bar" => "Bar",
221
- "baz" => { "1" => "bar" }
222
- }
223
- ],
224
- "tags" => {}
225
- }
226
- )
227
- # TODO: Not available in transaction.to_h yet.
228
- # https://github.com/appsignal/appsignal-agent/issues/293
229
- expect(transaction.request.env).to eq(
230
- :queue_start => Time.parse("2001-01-01 10:00:00UTC").to_f
231
- )
232
- expect_transaction_to_have_sidekiq_event(transaction_hash)
233
- end
234
-
235
- context "with ActionMailer job" do
236
- let(:item) do
237
- {
238
- "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
239
- "wrapped" => "ActionMailer::DeliveryJob",
240
- "queue" => "default",
241
- "args" => [{
242
- "job_class" => "ActiveMailerTestJob",
243
- "job_id" => "23e79d48-6966-40d0-b2d4-f7938463a263",
244
- "queue_name" => "default",
245
- "arguments" => [
246
- "MailerClass", "mailer_method", "deliver_now",
247
- "foo", { "foo" => "Foo", "bar" => "Bar", "baz" => { 1 => :bar } }
248
- ]
249
- }],
250
- "retry" => true,
251
- "jid" => "efb140489485999d32b5504c",
252
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
253
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f
254
- }
255
- end
256
-
257
- it "creates a transaction for the ActionMailer class" do
258
- perform_job
259
-
260
- transaction_hash = transaction.to_h
261
- expect(transaction_hash).to include(
262
- "id" => kind_of(String),
263
- "action" => "MailerClass#mailer_method",
264
- "error" => nil,
265
- "namespace" => namespace,
266
- "metadata" => {
267
- "queue" => "default"
268
- },
269
- "sample_data" => {
270
- "environment" => {},
271
- "params" => [
272
- "foo",
273
- {
274
- "foo" => "Foo",
275
- "bar" => "Bar",
276
- "baz" => { "1" => "bar" }
277
- }
278
- ],
279
- "tags" => {}
280
- }
281
- )
282
- end
283
- end
284
-
285
- context "with parameter filtering" do
286
- before do
287
- Appsignal.config = project_fixture_config("production")
288
- Appsignal.config[:filter_parameters] = ["foo"]
289
- end
290
-
291
- it "filters selected arguments" do
292
- perform_job
293
-
294
- transaction_hash = transaction.to_h
295
- expect(transaction_hash["sample_data"]).to include(
296
- "params" => [
297
- "foo",
298
- {
299
- "foo" => "[FILTERED]",
300
- "bar" => "Bar",
301
- "baz" => { "1" => "bar" }
302
- }
303
- ]
304
- )
305
- end
306
- end
307
-
308
- context "when Sidekiq job payload is missing the 'wrapped' value" do
309
- let(:item) do
310
- {
311
- "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
312
- "queue" => "default",
313
- "args" => [first_argument],
314
- "retry" => true,
315
- "jid" => "efb140489485999d32b5504c",
316
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
317
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f
318
- }
319
- end
320
- before { perform_job }
321
-
322
- context "when the first argument is not a Hash object" do
323
- let(:first_argument) { "foo" }
324
-
325
- include_examples "unknown job action name"
326
- end
327
-
328
- context "when the first argument is a Hash object not containing a job payload" do
329
- let(:first_argument) { { "foo" => "bar" } }
330
-
331
- include_examples "unknown job action name"
332
-
333
- context "when the argument contains an invalid job_class value" do
334
- let(:first_argument) { { "job_class" => :foo } }
335
-
336
- include_examples "unknown job action name"
337
- end
338
- end
339
-
340
- context "when the first argument is a Hash object containing a job payload" do
341
- let(:first_argument) do
342
- {
343
- "job_class" => "ActiveMailerTestJob",
344
- "job_id" => "23e79d48-6966-40d0-b2d4-f7938463a263",
345
- "queue_name" => "default",
346
- "arguments" => [
347
- "foo", { "foo" => "Foo", "bar" => "Bar", "baz" => { 1 => :bar } }
348
- ]
349
- }
350
- end
351
-
352
- it "sets the action name to the job class in the first argument" do
353
- transaction_hash = transaction.to_h
354
- expect(transaction_hash).to include(
355
- "action" => "ActiveMailerTestJob#perform"
356
- )
357
- end
358
-
359
- it "stores the job metadata on the transaction" do
360
- transaction_hash = transaction.to_h
361
- expect(transaction_hash).to include(
362
- "id" => kind_of(String),
363
- "error" => nil,
364
- "namespace" => namespace,
365
- "metadata" => {
366
- "queue" => "default"
367
- },
368
- "sample_data" => {
369
- "environment" => {},
370
- "params" => [
371
- "foo",
372
- {
373
- "foo" => "Foo",
374
- "bar" => "Bar",
375
- "baz" => { "1" => "bar" }
376
- }
377
- ],
378
- "tags" => {}
379
- }
380
- )
381
- end
382
-
383
- it "does not log a debug message" do
384
- expect(log_contents(log)).to_not contains_log(
385
- :debug, "Unable to determine an action name from Sidekiq payload"
386
- )
387
- end
388
- end
184
+ expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedClass#perform")
185
+ expect(transaction_hash["sample_data"]).to include("params" => [])
186
+ expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
389
187
  end
390
188
  end
391
189
  end
392
190
 
393
- shared_examples "unknown job action name" do
394
- it "sets the action name to unknown" do
395
- transaction_hash = transaction.to_h
396
- expect(transaction_hash).to include("action" => "unknown")
191
+ context "when using the Sidekiq ActiveRecord instance delayed extension" do
192
+ let(:item) do
193
+ {
194
+ "jid" => "efb140489485999d32b5504c",
195
+ "class" => "Sidekiq::Extensions::DelayedModel",
196
+ "queue" => "default",
197
+ "args" => [
198
+ "---\n- !ruby/object:DelayedTestClass {}\n- :foo_method\n- - :bar: :baz\n"
199
+ ],
200
+ "retry" => true,
201
+ "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
202
+ "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
203
+ "extra" => "data"
204
+ }
397
205
  end
398
206
 
399
- it "stores no sample data" do
207
+ it "uses the delayed class and method name for the action" do
208
+ perform_job
209
+
400
210
  transaction_hash = transaction.to_h
401
- expect(transaction_hash).to include(
402
- "sample_data" => {
403
- "environment" => {},
404
- "params" => [],
405
- "tags" => {}
406
- }
211
+ expect(transaction_hash["action"]).to eq("DelayedTestClass#foo_method")
212
+ expect(transaction_hash["sample_data"]).to include(
213
+ "params" => ["bar" => "baz"]
407
214
  )
408
215
  end
409
216
 
410
- it "logs a debug message" do
411
- expect(log_contents(log)).to contains_log(
412
- :debug, "Unable to determine an action name from Sidekiq payload: #{item}"
413
- )
217
+ context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
218
+ before { item["args"] = [] }
219
+
220
+ it "logs a warning and uses the default argument" do
221
+ perform_job
222
+
223
+ transaction_hash = transaction.to_h
224
+ expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedModel#perform")
225
+ expect(transaction_hash["sample_data"]).to include("params" => [])
226
+ expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
227
+ end
414
228
  end
415
229
  end
416
230
 
@@ -452,8 +266,6 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
452
266
  )
453
267
  expect_transaction_to_have_sidekiq_event(transaction_hash)
454
268
  end
455
-
456
- include_examples "sidekiq metadata"
457
269
  end
458
270
 
459
271
  context "without an error" do
@@ -487,8 +299,6 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
487
299
  )
488
300
  expect_transaction_to_have_sidekiq_event(transaction_hash)
489
301
  end
490
-
491
- include_examples "sidekiq metadata"
492
302
  end
493
303
 
494
304
  def perform_job
@@ -516,247 +326,183 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
516
326
  end
517
327
  end
518
328
 
519
- describe Appsignal::Hooks::SidekiqHook do
520
- describe "#dependencies_present?" do
521
- subject { described_class.new.dependencies_present? }
522
-
523
- context "when Sidekiq constant is found" do
524
- before { Object.const_set("Sidekiq", 1) }
525
- after { Object.send(:remove_const, "Sidekiq") }
526
-
527
- it { is_expected.to be_truthy }
329
+ if DependencyHelper.active_job_present?
330
+ require "active_job"
331
+ require "action_mailer"
332
+ require "sidekiq/testing"
333
+
334
+ describe "Sidekiq ActiveJob integration" do
335
+ let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
336
+ let(:time) { Time.parse("2001-01-01 10:00:00UTC") }
337
+ let(:log) { StringIO.new }
338
+ let(:given_args) do
339
+ [
340
+ "foo",
341
+ {
342
+ :foo => "Foo",
343
+ "bar" => "Bar",
344
+ "baz" => { "1" => "foo" }
345
+ }
346
+ ]
528
347
  end
529
-
530
- context "when Sidekiq constant is not found" do
531
- before { Object.send(:remove_const, "Sidekiq") if defined?(Sidekiq) }
532
-
533
- it { is_expected.to be_falsy }
348
+ let(:expected_args) do
349
+ [
350
+ "foo",
351
+ {
352
+ "_aj_symbol_keys" => ["foo"],
353
+ "foo" => "Foo",
354
+ "bar" => "Bar",
355
+ "baz" => {
356
+ "_aj_symbol_keys" => [],
357
+ "1" => "foo"
358
+ }
359
+ }
360
+ ]
534
361
  end
535
- end
536
-
537
- describe "#install" do
538
- before do
539
- Appsignal.config = project_fixture_config
540
- class Sidekiq
541
- def self.middlewares
542
- @middlewares ||= Set.new
543
- end
544
-
545
- def self.configure_server
546
- yield self
547
- end
548
-
549
- def self.server_middleware
550
- yield middlewares
362
+ let(:expected_tags) do
363
+ {}.tap do |hash|
364
+ if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
365
+ hash["provider_job_id"] = kind_of(String)
551
366
  end
552
367
  end
553
368
  end
554
- after { Object.send(:remove_const, "Sidekiq") }
555
-
556
- it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain" do
557
- described_class.new.install
558
-
559
- expect(Sidekiq.middlewares).to include(Appsignal::Hooks::SidekiqPlugin)
560
- end
561
- end
562
- end
563
-
564
- describe Appsignal::Hooks::SidekiqProbe do
565
- describe "#call" do
566
- let(:probe) { described_class.new }
567
- let(:redis_hostname) { "localhost" }
568
- let(:expected_default_tags) { { :hostname => "localhost" } }
569
369
  before do
570
- Appsignal.config = project_fixture_config
571
- class Sidekiq
572
- def self.redis_info
573
- {
574
- "connected_clients" => 2,
575
- "used_memory" => 1024,
576
- "used_memory_rss" => 512
577
- }
578
- end
579
-
580
- def self.redis
581
- yield Client.new
582
- end
583
-
584
- class Client
585
- def connection
586
- { :host => "localhost" }
587
- end
588
- end
589
-
590
- class Stats
591
- class << self
592
- attr_reader :calls
593
-
594
- def count_call
595
- @calls ||= -1
596
- @calls += 1
597
- end
598
- end
599
-
600
- def workers_size
601
- # First method called, so count it towards a call
602
- self.class.count_call
603
- 24
604
- end
605
-
606
- def processes_size
607
- 25
608
- end
609
-
610
- # Return two different values for two separate calls.
611
- # This allows us to test the delta of the value send as a gauge.
612
- def processed
613
- [10, 15][self.class.calls]
614
- end
615
-
616
- # Return two different values for two separate calls.
617
- # This allows us to test the delta of the value send as a gauge.
618
- def failed
619
- [10, 13][self.class.calls]
620
- end
370
+ start_agent
371
+ Appsignal.logger = test_logger(log)
372
+ ActiveJob::Base.queue_adapter = :sidekiq
621
373
 
622
- def retry_size
623
- 12
624
- end
374
+ class ActiveJobSidekiqTestJob < ActiveJob::Base
375
+ self.queue_adapter = :sidekiq
625
376
 
626
- # Return two different values for two separate calls.
627
- # This allows us to test the delta of the value send as a gauge.
628
- def dead_size
629
- [10, 12][self.class.calls]
630
- end
631
-
632
- def scheduled_size
633
- 14
634
- end
635
-
636
- def enqueued
637
- 15
638
- end
377
+ def perform(*_args)
639
378
  end
379
+ end
640
380
 
641
- class Queue
642
- Queue = Struct.new(:name, :size, :latency)
381
+ class ActiveJobSidekiqErrorTestJob < ActiveJob::Base
382
+ self.queue_adapter = :sidekiq
643
383
 
644
- def self.all
645
- [
646
- Queue.new("default", 10, 12),
647
- Queue.new("critical", 1, 2)
648
- ]
649
- end
384
+ def perform(*_args)
385
+ raise "uh oh"
650
386
  end
651
387
  end
652
- end
653
- after { Object.send(:remove_const, "Sidekiq") }
654
-
655
- describe ".dependencies_present?" do
656
- before do
657
- class Redis; end
658
- Redis.const_set(:VERSION, version)
388
+ # Manually add the AppSignal middleware for the Testing environment.
389
+ # It doesn't use configured middlewares by default looks like.
390
+ # We test somewhere else if the middleware is installed properly.
391
+ Sidekiq::Testing.server_middleware do |chain|
392
+ chain.add Appsignal::Hooks::SidekiqPlugin
659
393
  end
660
- after { Object.send(:remove_const, "Redis") }
661
-
662
- context "when Redis version is < 3.3.5" do
663
- let(:version) { "3.3.4" }
664
-
665
- it "does not start probe" do
666
- expect(described_class.dependencies_present?).to be_falsy
394
+ end
395
+ around do |example|
396
+ keep_transactions do
397
+ Sidekiq::Testing.fake! do
398
+ example.run
667
399
  end
668
400
  end
401
+ end
402
+ after do
403
+ Object.send(:remove_const, :ActiveJobSidekiqTestJob)
404
+ Object.send(:remove_const, :ActiveJobSidekiqErrorTestJob)
405
+ end
669
406
 
670
- context "when Redis version is >= 3.3.5" do
671
- let(:version) { "3.3.5" }
407
+ it "reports the transaction from the ActiveJob integration" do
408
+ perform_job(ActiveJobSidekiqTestJob, given_args)
672
409
 
673
- it "does not start probe" do
674
- expect(described_class.dependencies_present?).to be_truthy
675
- end
676
- end
410
+ transaction = last_transaction
411
+ transaction_hash = transaction.to_h
412
+ expect(transaction_hash).to include(
413
+ "action" => "ActiveJobSidekiqTestJob#perform",
414
+ "error" => nil,
415
+ "namespace" => namespace,
416
+ "metadata" => hash_including(
417
+ "queue" => "default"
418
+ ),
419
+ "sample_data" => hash_including(
420
+ "environment" => {},
421
+ "params" => [expected_args],
422
+ "tags" => expected_tags.merge("queue" => "default")
423
+ )
424
+ )
425
+ expect(transaction.request.env).to eq(:queue_start => time.to_f)
426
+ events = transaction_hash["events"]
427
+ .sort_by { |e| e["start"] }
428
+ .map { |event| event["name"] }
429
+ expect(events)
430
+ .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
677
431
  end
678
432
 
679
- it "loads Sidekiq::API" do
680
- expect(defined?(Sidekiq::API)).to be_falsy
681
- probe
682
- expect(defined?(Sidekiq::API)).to be_truthy
683
- end
433
+ context "with error" do
434
+ it "reports the error on the transaction from the ActiveRecord integration" do
435
+ expect do
436
+ perform_job(ActiveJobSidekiqErrorTestJob, given_args)
437
+ end.to raise_error(RuntimeError, "uh oh")
684
438
 
685
- it "logs config on initialize" do
686
- log = capture_logs { probe }
687
- expect(log).to contains_log(:debug, "Initializing Sidekiq probe\n")
439
+ transaction = last_transaction
440
+ transaction_hash = transaction.to_h
441
+ expect(transaction_hash).to include(
442
+ "action" => "ActiveJobSidekiqErrorTestJob#perform",
443
+ "error" => {
444
+ "name" => "RuntimeError",
445
+ "message" => "uh oh",
446
+ "backtrace" => kind_of(String)
447
+ },
448
+ "namespace" => namespace,
449
+ "metadata" => hash_including(
450
+ "queue" => "default"
451
+ ),
452
+ "sample_data" => hash_including(
453
+ "environment" => {},
454
+ "params" => [expected_args],
455
+ "tags" => expected_tags.merge("queue" => "default")
456
+ )
457
+ )
458
+ expect(transaction.request.env).to eq(:queue_start => time.to_f)
459
+ events = transaction_hash["events"]
460
+ .sort_by { |e| e["start"] }
461
+ .map { |event| event["name"] }
462
+ expect(events)
463
+ .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
464
+ end
688
465
  end
689
466
 
690
- it "logs used hostname on call once" do
691
- log = capture_logs { probe.call }
692
- expect(log).to contains_log(
693
- :debug,
694
- %(Sidekiq probe: Using Redis server hostname "localhost" as hostname)
695
- )
696
- log = capture_logs { probe.call }
697
- # Match more logs with incompelete message
698
- expect(log).to_not contains_log(:debug, %(Sidekiq probe: ))
699
- end
700
-
701
- it "collects custom metrics" do
702
- expect_gauge("worker_count", 24).twice
703
- expect_gauge("process_count", 25).twice
704
- expect_gauge("connection_count", 2).twice
705
- expect_gauge("memory_usage", 1024).twice
706
- expect_gauge("memory_usage_rss", 512).twice
707
- expect_gauge("job_count", 5, :status => :processed) # Gauge delta
708
- expect_gauge("job_count", 3, :status => :failed) # Gauge delta
709
- expect_gauge("job_count", 12, :status => :retry_queue).twice
710
- expect_gauge("job_count", 2, :status => :died) # Gauge delta
711
- expect_gauge("job_count", 14, :status => :scheduled).twice
712
- expect_gauge("job_count", 15, :status => :enqueued).twice
713
- expect_gauge("queue_length", 10, :queue => "default").twice
714
- expect_gauge("queue_latency", 12_000, :queue => "default").twice
715
- expect_gauge("queue_length", 1, :queue => "critical").twice
716
- expect_gauge("queue_latency", 2_000, :queue => "critical").twice
717
- # Call probe twice so we can calculate the delta for some gauge values
718
- probe.call
719
- probe.call
720
- end
721
-
722
- context "when `redis_info` is not defined" do
467
+ context "with ActionMailer" do
468
+ include ActionMailerHelpers
469
+
723
470
  before do
724
- allow(Sidekiq).to receive(:respond_to?).with(:redis_info).and_return(false)
471
+ class ActionMailerSidekiqTestJob < ActionMailer::Base
472
+ def welcome(*args)
473
+ end
474
+ end
725
475
  end
726
476
 
727
- it "does not collect redis metrics" do
728
- expect_gauge("connection_count", 2).never
729
- expect_gauge("memory_usage", 1024).never
730
- expect_gauge("memory_usage_rss", 512).never
731
- probe.call
477
+ it "reports ActionMailer data on the transaction" do
478
+ perform_mailer(ActionMailerSidekiqTestJob, :welcome, given_args)
479
+
480
+ transaction = last_transaction
481
+ transaction_hash = transaction.to_h
482
+ expect(transaction_hash).to include(
483
+ "action" => "ActionMailerSidekiqTestJob#welcome",
484
+ "sample_data" => hash_including(
485
+ "params" => ["ActionMailerSidekiqTestJob", "welcome", "deliver_now"] + expected_args
486
+ )
487
+ )
732
488
  end
733
489
  end
734
490
 
735
- context "when hostname is configured for probe" do
736
- let(:redis_hostname) { "my_redis_server" }
737
- let(:probe) { described_class.new(:hostname => redis_hostname) }
738
-
739
- it "uses the redis hostname for the hostname tag" do
740
- allow(Appsignal).to receive(:set_gauge).and_call_original
741
- log = capture_logs { probe }
742
- expect(log).to contains_log(
743
- :debug,
744
- %(Initializing Sidekiq probe with config: {:hostname=>"#{redis_hostname}"})
745
- )
746
- log = capture_logs { probe.call }
747
- expect(log).to contains_log(
748
- :debug,
749
- "Sidekiq probe: Using hostname config option #{redis_hostname.inspect} as hostname"
750
- )
751
- expect(Appsignal).to have_received(:set_gauge)
752
- .with(anything, anything, :hostname => redis_hostname).at_least(:once)
491
+ def perform_sidekiq
492
+ Timecop.freeze(time) do
493
+ yield
494
+ # Combined with Sidekiq::Testing.fake! and drain_all we get a
495
+ # enqueue_at in the job data.
496
+ Sidekiq::Worker.drain_all
753
497
  end
754
498
  end
755
499
 
756
- def expect_gauge(key, value, tags = {})
757
- expect(Appsignal).to receive(:set_gauge)
758
- .with("sidekiq_#{key}", value, expected_default_tags.merge(tags))
759
- .and_call_original
500
+ def perform_job(job_class, args)
501
+ perform_sidekiq { job_class.perform_later(args) }
502
+ end
503
+
504
+ def perform_mailer(mailer, method, args = nil)
505
+ perform_sidekiq { perform_action_mailer(mailer, method, args) }
760
506
  end
761
507
  end
762
508
  end