appsignal 3.0.0.beta.1 → 3.0.0.rc.1

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.
@@ -5,56 +5,34 @@ if defined?(Appsignal)
5
5
  end
6
6
 
7
7
  class Object
8
- if Appsignal::System.ruby_2_7_or_newer?
9
- def self.appsignal_instrument_class_method(method_name, options = {})
10
- singleton_class.send \
11
- :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
12
- singleton_class.send(:define_method, method_name) do |*args, **kwargs, &block|
13
- name = options.fetch(:name) do
14
- "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
15
- end
16
- Appsignal.instrument name do
17
- send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
18
- end
8
+ def self.appsignal_instrument_class_method(method_name, options = {})
9
+ singleton_class.send \
10
+ :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
11
+ singleton_class.send(:define_method, method_name) do |*args, &block|
12
+ name = options.fetch(:name) do
13
+ "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
19
14
  end
20
- end
21
-
22
- def self.appsignal_instrument_method(method_name, options = {})
23
- alias_method "appsignal_uninstrumented_#{method_name}", method_name
24
- define_method method_name do |*args, **kwargs, &block|
25
- name = options.fetch(:name) do
26
- "#{method_name}.#{appsignal_reverse_class_name}.other"
27
- end
28
- Appsignal.instrument name do
29
- send "appsignal_uninstrumented_#{method_name}", *args, **kwargs, &block
30
- end
15
+ Appsignal.instrument name do
16
+ send "appsignal_uninstrumented_#{method_name}", *args, &block
31
17
  end
32
18
  end
33
- else
34
- def self.appsignal_instrument_class_method(method_name, options = {})
35
- singleton_class.send \
36
- :alias_method, "appsignal_uninstrumented_#{method_name}", method_name
37
- singleton_class.send(:define_method, method_name) do |*args, &block|
38
- name = options.fetch(:name) do
39
- "#{method_name}.class_method.#{appsignal_reverse_class_name}.other"
40
- end
41
- Appsignal.instrument name do
42
- send "appsignal_uninstrumented_#{method_name}", *args, &block
43
- end
44
- end
19
+
20
+ if singleton_class.respond_to?(:ruby2_keywords, true) # rubocop:disable Style/GuardClause
21
+ singleton_class.send(:ruby2_keywords, method_name)
45
22
  end
23
+ end
46
24
 
47
- def self.appsignal_instrument_method(method_name, options = {})
48
- alias_method "appsignal_uninstrumented_#{method_name}", method_name
49
- define_method method_name do |*args, &block|
50
- name = options.fetch(:name) do
51
- "#{method_name}.#{appsignal_reverse_class_name}.other"
52
- end
53
- Appsignal.instrument name do
54
- send "appsignal_uninstrumented_#{method_name}", *args, &block
55
- end
25
+ def self.appsignal_instrument_method(method_name, options = {})
26
+ alias_method "appsignal_uninstrumented_#{method_name}", method_name
27
+ define_method method_name do |*args, &block|
28
+ name = options.fetch(:name) do
29
+ "#{method_name}.#{appsignal_reverse_class_name}.other"
30
+ end
31
+ Appsignal.instrument name do
32
+ send "appsignal_uninstrumented_#{method_name}", *args, &block
56
33
  end
57
34
  end
35
+ ruby2_keywords method_name if respond_to?(:ruby2_keywords, true)
58
36
  end
59
37
 
