appsignal 2.10.6 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +138 -62
  4. data/CHANGELOG.md +56 -0
  5. data/README.md +4 -4
  6. data/Rakefile +16 -4
  7. data/appsignal.gemspec +1 -1
  8. data/build_matrix.yml +20 -9
  9. data/ext/Rakefile +2 -0
  10. data/ext/agent.yml +19 -19
  11. data/ext/appsignal_extension.c +10 -1
  12. data/ext/base.rb +22 -4
  13. data/ext/extconf.rb +2 -0
  14. data/gemfiles/padrino.gemfile +2 -2
  15. data/gemfiles/rails-4.2.gemfile +9 -2
  16. data/gemfiles/rails-5.0.gemfile +1 -0
  17. data/gemfiles/rails-5.1.gemfile +1 -0
  18. data/gemfiles/rails-5.2.gemfile +1 -0
  19. data/gemfiles/rails-6.0.gemfile +1 -0
  20. data/gemfiles/resque-1.gemfile +7 -0
  21. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  22. data/lib/appsignal.rb +22 -1
  23. data/lib/appsignal/auth_check.rb +4 -2
  24. data/lib/appsignal/capistrano.rb +2 -0
  25. data/lib/appsignal/cli/diagnose.rb +1 -1
  26. data/lib/appsignal/config.rb +85 -16
  27. data/lib/appsignal/environment.rb +126 -0
  28. data/lib/appsignal/extension.rb +6 -5
  29. data/lib/appsignal/extension/jruby.rb +16 -5
  30. data/lib/appsignal/hooks.rb +25 -0
  31. data/lib/appsignal/hooks/active_job.rb +137 -0
  32. data/lib/appsignal/hooks/net_http.rb +10 -13
  33. data/lib/appsignal/hooks/puma.rb +1 -58
  34. data/lib/appsignal/hooks/redis.rb +2 -0
  35. data/lib/appsignal/hooks/resque.rb +60 -0
  36. data/lib/appsignal/hooks/sequel.rb +2 -0
  37. data/lib/appsignal/hooks/sidekiq.rb +18 -192
  38. data/lib/appsignal/integrations/delayed_job_plugin.rb +16 -3
  39. data/lib/appsignal/integrations/object.rb +4 -0
  40. data/lib/appsignal/integrations/que.rb +1 -1
  41. data/lib/appsignal/integrations/resque.rb +9 -12
  42. data/lib/appsignal/integrations/resque_active_job.rb +9 -24
  43. data/lib/appsignal/probes.rb +7 -0
  44. data/lib/appsignal/probes/puma.rb +61 -0
  45. data/lib/appsignal/probes/sidekiq.rb +104 -0
  46. data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
  47. data/lib/appsignal/system.rb +0 -6
  48. data/lib/appsignal/transaction.rb +32 -7
  49. data/lib/appsignal/utils/deprecation_message.rb +6 -2
  50. data/lib/appsignal/version.rb +1 -1
  51. data/lib/puma/plugin/appsignal.rb +2 -1
  52. data/spec/lib/appsignal/auth_check_spec.rb +23 -0
  53. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  54. data/spec/lib/appsignal/capistrano3_spec.rb +1 -1
  55. data/spec/lib/appsignal/cli/diagnose_spec.rb +44 -1
  56. data/spec/lib/appsignal/config_spec.rb +44 -1
  57. data/spec/lib/appsignal/environment_spec.rb +167 -0
  58. data/spec/lib/appsignal/extension/jruby_spec.rb +31 -28
  59. data/spec/lib/appsignal/extension_install_failure_spec.rb +23 -0
  60. data/spec/lib/appsignal/hooks/activejob_spec.rb +591 -0
  61. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +187 -166
  62. data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
  63. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  64. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +297 -549
  65. data/spec/lib/appsignal/hooks_spec.rb +57 -0
  66. data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
  67. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  68. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -137
  69. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  70. data/spec/lib/appsignal/marker_spec.rb +1 -1
  71. data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
  72. data/spec/lib/appsignal/probes/sidekiq_spec.rb +204 -0
  73. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
  74. data/spec/lib/appsignal/system_spec.rb +0 -36
  75. data/spec/lib/appsignal/transaction_spec.rb +35 -20
  76. data/spec/lib/appsignal_spec.rb +22 -0
  77. data/spec/lib/puma/appsignal_spec.rb +1 -1
  78. data/spec/spec_helper.rb +5 -0
  79. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  80. data/spec/support/helpers/config_helpers.rb +3 -2
  81. data/spec/support/helpers/dependency_helper.rb +12 -0
  82. data/spec/support/helpers/env_helpers.rb +1 -1
  83. data/spec/support/helpers/environment_metdata_helper.rb +16 -0
  84. data/spec/support/helpers/transaction_helpers.rb +6 -0
  85. data/spec/support/stubs/sidekiq/api.rb +2 -2
  86. data/spec/support/testing.rb +19 -19
  87. metadata +31 -9
  88. data/lib/appsignal/integrations/net_http.rb +0 -16
