appsignal 2.11.0.alpha.2 → 2.11.0.beta.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +94 -10
  4. data/CHANGELOG.md +31 -1
  5. data/README.md +4 -4
  6. data/Rakefile +16 -4
  7. data/appsignal.gemspec +1 -1
  8. data/build_matrix.yml +7 -3
  9. data/ext/Rakefile +2 -0
  10. data/ext/agent.yml +19 -19
  11. data/ext/base.rb +7 -0
  12. data/ext/extconf.rb +2 -0
  13. data/gemfiles/rails-4.2.gemfile +9 -2
  14. data/gemfiles/rails-5.0.gemfile +1 -0
  15. data/gemfiles/rails-5.1.gemfile +1 -0
  16. data/gemfiles/rails-5.2.gemfile +1 -0
  17. data/gemfiles/rails-6.0.gemfile +1 -0
  18. data/gemfiles/resque-1.gemfile +7 -0
  19. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  20. data/lib/appsignal.rb +1 -0
  21. data/lib/appsignal/auth_check.rb +4 -2
  22. data/lib/appsignal/cli/diagnose.rb +1 -1
  23. data/lib/appsignal/config.rb +35 -2
  24. data/lib/appsignal/extension.rb +6 -5
  25. data/lib/appsignal/extension/jruby.rb +6 -5
  26. data/lib/appsignal/hooks.rb +25 -0
  27. data/lib/appsignal/hooks/active_job.rb +137 -0
  28. data/lib/appsignal/hooks/puma.rb +0 -1
  29. data/lib/appsignal/hooks/resque.rb +60 -0
  30. data/lib/appsignal/hooks/sidekiq.rb +17 -94
  31. data/lib/appsignal/integrations/delayed_job_plugin.rb +1 -1
  32. data/lib/appsignal/integrations/que.rb +1 -1
  33. data/lib/appsignal/integrations/resque.rb +9 -12
  34. data/lib/appsignal/integrations/resque_active_job.rb +9 -32
  35. data/lib/appsignal/probes.rb +7 -0
  36. data/lib/appsignal/probes/puma.rb +1 -1
  37. data/lib/appsignal/probes/sidekiq.rb +3 -1
  38. data/lib/appsignal/transaction.rb +10 -0
  39. data/lib/appsignal/utils/deprecation_message.rb +6 -2
  40. data/lib/appsignal/version.rb +1 -1
  41. data/spec/lib/appsignal/auth_check_spec.rb +23 -0
  42. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  43. data/spec/lib/appsignal/capistrano3_spec.rb +1 -1
  44. data/spec/lib/appsignal/cli/diagnose_spec.rb +42 -0
  45. data/spec/lib/appsignal/config_spec.rb +21 -0
  46. data/spec/lib/appsignal/extension/jruby_spec.rb +31 -28
  47. data/spec/lib/appsignal/extension_install_failure_spec.rb +23 -0
  48. data/spec/lib/appsignal/hooks/activejob_spec.rb +591 -0
  49. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +3 -14
  50. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  51. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +222 -268
  52. data/spec/lib/appsignal/hooks_spec.rb +57 -0
  53. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  54. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -179
  55. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  56. data/spec/lib/appsignal/marker_spec.rb +1 -1
  57. data/spec/lib/appsignal/probes/sidekiq_spec.rb +10 -7
  58. data/spec/lib/appsignal/transaction_spec.rb +5 -7
  59. data/spec/spec_helper.rb +5 -0
  60. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  61. data/spec/support/helpers/config_helpers.rb +3 -2
  62. data/spec/support/helpers/dependency_helper.rb +9 -2
  63. data/spec/support/helpers/transaction_helpers.rb +6 -0
  64. data/spec/support/stubs/sidekiq/api.rb +1 -1
  65. data/spec/support/testing.rb +19 -19
  66. metadata +16 -4
@@ -69,10 +69,34 @@ module Appsignal
69
69
  text.size > 200 ? "#{text[0...197]}..." : text
70
70
  end
71
71
  end