60
38
  def self.appsignal_reverse_class_name
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Appsignal
6
+ module Integrations
7
+ # Error handler for Sidekiq to report errors from jobs and internal Sidekiq
8
+ # errors.
9
+ #
10
+ # @api private
11
+ class SidekiqErrorHandler
12
+ def call(exception, sidekiq_context)
13
+ transaction = Appsignal::Transaction.current
14
+
15
+ if transaction.nil_transaction?
16
+ # Sidekiq error outside of the middleware scope.
17
+ # Can be a job JSON parse error or some other error happening in
18
+ # Sidekiq.
19
+ transaction = Appsignal::Transaction.create(
20
+ SecureRandom.uuid, # Newly generated job id
21
+ Appsignal::Transaction::BACKGROUND_JOB,
22
+ Appsignal::Transaction::GenericRequest.new({})
23
+ )
24
+ transaction.set_action_if_nil("SidekiqInternal")
25
+ transaction.set_metadata("sidekiq_error", sidekiq_context[:context])
26
+ transaction.params = { :jobstr => sidekiq_context[:jobstr] }
27
+ end
28
+
29
+ transaction.set_error(exception)
30
+ Appsignal::Transaction.complete_current!
31
+ end
32
+ end
33
+
34
+ # @api private
35
+ class SidekiqMiddleware # rubocop:disable Metrics/ClassLength
36
+ include Appsignal::Hooks::Helpers
37
+
38
+ EXCLUDED_JOB_KEYS = %w[
39
+ args backtrace class created_at enqueued_at error_backtrace error_class
40
+ error_message failed_at jid retried_at retry wrapped
41
+ ].freeze
42
+
43
+ def call(_worker, item, _queue)
44
+ job_status = nil
45
+ transaction = Appsignal::Transaction.create(
46
+ item["jid"],
47
+ Appsignal::Transaction::BACKGROUND_JOB,
48
+ Appsignal::Transaction::GenericRequest.new(
49
+ :queue_start => item["enqueued_at"]
50
+ )
51
+ )
52
+
53
+ Appsignal.instrument "perform_job.sidekiq" do
54
+ yield
55
+ end
56
+ rescue Exception => exception # rubocop:disable Lint/RescueException
57
+ job_status = :failed
58
+ raise exception
59
+ ensure
60
+ if transaction
61
+ transaction.set_action_if_nil(formatted_action_name(item))
62
+
63
+ params = filtered_arguments(item)
64
+ transaction.params = params if params
65
+
66
+ formatted_metadata(item).each do |key, value|
67
+ transaction.set_metadata key, value
68
+ end
69
+ transaction.set_http_or_background_queue_start
70
+ Appsignal::Transaction.complete_current! unless exception
71
+
72
+ queue = item["queue"] || "unknown"
73
+ if job_status
74
+ increment_counter "queue_job_count", 1,
75
+ :queue => queue,
76
+ :status => job_status
77
+ end
78
+ increment_counter "queue_job_count", 1,
79
+ :queue => queue,
80
+ :status => :processed
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def increment_counter(key, value, tags = {})
87
+ Appsignal.increment_counter "sidekiq_#{key}", value, tags
88
+ end
89
+
90
+ def formatted_action_name(job)
91
+ sidekiq_action_name = parse_action_name(job)
92
+ return unless sidekiq_action_name
93
+
94
+ complete_action = sidekiq_action_name =~ /\.|#/
95
+ return sidekiq_action_name if complete_action
96
+
97
+ "#{sidekiq_action_name}#perform"
98
+ end
99
+
100
+ def filtered_arguments(job)
101
+ arguments = parse_arguments(job)
102
+ return unless arguments
103
+
104
+ Appsignal::Utils::HashSanitizer.sanitize(
105
+ arguments,
106
+ Appsignal.config[:filter_parameters]
107
+ )
108
+ end
109
+
110
+ def formatted_metadata(item)
111
+ {}.tap do |hash|
112
+ (item || {}).each do |key, value|
113
+ next if EXCLUDED_JOB_KEYS.include?(key)
114
+
115
+ hash[key] = truncate(string_or_inspect(value))
116
+ end
117
+ end
118
+ end
119
+
120
+ # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L316-L334
121
+ def parse_action_name(job)
122
+ args = job.fetch("args", [])
123
+ job_class = job["class"]
124
+ case job_class
125
+ when "Sidekiq::Extensions::DelayedModel"
126
+ safe_load(args[0], job_class) do |target, method, _|
127
+ "#{target.class}##{method}"
128
+ end
129
+ when /\ASidekiq::Extensions::Delayed/
130
+ safe_load(args[0], job_class) do |target, method, _|
131
+ "#{target}.#{method}"
132
+ end
133
+ else
134
+ job_class
135
+ end
136
+ end
137
+
138
+ # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L336-L358
139
+ def parse_arguments(job)
140
+ args = job.fetch("args", [])
141
+ case job["class"]
142
+ when /\ASidekiq::Extensions::Delayed/
143
+ safe_load(args[0], args) do |_, _, arg|
144
+ arg
145
+ end
146
+ when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
147
+ nil # Set in the ActiveJob integration
148
+ else
149
+ # Sidekiq Enterprise argument encryption.
150
+ # More information: https://github.com/mperham/sidekiq/wiki/Ent-Encryption
151
+ if job["encrypt".freeze]
152
+ # No point in showing 150+ bytes of random garbage
153
+ args[-1] = "[encrypted data]".freeze
154
+ end
155
+ args
156
+ end
157
+ end
158
+
159
+ # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L403-L412
160
+ def safe_load(content, default)
161
+ yield(*YAML.load(content))
162
+ rescue => error
163
+ # Sidekiq issue #1761: in dev mode, it's possible to have jobs enqueued
164
+ # which haven't been loaded into memory yet so the YAML can't be
165
+ # loaded.
166
+ Appsignal.logger.warn "Unable to load YAML: #{error.message}"
167
+ default
168
+ end
169
+ end
170
+ end
171
+ end
@@ -123,6 +123,12 @@ module Appsignal
123
123
  def start