@@ -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
 
@@ -25,9 +76,10 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
25
76
  ]
26
77
  end
27
78
  let(:job_class) { "TestClass" }
79
+ let(:jid) { "b4a577edbccf1d805744efa9" }
28
80
  let(:item) do
29
81
  {
30
- "jid" => "b4a577edbccf1d805744efa9",
82
+ "jid" => jid,
31
83
  "class" => job_class,
32
84
  "retry_count" => 0,
33
85
  "queue" => "default",
@@ -48,369 +100,132 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
48
100
  expect(log_contents(log)).to_not contains_log(:warn, "Unable to load YAML")
49
101
  end
50
102
 
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
103
+ describe "internal Sidekiq job values" do
104
+ it "does not save internal Sidekiq values as metadata on transaction" do
105
+ perform_job
55
106
 
56
- transaction_hash = transaction.to_h
57
- expect(transaction_hash["metadata"].keys)
58
- .to_not include(*Appsignal::Hooks::SidekiqPlugin::JOB_KEYS)
59
- end
107
+ transaction_hash = transaction.to_h
108
+ expect(transaction_hash["metadata"].keys)
109
+ .to_not include(*Appsignal::Hooks::SidekiqPlugin::EXCLUDED_JOB_KEYS)
60
110
  end
111
+ end
61
112
 
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
113
+ context "with parameter filtering" do
114
+ before do
115
+ Appsignal.config = project_fixture_config("production")
116
+ Appsignal.config[:filter_parameters] = ["foo"]
83
117
  end
84
118
 
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
119
+ it "filters selected arguments" do
120
+ perform_job
93
121
 
94
- transaction_hash = transaction.to_h
95
- expect(transaction_hash["sample_data"]).to include(
96
- "params" => expected_args << "[encrypted data]"
97
- )
98
- end
122
+ transaction_hash = transaction.to_h
123
+ expect(transaction_hash["sample_data"]).to include(
124
+ "params" => [
125
+ "foo",
126
+ {
127
+ "foo" => "[FILTERED]",
128
+ "bar" => "Bar",
129
+ "baz" => { "1" => "foo" }
130
+ }
131
+ ]
132
+ )
99
133
  end
134
+ end
100
135
 
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
136
+ context "with encrypted arguments" do
137
+ before do
138
+ item["encrypt"] = true
139
+ item["args"] << "super secret value" # Last argument will be replaced
139
140
  end
140
141
 
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
142
+ it "replaces the last argument (the secret bag) with an [encrypted data] string" do
143
+ perform_job
159
144
 
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
145
+ transaction_hash = transaction.to_h
146
+ expect(transaction_hash["sample_data"]).to include(
147
+ "params" => expected_args << "[encrypted data]"
148
+ )
149
+ end
150
+ end
166
151
 
167
- context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
168
- before { item["args"] = [] }
152
+ context "when using the Sidekiq delayed extension" do
153
+ let(:item) do
154
+ {
155
+ "jid" => jid,
156
+ "class" => "Sidekiq::Extensions::DelayedClass",
157
+ "queue" => "default",
158
+ "args" => [
159
+ "---\n- !ruby/class 'DelayedTestClass'\n- :foo_method\n- - :bar: baz\n"
160
+ ],
161
+ "retry" => true,
162
+ "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
163
+ "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
164
+ "extra" => "data"
165
+ }
166
+ end
169
167
 
170
- it "logs a warning and uses the default argument" do
171
- perform_job
168
+ it "uses the delayed class and method name for the action" do
169
+ perform_job
172
170
 
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
171
+ transaction_hash = transaction.to_h
172
+ expect(transaction_hash["action"]).to eq("DelayedTestClass.foo_method")
173
+ expect(transaction_hash["sample_data"]).to include(
174
+ "params" => ["bar" => "baz"]
175
+ )
179
176
  end
180
177
 
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
178
+ context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
179
+ before { item["args"] = [] }
201
180
 
202
- it "creates a transaction with events" do
181
+ it "logs a warning and uses the default argument" do
203
182
  perform_job
204
183
 
205
184
  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
185
+ expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedClass#perform")
186
+ expect(transaction_hash["sample_data"]).to include("params" => [])
187
+ expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
389
188
  end
390
189
  end
391
190
  end
392
191
 
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")
192
+ context "when using the Sidekiq ActiveRecord instance delayed extension" do
193
+ let(:item) do
194
+ {
195
+ "jid" => jid,
196
+ "class" => "Sidekiq::Extensions::DelayedModel",
197
+ "queue" => "default",
198
+ "args" => [
199
+ "---\n- !ruby/object:DelayedTestClass {}\n- :foo_method\n- - :bar: :baz\n"
200
+ ],
201
+ "retry" => true,
202
+ "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
203
+ "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
204
+ "extra" => "data"
205
+ }
397
206
  end
398
207
 
399
- it "stores no sample data" do
208
+ it "uses the delayed class and method name for the action" do
209
+ perform_job
210
+
400
211
  transaction_hash = transaction.to_h
401
- expect(transaction_hash).to include(
402
- "sample_data" => {
403
- "environment" => {},
404
- "params" => [],
405
- "tags" => {}
406
- }
212
+ expect(transaction_hash["action"]).to eq("DelayedTestClass#foo_method")
213
+ expect(transaction_hash["sample_data"]).to include(
214
+ "params" => ["bar" => "baz"]
407
215
  )
408
216
  end
409
217
 
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
- )
218
+ context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
219
+ before { item["args"] = [] }
220
+
221
+ it "logs a warning and uses the default argument" do
222
+ perform_job
223
+
224
+ transaction_hash = transaction.to_h
225
+ expect(transaction_hash["action"]).to eq("Sidekiq::Extensions::DelayedModel#perform")
226
+ expect(transaction_hash["sample_data"]).to include("params" => [])
227
+ expect(log_contents(log)).to contains_log(:warn, "Unable to load YAML")
228
+ end
414
229
  end
415
230
  end
416
231
 
@@ -429,7 +244,7 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
429
244
 
430
245
  transaction_hash = transaction.to_h
431
246
  expect(transaction_hash).to include(
432
- "id" => kind_of(String),
247
+ "id" => jid,
433
248
  "action" => "TestClass#perform",
434
249
  "error" => {
435
250
  "name" => "ExampleException",
@@ -452,8 +267,6 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
452
267
  )
453
268
  expect_transaction_to_have_sidekiq_event(transaction_hash)
454
269
  end
455
-
456
- include_examples "sidekiq metadata"
457
270
  end
458
271
 
459
272
  context "without an error" do
@@ -465,7 +278,7 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
465
278
 
466
279
  transaction_hash = transaction.to_h
467
280
  expect(transaction_hash).to include(
468
- "id" => kind_of(String),
281
+ "id" => jid,
469
282
  "action" => "TestClass#perform",
470
283
  "error" => nil,
471
284
  "metadata" => {
@@ -487,8 +300,6 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
487
300
  )
488
301
  expect_transaction_to_have_sidekiq_event(transaction_hash)
489
302
  end
490
-
491
- include_examples "sidekiq metadata"
492
303
  end
493
304
 
494
305
  def perform_job
@@ -516,247 +327,184 @@ describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
516
327
  end
517
328
  end
518
329
 
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 }
330
+ if DependencyHelper.active_job_present?
331
+ require "active_job"
332
+ require "action_mailer"
333
+ require "sidekiq/testing"
334
+
335
+ describe "Sidekiq ActiveJob integration" do
336
+ let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
337
+ let(:time) { Time.parse("2001-01-01 10:00:00UTC") }
338
+ let(:log) { StringIO.new }
339
+ let(:given_args) do
340
+ [
341
+ "foo",
342
+ {
343
+ :foo => "Foo",
344
+ "bar" => "Bar",
345
+ "baz" => { "1" => "foo" }
346
+ }
347
+ ]
528
348
  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 }
349
+ let(:expected_args) do
350
+ [
351
+ "foo",
352
+ {
353
+ "_aj_symbol_keys" => ["foo"],
354
+ "foo" => "Foo",
355
+ "bar" => "Bar",
356
+ "baz" => {
357
+ "_aj_symbol_keys" => [],
358
+ "1" => "foo"
359
+ }
360
+ }
361
+ ]
534
362
  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
363
+ let(:expected_tags) do
364
+ {}.tap do |hash|
365
+ hash["active_job_id"] = kind_of(String)
366
+ if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
367
+ hash["provider_job_id"] = kind_of(String)
551
368
  end
552
369
  end
553
370
  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
371
  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
372
+ start_agent
373
+ Appsignal.logger = test_logger(log)
374
+ ActiveJob::Base.queue_adapter = :sidekiq
621
375
 
622
- def retry_size
623
- 12
624
- end
376
+ class ActiveJobSidekiqTestJob < ActiveJob::Base
377
+ self.queue_adapter = :sidekiq
625
378
 
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
379
+ def perform(*_args)
639
380
  end
381
+ end
640
382
 
641
- class Queue
642
- Queue = Struct.new(:name, :size, :latency)
383
+ class ActiveJobSidekiqErrorTestJob < ActiveJob::Base
384
+ self.queue_adapter = :sidekiq
643
385
 
644
- def self.all
645
- [
646
- Queue.new("default", 10, 12),
647
- Queue.new("critical", 1, 2)
648
- ]
649
- end
386
+ def perform(*_args)
387
+ raise "uh oh"
650
388
  end
651
389
  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)