72
+
73
+ # Alias Probes constants that have moved to their own module in version
74
+ # 2.11.0.
75
+ def self.const_missing(name)
76
+ case name
77
+ when :SidekiqProbe
78
+ callers = caller
79
+ Appsignal::Utils::DeprecationMessage.message \
80
+ "The constant Appsignal::Hooks::SidekiqProbe has been deprecated. " \
81
+ "Please update the constant name to Appsignal::Probes::SidekiqProbe " \
82
+ "in the following file to remove this message.\n#{callers.first}"
83
+ Appsignal::Probes::SidekiqProbe
84
+ when :PumaProbe
85
+ callers = caller
86
+ Appsignal::Utils::DeprecationMessage.message \
87
+ "The constant Appsignal::Hooks::PumaProbe has been deprecated. " \
88
+ "Please update the constant name to Appsignal::Probes::PumaProbe " \
89
+ "in the following file to remove this message.\n#{callers.first}"
90
+ Appsignal::Probes::PumaProbe
91
+ else
92
+ super
93
+ end
94
+ end
72
95
  end
73
96
  end
74
97
 
75
98
  require "appsignal/hooks/action_cable"
99
+ require "appsignal/hooks/active_job"
76
100
  require "appsignal/hooks/active_support_notifications"
77
101
  require "appsignal/hooks/celluloid"
78
102
  require "appsignal/hooks/delayed_job"
@@ -81,6 +105,7 @@ require "appsignal/hooks/passenger"
81
105
  require "appsignal/hooks/puma"
82
106
  require "appsignal/hooks/rake"
83
107
  require "appsignal/hooks/redis"
108
+ require "appsignal/hooks/resque"
84
109
  require "appsignal/hooks/sequel"
85
110
  require "appsignal/hooks/shoryuken"
86
111
  require "appsignal/hooks/sidekiq"
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class Hooks
5
+ # @api private
6
+ class ActiveJobHook < Appsignal::Hooks::Hook
7
+ register :active_job
8
+
9
+ def dependencies_present?
10
+ defined?(::ActiveJob)
11
+ end
12
+
13
+ def install
14
+ ::ActiveJob::Base
15
+ .extend ::Appsignal::Hooks::ActiveJobHook::ActiveJobClassInstrumentation
16
+ end
17
+
18
+ module ActiveJobClassInstrumentation
19
+ def execute(job)
20
+ job_status = nil
21
+ current_transaction = Appsignal::Transaction.current
22
+ transaction =
23
+ if current_transaction.nil_transaction?
24
+ # No standalone integration started before ActiveJob integration.
25
+ # We don't have a separate integration for this QueueAdapter like
26
+ # we do for Sidekiq.
27
+ #
28
+ # Prefer job_id from provider, instead of ActiveJob's internal ID.
29
+ Appsignal::Transaction.create(
30
+ job["provider_job_id"] || job["job_id"],
31
+ Appsignal::Transaction::BACKGROUND_JOB,
32
+ Appsignal::Transaction::GenericRequest.new({})
33
+ )
34
+ else
35
+ current_transaction
36
+ end
37
+
38
+ super
39
+ rescue Exception => exception # rubocop:disable Lint/RescueException
40
+ job_status = :failed
41
+ transaction.set_error(exception)
42
+ raise exception
43
+ ensure
44
+ if transaction
45
+ transaction.params =
46
+ Appsignal::Utils::HashSanitizer.sanitize(
47
+ job["arguments"],
48
+ Appsignal.config[:filter_parameters]
49
+ )
50
+
51
+ transaction_tags = ActiveJobHelpers.transaction_tags_for(job)
52
+ transaction_tags["active_job_id"] = job["job_id"]
53
+ provider_job_id = job["provider_job_id"]
54
+ if provider_job_id
55
+ transaction_tags[:provider_job_id] = provider_job_id
56
+ end
57
+ transaction.set_tags(transaction_tags)
58
+
59
+ transaction.set_action_if_nil(ActiveJobHelpers.action_name(job))
60
+ enqueued_at = job["enqueued_at"]
61
+ if enqueued_at # Present in Rails 6 and up
62
+ transaction.set_queue_start((Time.parse(enqueued_at).to_f * 1_000).to_i)
63
+ end
64
+
65
+ if current_transaction.nil_transaction?
66
+ # Only complete transaction if ActiveJob is not wrapped in
67
+ # another supported integration, such as Sidekiq.
68
+ Appsignal::Transaction.complete_current!
69
+ end
70
+ end
71
+
72
+ metrics = ActiveJobHelpers.metrics_for(job)
73
+ metrics.each do |(metric_name, tags)|
74
+ if job_status
75
+ ActiveJobHelpers.increment_counter metric_name, 1,
76
+ tags.merge(:status => job_status)
77
+ end
78
+ ActiveJobHelpers.increment_counter metric_name, 1,
79
+ tags.merge(:status => :processed)
80
+ end
81
+ end
82
+ end
83
+
84
+ module ActiveJobHelpers
85
+ ACTION_MAILER_CLASSES = [
86
+ "ActionMailer::DeliveryJob",
87
+ "ActionMailer::Parameterized::DeliveryJob",
88
+ "ActionMailer::MailDeliveryJob"
89
+ ].freeze
90
+
91
+ def self.action_name(job)
92
+ case job["job_class"]
93
+ when *ACTION_MAILER_CLASSES
94
+ job["arguments"][0..1].join("#")
95
+ else
96
+ "#{job["job_class"]}#perform"
97
+ end
98
+ end
99
+
100
+ # Returns an array of metrics with tags used to report the job metrics
101
+ #
102
+ # If job ONLY has a queue, it will return `queue_job_count` with tags.
103
+ # If job has a queue AND priority, it will ALSO return
104
+ # `queue_priority_job_count` with tags.
105
+ #
106
+ # @return [Array] Array of metrics with tags to report.
107
+ def self.metrics_for(job)
108
+ tags = { :queue => job["queue_name"] }
109
+ metrics = [["queue_job_count", tags]]
110
+
111
+ priority = job["priority"]
112
+ if priority
113
+ metrics << [
114
+ "queue_priority_job_count",
115
+ tags.merge(:priority => priority)
116
+ ]
117
+ end
118
+
119
+ metrics
120
+ end
121
+
122
+ def self.transaction_tags_for(job)
123
+ tags = {}
124
+ queue = job["queue_name"]
125
+ tags[:queue] = queue if queue
126
+ priority = job["priority"]
127
+ tags[:priority] = priority if priority
128
+ tags
129
+ end
130
+
131
+ def self.increment_counter(key, value, tags = {})
132
+ Appsignal.increment_counter "active_job_#{key}", value, tags
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -24,7 +24,6 @@ module Appsignal
24
24
  # runs in the Puma main process.