124
124
  stop
125
125
  @thread = Thread.new do
126
+ # Advise multi-threaded app servers to ignore this thread
127
+ # for the purposes of fork safety warnings
128
+ if Thread.current.respond_to?(:thread_variable_set)
129
+ Thread.current.thread_variable_set(:fork_safe, true)
130
+ end
131
+
126
132
  sleep initial_wait_time
127
133
  initialize_probes
128
134
  loop do
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "3.0.0.beta.1".freeze
4
+ VERSION = "3.0.0.rc.1".freeze
5
5
  end
@@ -2,6 +2,8 @@ describe Appsignal::Hooks::ActionCableHook do
2
2
  if DependencyHelper.action_cable_present?
3
3
  context "with ActionCable" do
4
4
  require "action_cable/engine"
5
+ # Require test helper to test with ConnectionStub
6
+ require "action_cable/channel/test_case" if DependencyHelper.rails6_present?
5
7
 
6
8
  describe ".dependencies_present?" do
7
9
  subject { described_class.new.dependencies_present? }
@@ -262,6 +264,49 @@ describe Appsignal::Hooks::ActionCableHook do
262
264
  )
263
265
  end
264
266
  end
267
+
268
+ if DependencyHelper.rails6_present?
269
+ context "with ConnectionStub" do
270
+ let(:connection) { ActionCable::Channel::ConnectionStub.new }
271
+ let(:transaction_id) { "Stubbed transaction id" }
272
+ before do
273
+ # Stub future (private AppSignal) transaction id generated by the hook.
274
+ expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
275
+ end
276
+
277
+ it "does not fail on missing `#env` method on `ConnectionStub`" do
278
+ instance.subscribe_to_channel
279
+
280
+ expect(subject).to include(
281
+ "action" => "MyChannel#subscribed",
282
+ "error" => nil,
283
+ "id" => transaction_id,
284
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
285
+ "metadata" => {
286
+ "method" => "websocket",
287
+ "path" => "" # No path as the ConnectionStub doesn't have the real request env
288
+ }
289
+ )
290
+ expect(subject["events"].first).to include(
291
+ "allocation_count" => kind_of(Integer),
292
+ "body" => "",
293
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
294
+ "child_allocation_count" => kind_of(Integer),
295
+ "child_duration" => kind_of(Float),
296
+ "child_gc_duration" => kind_of(Float),
297
+ "count" => 1,
298
+ "gc_duration" => kind_of(Float),
299
+ "start" => kind_of(Float),
300
+ "duration" => kind_of(Float),
301
+ "name" => "subscribed.action_cable",
302
+ "title" => ""
303
+ )
304
+ expect(subject["sample_data"]).to include(
305
+ "params" => { "internal" => "true" }
306
+ )
307
+ end
308
+ end
309
+ end
265
310
  end
266
311
 
267
312
  describe "unsubscribe callback" do
@@ -349,6 +394,49 @@ describe Appsignal::Hooks::ActionCableHook do
349
394
  )
350
395
  end
351
396
  end