390
+ # Manually add the AppSignal middleware for the Testing environment.
391
+ # It doesn't use configured middlewares by default looks like.
392
+ # We test somewhere else if the middleware is installed properly.
393
+ Sidekiq::Testing.server_middleware do |chain|
394
+ chain.add Appsignal::Hooks::SidekiqPlugin
659
395
  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
396
+ end
397
+ around do |example|
398
+ keep_transactions do
399
+ Sidekiq::Testing.fake! do
400
+ example.run
667
401
  end
668
402
  end
403
+ end
404
+ after do
405
+ Object.send(:remove_const, :ActiveJobSidekiqTestJob)
406
+ Object.send(:remove_const, :ActiveJobSidekiqErrorTestJob)
407
+ end
669
408
 
670
- context "when Redis version is >= 3.3.5" do
671
- let(:version) { "3.3.5" }
409
+ it "reports the transaction from the ActiveJob integration" do
410
+ perform_job(ActiveJobSidekiqTestJob, given_args)
672
411
 
673
- it "does not start probe" do
674
- expect(described_class.dependencies_present?).to be_truthy
675
- end
676
- end
412
+ transaction = last_transaction
413
+ transaction_hash = transaction.to_h
414
+ expect(transaction_hash).to include(
415
+ "action" => "ActiveJobSidekiqTestJob#perform",
416
+ "error" => nil,
417
+ "namespace" => namespace,
418
+ "metadata" => hash_including(
419
+ "queue" => "default"
420
+ ),
421
+ "sample_data" => hash_including(
422
+ "environment" => {},
423
+ "params" => [expected_args],
424
+ "tags" => expected_tags.merge("queue" => "default")
425
+ )
426
+ )
427
+ expect(transaction.request.env).to eq(:queue_start => time.to_f)
428
+ events = transaction_hash["events"]
429
+ .sort_by { |e| e["start"] }
430
+ .map { |event| event["name"] }
431
+ expect(events)
432
+ .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
677
433
  end