25
25
  # For more information:
26
26
  # https://docs.appsignal.com/ruby/integrations/puma.html
27
- require "appsignal/probes/puma"
28
27
  Appsignal::Minutely.probes.register :puma, ::Appsignal::Probes::PumaProbe
29
28
  end
30
29
 
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ class Hooks
5
+ # @api private
6
+ class ResqueHook < Appsignal::Hooks::Hook
7
+ register :resque
8
+
9
+ def dependencies_present?
10
+ defined?(::Resque)
11
+ end
12
+
13
+ def install
14
+ Resque::Job.class_eval do
15
+ alias_method :perform_without_appsignal, :perform
16
+
17
+ def perform
18
+ transaction = Appsignal::Transaction.create(
19
+ SecureRandom.uuid,
20
+ Appsignal::Transaction::BACKGROUND_JOB,
21
+ Appsignal::Transaction::GenericRequest.new({})
22
+ )
23
+
24
+ Appsignal.instrument "perform.resque" do
25
+ perform_without_appsignal
26
+ end
27
+ rescue Exception => exception # rubocop:disable Lint/RescueException
28
+ transaction.set_error(exception)
29
+ raise exception
30
+ ensure
31
+ if transaction
32
+ transaction.set_action_if_nil("#{payload["class"]}#perform")
33
+ args =
34
+ Appsignal::Utils::HashSanitizer.sanitize(
35
+ ResqueHelpers.arguments(payload),
36
+ Appsignal.config[:filter_parameters]
37
+ )
38
+ transaction.params = args if args
39
+ transaction.set_tags("queue" => queue)
40
+
41
+ Appsignal::Transaction.complete_current!
42
+ end
43
+ Appsignal.stop("resque")
44
+ end
45
+ end
46
+ end
47
+
48
+ class ResqueHelpers
49
+ def self.arguments(payload)
50
+ case payload["class"]
51
+ when "ActiveJob::QueueAdapters::ResqueAdapter::JobWrapper"
52
+ nil # Set in the ActiveJob integration
53
+ else
54
+ payload["args"]
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -12,7 +12,6 @@ module Appsignal
12
12
  end