397
+
398
+ if DependencyHelper.rails6_present?
399
+ context "with ConnectionStub" do
400
+ let(:connection) { ActionCable::Channel::ConnectionStub.new }
401
+ let(:transaction_id) { "Stubbed transaction id" }
402
+ before do
403
+ # Stub future (private AppSignal) transaction id generated by the hook.
404
+ expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
405
+ end
406
+
407
+ it "does not fail on missing `#env` method on `ConnectionStub`" do
408
+ instance.unsubscribe_from_channel
409
+
410
+ expect(subject).to include(
411
+ "action" => "MyChannel#unsubscribed",
412
+ "error" => nil,
413
+ "id" => transaction_id,
414
+ "namespace" => Appsignal::Transaction::ACTION_CABLE,
415
+ "metadata" => {
416
+ "method" => "websocket",
417
+ "path" => "" # No path as the ConnectionStub doesn't have the real request env
418
+ }
419
+ )
420
+ expect(subject["events"].first).to include(
421
+ "allocation_count" => kind_of(Integer),
422
+ "body" => "",
423
+ "body_format" => Appsignal::EventFormatter::DEFAULT,
424
+ "child_allocation_count" => kind_of(Integer),
425
+ "child_duration" => kind_of(Float),
426
+ "child_gc_duration" => kind_of(Float),
427
+ "count" => 1,
428
+ "gc_duration" => kind_of(Float),
429
+ "start" => kind_of(Float),
430
+ "duration" => kind_of(Float),
431
+ "name" => "unsubscribed.action_cable",
432
+ "title" => ""
433
+ )
434
+ expect(subject["sample_data"]).to include(
435
+ "params" => { "internal" => "true" }
436
+ )
437
+ end
438
+ end
439
+ end
352
440
  end
353
441
  end
354
442
  end
@@ -16,14 +16,31 @@ describe Appsignal::Hooks::SidekiqHook do
16
16
  end
17
17
 
18
18
  describe "#install" do
19
- class SidekiqMiddlewareMock < Set
20
- def exists?(middleware)
21
- include?(middleware)
19
+ class SidekiqMiddlewareMockWithPrepend < Array
20
+ alias add <<
21
+ alias exists? include?
22
+
23
+ unless method_defined? :prepend
24
+ def prepend(middleware) # For Ruby < 2.5
25
+ insert(0, middleware)
26
+ end
22
27
  end
23
28
  end
29
+
30
+ class SidekiqMiddlewareMockWithoutPrepend < Array
31
+ alias add <<
32
+ alias exists? include?
33
+
34
+ undef_method :prepend if method_defined? :prepend # For Ruby >= 2.5
35
+ end
36
+
24
37
  module SidekiqMock
38
+ def self.middleware_mock=(mock)
39
+ @middlewares = mock.new
40
+ end
41
+
25
42
  def self.middlewares
26
- @middlewares ||= SidekiqMiddlewareMock.new
43
+ @middlewares
27
44
  end
28
45
 
29
46
  def self.configure_server
@@ -34,479 +51,64 @@ describe Appsignal::Hooks::SidekiqHook do
34
51
  yield middlewares if block_given?
35
52
  middlewares
36
53
  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
-
52
- describe Appsignal::Hooks::SidekiqPlugin, :with_yaml_parse_error => false do
53
- class DelayedTestClass; end
54
-
55
- let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
56
- let(:worker) { anything }
57
- let(:queue) { anything }
58
- let(:given_args) do
59
- [
60
- "foo",
61
- {
62
- :foo => "Foo",
63
- :bar => "Bar",
64
- "baz" => { 1 => :foo }
65
- }
66
- ]
67
- end
68
- let(:expected_args) do
69
- [
70
- "foo",
71
- {
72
- "foo" => "Foo",
73
- "bar" => "Bar",
74
- "baz" => { "1" => "foo" }
75
- }
76
- ]
77
- end
78
- let(:job_class) { "TestClass" }
79
- let(:jid) { "b4a577edbccf1d805744efa9" }
80
- let(:item) do
81
- {
82
- "jid" => jid,
83
- "class" => job_class,
84
- "retry_count" => 0,
85
- "queue" => "default",
86
- "created_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
87
- "enqueued_at" => Time.parse("2001-01-01 10:00:00UTC").to_f,
88
- "args" => given_args,
89
- "extra" => "data"
90
- }
91
- end
92
- let(:plugin) { Appsignal::Hooks::SidekiqPlugin.new }
93
- let(:log) { StringIO.new }
94
- before do
95
- start_agent
96
- Appsignal.logger = test_logger(log)
97
- end
98
- around { |example| keep_transactions { example.run } }
99
- after :with_yaml_parse_error => false do
100
- expect(log_contents(log)).to_not contains_log(:warn, "Unable to load YAML")
101
- end
102
-
103
- describe "internal Sidekiq job values" do
104
- it "does not save internal Sidekiq values as metadata on transaction" do
105
- perform_job
106
-
107
- transaction_hash = transaction.to_h
108
- expect(transaction_hash["metadata"].keys)
109
- .to_not include(*Appsignal::Hooks::SidekiqPlugin::EXCLUDED_JOB_KEYS)
110
- end
111
- end
112
-
113
- context "with parameter filtering" do
114
- before do
115
- Appsignal.config = project_fixture_config("production")
116
- Appsignal.config[:filter_parameters] = ["foo"]
117
- end
118
-
119
- it "filters selected arguments" do
120
- perform_job
121
54
 
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
- )
133
- end
134
- end
135
-
136
- context "with encrypted arguments" do
137
- before do
138
- item["encrypt"] = true
139
- item["args"] << "super secret value" # Last argument will be replaced
140
- end
141
-
142
- it "replaces the last argument (the secret bag) with an [encrypted data] string" do
143
- perform_job
144
-
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
151
-
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
167
-
168
- it "uses the delayed class and method name for the action" do
169
- perform_job
170
-
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
- )
176
- end
177
-
178
- context "when job arguments is a malformed YAML object", :with_yaml_parse_error => true do
179
- before { item["args"] = [] }
180
-
181
- it "logs a warning and uses the default argument" do
182
- perform_job
183
-
184
- transaction_hash = transaction.to_h
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")
55
+ def self.error_handlers
56
+ @error_handlers ||= []
188
57
  end