678
434
 
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
435
+ context "with error" do
436
+ it "reports the error on the transaction from the ActiveRecord integration" do
437
+ expect do
438
+ perform_job(ActiveJobSidekiqErrorTestJob, given_args)
439
+ end.to raise_error(RuntimeError, "uh oh")
684
440
 
685
- it "logs config on initialize" do
686
- log = capture_logs { probe }
687
- expect(log).to contains_log(:debug, "Initializing Sidekiq probe\n")
441
+ transaction = last_transaction
442
+ transaction_hash = transaction.to_h
443
+ expect(transaction_hash).to include(
444
+ "action" => "ActiveJobSidekiqErrorTestJob#perform",
445
+ "error" => {
446
+ "name" => "RuntimeError",
447
+ "message" => "uh oh",
448
+ "backtrace" => kind_of(String)
449
+ },
450
+ "namespace" => namespace,
451
+ "metadata" => hash_including(
452
+ "queue" => "default"
453
+ ),
454
+ "sample_data" => hash_including(
455
+ "environment" => {},
456
+ "params" => [expected_args],
457
+ "tags" => expected_tags.merge("queue" => "default")
458
+ )
459
+ )
460
+ expect(transaction.request.env).to eq(:queue_start => time.to_f)
461
+ events = transaction_hash["events"]
462
+ .sort_by { |e| e["start"] }
463
+ .map { |event| event["name"] }
464
+ expect(events)
465
+ .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
466
+ end
688
467
  end
