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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +1 -1
- data/.semaphore/semaphore.yml +88 -88
- data/CHANGELOG.md +20 -0
- data/appsignal.gemspec +1 -1
- data/build_matrix.yml +11 -11
- data/lib/appsignal.rb +1 -2
- data/lib/appsignal/helpers/instrumentation.rb +69 -5
- data/lib/appsignal/hooks.rb +16 -0
- data/lib/appsignal/hooks/action_cable.rb +10 -2
- data/lib/appsignal/hooks/sidekiq.rb +9 -142
- data/lib/appsignal/integrations/object.rb +21 -43
- data/lib/appsignal/integrations/sidekiq.rb +171 -0
- data/lib/appsignal/minutely.rb +6 -0
- data/lib/appsignal/version.rb +1 -1
- data/spec/lib/appsignal/hooks/action_cable_spec.rb +88 -0
- data/spec/lib/appsignal/hooks/sidekiq_spec.rb +60 -458
- data/spec/lib/appsignal/hooks_spec.rb +41 -0
- data/spec/lib/appsignal/integrations/object_spec.rb +91 -4
- data/spec/lib/appsignal/integrations/sidekiq_spec.rb +524 -0
- data/spec/lib/appsignal_spec.rb +162 -47
- data/spec/lib/puma/appsignal_spec.rb +28 -0
- metadata +7 -4
@@ -5,56 +5,34 @@ if defined?(Appsignal)
|
|
5
5
|
end
|
6
6
|
|
7
7
|
class Object
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
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
|
-
|
34
|
-
|
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
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
data/lib/appsignal/minutely.rb
CHANGED
@@ -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
|
data/lib/appsignal/version.rb
CHANGED
@@ -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
|
20
|
-
|
21
|
-
|
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
|
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
|
-
|
123
|
-
|
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
|
-
|
219
|
-
|
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
|
-
|
375
|
-
|
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
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
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 "
|
472
|
-
|
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
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
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
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
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
|
-
|
496
|
-
|
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
|
-
|
505
|
-
|
506
|
-
|
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
|
-
|
509
|
-
|
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
|