189
58
  end
190
- end
191
-
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
- }
206
- end
207
-
208
- it "uses the delayed class and method name for the action" do
209
- perform_job
210
-
211
- transaction_hash = transaction.to_h
212
- expect(transaction_hash["action"]).to eq("DelayedTestClass#foo_method")
213
- expect(transaction_hash["sample_data"]).to include(
214
- "params" => ["bar" => "baz"]
215
- )
216
- end
217
59
 
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")
60
+ def add_middleware(middleware)
61
+ Sidekiq.configure_server do |sidekiq_config|
62
+ sidekiq_config.middlewares.add(middleware)
228
63
  end
229
64
  end
230
- end
231
-
232
- context "with an error" do
233
- let(:error) { ExampleException }
234
65
 
235
- it "creates a transaction and adds the error" do
236
- expect(Appsignal).to receive(:increment_counter)
237
- .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :failed)
238
- expect(Appsignal).to receive(:increment_counter)
239
- .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :processed)
240
-
241
- expect do
242
- perform_job { raise error, "uh oh" }
243
- end.to raise_error(error)
244
-
245
- transaction_hash = transaction.to_h
246
- expect(transaction_hash).to include(
247
- "id" => jid,
248
- "action" => "TestClass#perform",
249
- "error" => {
250
- "name" => "ExampleException",
251
- "message" => "uh oh",
252
- # TODO: backtrace should be an Array of Strings
253
- # https://github.com/appsignal/appsignal-agent/issues/294
254
- "backtrace" => kind_of(String)
255
- },
256
- "metadata" => {
257
- "extra" => "data",
258
- "queue" => "default",
259
- "retry_count" => "0"
260
- },
261
- "namespace" => namespace,
262
- "sample_data" => {
263
- "environment" => {},
264
- "params" => expected_args,
265
- "tags" => {},
266
- "breadcrumbs" => []
267
- }
268
- )
269
- expect_transaction_to_have_sidekiq_event(transaction_hash)
270
- end
271
- end
272
-
273
- context "without an error" do
274
- it "creates a transaction with events" do
275
- expect(Appsignal).to receive(:increment_counter)
276
- .with("sidekiq_queue_job_count", 1, :queue => "default", :status => :processed)
277
-
278
- perform_job
279
-
280
- transaction_hash = transaction.to_h
281
- expect(transaction_hash).to include(
282
- "id" => jid,
283
- "action" => "TestClass#perform",
284
- "error" => nil,
285
- "metadata" => {
286
- "extra" => "data",
287
- "queue" => "default",
288
- "retry_count" => "0"
289
- },
290
- "namespace" => namespace,
291
- "sample_data" => {
292
- "environment" => {},
293
- "params" => expected_args,
294
- "tags" => {},
295
- "breadcrumbs" => []
296
- }
297
- )
298
- # TODO: Not available in transaction.to_h yet.
299
- # https://github.com/appsignal/appsignal-agent/issues/293
300
- expect(transaction.request.env).to eq(
301
- :queue_start => Time.parse("2001-01-01 10:00:00UTC").to_f
302
- )
303
- expect_transaction_to_have_sidekiq_event(transaction_hash)
304
- end
305
- end
306
-
307
- def perform_job
308
- Timecop.freeze(Time.parse("2001-01-01 10:01:00UTC")) do
309
- plugin.call(worker, item, queue) do
310
- yield if block_given?
311
- end
312
- end
313
- end
314
-
315
- def transaction
316
- last_transaction
317
- end
318
-
319
- def expect_transaction_to_have_sidekiq_event(transaction_hash)
320
- events = transaction_hash["events"]
321
- expect(events.count).to eq(1)
322
- expect(events.first).to include(
323
- "name" => "perform_job.sidekiq",
324
- "title" => "",
325
- "count" => 1,
326
- "body" => "",
327
- "body_format" => Appsignal::EventFormatter::DEFAULT
328
- )
329
- end
330
- end
331
-
332
- if DependencyHelper.active_job_present?
333
- require "active_job"
334
- require "action_mailer"
335
- require "sidekiq/testing"
336
-
337
- describe "Sidekiq ActiveJob integration" do
338
- let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
339
- let(:time) { Time.parse("2001-01-01 10:00:00UTC") }
340
- let(:log) { StringIO.new }
341
- let(:given_args) do
342
- [
343
- "foo",
344
- {
345
- :foo => "Foo",
346
- "bar" => "Bar",
347
- "baz" => { "1" => "foo" }
348
- }
349
- ]
350
- end
351
- let(:expected_args) do
352
- [
353
- "foo",
354
- {
355
- "_aj_symbol_keys" => ["foo"],
356
- "foo" => "Foo",
357
- "bar" => "Bar",
358
- "baz" => {
359
- "_aj_symbol_keys" => [],
360
- "1" => "foo"
361
- }
362
- }
363
- ]
364
- end
365
- let(:expected_tags) do
366
- {}.tap do |hash|
367
- hash["active_job_id"] = kind_of(String)
368
- if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
369
- hash["provider_job_id"] = kind_of(String)
370
- end
371
- end
372
- end
373
66
  before do