13
13
 
14
14
  def install
15
- require "appsignal/probes/sidekiq"
16
15
  Appsignal::Minutely.probes.register :sidekiq, Appsignal::Probes::SidekiqProbe
17
16
 
18
17
  ::Sidekiq.configure_server do |config|
@@ -27,8 +26,7 @@ module Appsignal
27
26
  class SidekiqPlugin # rubocop:disable Metrics/ClassLength
28
27
  include Appsignal::Hooks::Helpers
29
28
 
30
- UNKNOWN_ACTION_NAME = "unknown".freeze
31
- JOB_KEYS = %w[
29
+ EXCLUDED_JOB_KEYS = %w[
32
30
  args backtrace class created_at enqueued_at error_backtrace error_class
33
31
  error_message failed_at jid retried_at retry wrapped
34
32
  ].freeze
@@ -36,7 +34,7 @@ module Appsignal
36
34
  def call(_worker, item, _queue)
37
35
  job_status = nil
38
36
  transaction = Appsignal::Transaction.create(
39
- SecureRandom.uuid,
37
+ item["jid"],
40
38
  Appsignal::Transaction::BACKGROUND_JOB,
41
39
  Appsignal::Transaction::GenericRequest.new(
42
40
  :queue_start => item["enqueued_at"]
@@ -55,7 +53,10 @@ module Appsignal
55
53
  ensure
56
54
  if transaction
57
55
  transaction.set_action_if_nil(formatted_action_name(item))
58
- transaction.params = filtered_arguments(item)
56
+
57
+ params = filtered_arguments(item)
58
+ transaction.params = params if params
59
+
59
60
  formatted_metadata(item).each do |key, value|
60
61
  transaction.set_metadata key, value
61
62
  end
@@ -81,16 +82,20 @@ module Appsignal
81
82
 
82
83
  def formatted_action_name(job)
83
84
  sidekiq_action_name = parse_action_name(job)
85
+ return unless sidekiq_action_name
86
+
84
87
  complete_action = sidekiq_action_name =~ /\.|#/
85
- if complete_action || sidekiq_action_name == UNKNOWN_ACTION_NAME
86
- return sidekiq_action_name
87
- end
88
+ return sidekiq_action_name if complete_action
89
+
88
90
  "#{sidekiq_action_name}#perform"
89
91
  end
90
92
 
91
93
  def filtered_arguments(job)
94
+ arguments = parse_arguments(job)
95
+ return unless arguments
96
+
92
97
  Appsignal::Utils::HashSanitizer.sanitize(
93
- parse_arguments(job),
98
+ arguments,
94
99
  Appsignal.config[:filter_parameters]
95
100
  )
96
101
  end
@@ -98,7 +103,8 @@ module Appsignal
98
103
  def formatted_metadata(item)
99
104
  {}.tap do |hash|
100
105
  (item || {}).each do |key, value|
101
- next if JOB_KEYS.include?(key)
106
+ next if EXCLUDED_JOB_KEYS.include?(key)
107
+
102
108
  hash[key] = truncate(string_or_inspect(value))
103
109
  end
104
110
  end
@@ -117,81 +123,11 @@ module Appsignal
117
123
  safe_load(args[0], job_class) do |target, method, _|
118
124
  "#{target}.#{method}"
119
125
  end
120
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
121
- wrapped_job = job["wrapped"]
122
- if wrapped_job
123
- parse_active_job_action_name_from_wrapped job
124
- else
125
- parse_active_job_action_name_from_arguments job
126
- end
127
126
  else
128
127
  job_class
129
128
  end
130
129
  end
131
130
 
132
- # Return the ActiveJob wrapped job name.
133
- #
134
- # Returns "unknown" if no acceptable job class name could be found.
135
- #
136
- # @example Payload with "wrapped" value
137
- # {
138
- # "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
139
- # "wrapped" => "MyWrappedJob",
140
- # # ...
141
- # }
142
- def parse_active_job_action_name_from_wrapped(job)
143
- job_class = job["wrapped"]
144
- case job_class
145
- when "ActionMailer::DeliveryJob"
146
- extract_action_mailer_name job["args"]
147
- when String
148
- job_class
149
- else
150
- unknown_action_name_for job
151
- end
152
- end
153
-
154
- # Return the ActiveJob job name based on the job's arguments.
155
- #
156
- # Returns "unknown" if no acceptable job class name could be found.
157
- #
158
- # @example Payload without "wrapped" value
159
- # {
160
- # "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
161
- # "args" => [{
162
- # "job_class" => "MyWrappedJob",
163
- # # ...
164
- # }]
165
- # # ...
166
- # }
167
- def parse_active_job_action_name_from_arguments(job)
168
- args = job.fetch("args", [])
169
- first_arg = args[0]
170
- if first_arg == "ActionMailer::DeliveryJob"
171
- extract_action_mailer_name args
172
- elsif active_job_payload?(first_arg)
173
- first_arg["job_class"]
174
- else
175
- unknown_action_name_for job
176
- end
177
- end
178
-
179
- # Checks if the first argument in the job payload is an ActiveJob payload.
180
- def active_job_payload?(arg)
181
- arg.is_a?(Hash) && arg["job_class"].is_a?(String)
182
- end
183
-
184
- def unknown_action_name_for(job)
185
- Appsignal.logger.debug \
186
- "Unable to determine an action name from Sidekiq payload: #{job}"
187
- UNKNOWN_ACTION_NAME
188
- end
189
-
190
- def extract_action_mailer_name(args)
191
- # Returns in format: MailerClass#mailer_method
192
- args[0]["arguments"][0..1].join("#")
193
- end
194
-
195
131
  # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L336-L358
196
132
  def parse_arguments(job)
197
133
  args = job.fetch("args", [])
@@ -201,20 +137,7 @@ module Appsignal
201
137
  arg
202
138
  end
203
139
  when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
204
- is_wrapped = job["wrapped"]
205
- first_arg = args[0]
206
- job_args =
207
- if is_wrapped || active_job_payload?(first_arg)
208
- first_arg["arguments"]
209
- else
210
- []
211
- end
212
- if (is_wrapped || first_arg) == "ActionMailer::DeliveryJob"
213
- # Remove MailerClass, mailer_method and "deliver_now"
214
- job_args.drop(3)
215
- else
216
- job_args
217
- end
140
+ nil # Set in the ActiveJob integration
218
141
  else
219
142
  # Sidekiq Enterprise argument encryption.
220
143
  # More information: https://github.com/mperham/sidekiq/wiki/Ent-Encryption
@@ -64,7 +64,7 @@ module Appsignal
64
64
  dot_split = default_name.split(".")
65
65
  return default_name if dot_split.length == 2
66
66
 
67
- ["unknown"]
67
+ "#{default_name}#perform"
68
68
  end
69
69
 
70
70
  def self.extract_value(object_or_hash, field, default_value = nil, convert_to_s = false)
@@ -32,7 +32,7 @@ module Appsignal
32
32
  transaction.set_error(error)
33
33
  raise error
34
34
  ensure
35
- transaction.set_action "#{local_attrs[:job_class]}#run"
35
+ transaction.set_action_if_nil "#{local_attrs[:job_class]}#run"
36
36
  Appsignal::Transaction.complete_current!
37
37
  end
38
38
  end
@@ -4,18 +4,15 @@ module Appsignal
4
4
  module Integrations
5
5
  # @api private
6
6
  module ResquePlugin
7
- # Do not use this file as a template for your own background processor
8
- # Resque is an exception to the rule and the code below causes the
9
- # extension to shut itself down after a single job.
10
- # see http://docs.appsignal.com/background-monitoring/custom.html
11
- def around_perform_resque_plugin(*_args)
12
- Appsignal.monitor_single_transaction(
13
- "perform_job.resque",
14
- :class => to_s,
15
- :method => "perform"
16
- ) do
17
- yield
18
- end
7
+ def self.extended(_)
8
+ callers = caller
9
+ Appsignal::Utils::DeprecationMessage.message \
10
+ "The AppSignal ResquePlugin is deprecated and does " \
11
+ "nothing on extend. In this version of the AppSignal Ruby gem " \
12
+ "the integration with Resque is automatic on all Resque workers. " \
13
+ "Please remove the following line from this file to remove this " \
14
+ "message: extend Appsignal::Integrations::ResquePlugin\n" \
15
+ "#{callers.first}"
19
16
  end
20
17
  end
21
18
  end