689
468
 
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
469
+ context "with ActionMailer" do
470
+ include ActionMailerHelpers
471
+
723
472
  before do
724
- allow(Sidekiq).to receive(:respond_to?).with(:redis_info).and_return(false)
473
+ class ActionMailerSidekiqTestJob < ActionMailer::Base
474
+ def welcome(*args)
475
+ end
476
+ end
725
477
  end
726
478
 
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
479
+ it "reports ActionMailer data on the transaction" do
480
+ perform_mailer(ActionMailerSidekiqTestJob, :welcome, given_args)
481
+
482
+ transaction = last_transaction
483
+ transaction_hash = transaction.to_h
484
+ expect(transaction_hash).to include(
485
+ "action" => "ActionMailerSidekiqTestJob#welcome",
486
+ "sample_data" => hash_including(
487
+ "params" => ["ActionMailerSidekiqTestJob", "welcome", "deliver_now"] + expected_args
488
+ )
489
+ )
732
490
  end
733
491
  end
734
492
 
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)
493
+ def perform_sidekiq
494
+ Timecop.freeze(time) do
495
+ yield
496
+ # Combined with Sidekiq::Testing.fake! and drain_all we get a
497
+ # enqueue_at in the job data.
498
+ Sidekiq::Worker.drain_all
753
499
  end
754
500
  end
755
501
 
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
502
+ def perform_job(job_class, args)
503
+ perform_sidekiq { job_class.perform_later(args) }
504
+ end
505
+
506
+ def perform_mailer(mailer, method, args = nil)
507
+ perform_sidekiq { perform_action_mailer(mailer, method, args) }
760
508
  end
761
509
  end
762
510
  end