374
- start_agent
375
- Appsignal.logger = test_logger(log)
376
- ActiveJob::Base.queue_adapter = :sidekiq
377
-
378
- class ActiveJobSidekiqTestJob < ActiveJob::Base
379
- self.queue_adapter = :sidekiq
380
-
381
- def perform(*_args)
382
- end
383
- end
384
-
385
- class ActiveJobSidekiqErrorTestJob < ActiveJob::Base
386
- self.queue_adapter = :sidekiq
387
-
388
- def perform(*_args)
389
- raise "uh oh"
390
- end
391
- end
392
- # Manually add the AppSignal middleware for the Testing environment.
393
- # It doesn't use configured middlewares by default looks like.
394
- # We test somewhere else if the middleware is installed properly.
395
- Sidekiq::Testing.server_middleware do |chain|
396
- chain.add Appsignal::Hooks::SidekiqPlugin
397
- end
398
- end
399
- around do |example|
400
- keep_transactions do
401
- Sidekiq::Testing.fake! do
402
- example.run
403
- end
404
- end
405
- end
406
- after do
407
- Object.send(:remove_const, :ActiveJobSidekiqTestJob)
408
- Object.send(:remove_const, :ActiveJobSidekiqErrorTestJob)
409
- end
410
-
411
- it "reports the transaction from the ActiveJob integration" do
412
- perform_job(ActiveJobSidekiqTestJob, given_args)
413
-
414
- transaction = last_transaction
415
- transaction_hash = transaction.to_h
416
- expect(transaction_hash).to include(
417
- "action" => "ActiveJobSidekiqTestJob#perform",
418
- "error" => nil,
419
- "namespace" => namespace,
420
- "metadata" => hash_including(
421
- "queue" => "default"
422
- ),
423
- "sample_data" => hash_including(
424
- "environment" => {},
425
- "params" => [expected_args],
426
- "tags" => expected_tags.merge("queue" => "default")
427
- )
428
- )
429
- expect(transaction.request.env).to eq(:queue_start => time.to_f)
430
- events = transaction_hash["events"]
431
- .sort_by { |e| e["start"] }
432
- .map { |event| event["name"] }
433
- expect(events)
434
- .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
67
+ Appsignal.config = project_fixture_config
68
+ stub_const "Sidekiq", SidekiqMock
435
69
  end
436
70
 
437
- context "with error" do
438
- it "reports the error on the transaction from the ActiveRecord integration" do
439
- expect do
440
- perform_job(ActiveJobSidekiqErrorTestJob, given_args)
441
- end.to raise_error(RuntimeError, "uh oh")
442
-
443
- transaction = last_transaction
444
- transaction_hash = transaction.to_h
445
- expect(transaction_hash).to include(
446
- "action" => "ActiveJobSidekiqErrorTestJob#perform",
447
- "error" => {
448
- "name" => "RuntimeError",
449
- "message" => "uh oh",
450
- "backtrace" => kind_of(String)
451
- },
452
- "namespace" => namespace,
453
- "metadata" => hash_including(
454
- "queue" => "default"
455
- ),
456
- "sample_data" => hash_including(
457
- "environment" => {},
458
- "params" => [expected_args],
459
- "tags" => expected_tags.merge("queue" => "default")
460
- )
461
- )
462
- expect(transaction.request.env).to eq(:queue_start => time.to_f)
463
- events = transaction_hash["events"]
464
- .sort_by { |e| e["start"] }
465
- .map { |event| event["name"] }
466
- expect(events)
467
- .to eq(["perform_job.sidekiq", "perform_start.active_job", "perform.active_job"])
468
- end
71
+ it "adds error handler" do
72
+ Sidekiq.middleware_mock = SidekiqMiddlewareMockWithPrepend
73
+ described_class.new.install
74
+ expect(Sidekiq.error_handlers).to include(Appsignal::Integrations::SidekiqErrorHandler)
469
75
  end
470
76
 
471
- context "with ActionMailer" do
472
- include ActionMailerHelpers
77
+ context "when Sidekiq middleware responds to prepend method" do # Sidekiq 3.3.0 and newer
78
+ before { Sidekiq.middleware_mock = SidekiqMiddlewareMockWithPrepend }
473
79
 
474
- before do
475
- class ActionMailerSidekiqTestJob < ActionMailer::Base
476
- def welcome(*args)
477
- end
478
- end
479
- end
80
+ it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain in the first position" do
81
+ user_middleware1 = proc {}
82
+ add_middleware(user_middleware1)
83
+ described_class.new.install
84
+ user_middleware2 = proc {}
85
+ add_middleware(user_middleware2)
480
86
 
481
- it "reports ActionMailer data on the transaction" do
482
- perform_mailer(ActionMailerSidekiqTestJob, :welcome, given_args)
483
-
484
- transaction = last_transaction
485
- transaction_hash = transaction.to_h
486
- expect(transaction_hash).to include(
487
- "action" => "ActionMailerSidekiqTestJob#welcome",
488
- "sample_data" => hash_including(
489
- "params" => ["ActionMailerSidekiqTestJob", "welcome", "deliver_now"] + expected_args
490
- )
491
- )
87
+ expect(Sidekiq.server_middleware).to eql([
88
+ Appsignal::Integrations::SidekiqMiddleware, # Prepend makes it the first entry
89
+ user_middleware1,
90
+ user_middleware2
91
+ ])
492
92
  end
493
93
  end
494
94
 
495
- def perform_sidekiq
496
- Timecop.freeze(time) do
497
- yield
498
- # Combined with Sidekiq::Testing.fake! and drain_all we get a
499
- # enqueue_at in the job data.
500
- Sidekiq::Worker.drain_all
501
- end
502
- end
95
+ context "when Sidekiq middleware does not respond to prepend method" do
96
+ before { Sidekiq.middleware_mock = SidekiqMiddlewareMockWithoutPrepend }
503
97
 
504
- def perform_job(job_class, args)
505
- perform_sidekiq { job_class.perform_later(args) }
506
- end
98
+ it "adds the AppSignal SidekiqPlugin to the Sidekiq middleware chain" do
99
+ user_middleware1 = proc {}
100
+ add_middleware(user_middleware1)
101
+ described_class.new.install
102
+ user_middleware2 = proc {}
103
+ add_middleware(user_middleware2)
507
104
 
508
- def perform_mailer(mailer, method, args = nil)
509
- perform_sidekiq { perform_action_mailer(mailer, method, args) }
105
+ # Add middlewares in whatever order they were added
106
+ expect(Sidekiq.server_middleware).to eql([
107
+ user_middleware1,
108
+ Appsignal::Integrations::SidekiqMiddleware,
109
+ user_middleware2
110
+ ])
111
+ end
510
112
  end
511
113
  end
512
114
  end