appsignal 2.10.9.beta.1-java → 2.11.0.beta.2-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +75 -61
  4. data/CHANGELOG.md +21 -2
  5. data/build_matrix.yml +13 -7
  6. data/ext/agent.yml +19 -19
  7. data/ext/appsignal_extension.c +10 -1
  8. data/gemfiles/padrino.gemfile +2 -2
  9. data/gemfiles/rails-4.2.gemfile +9 -2
  10. data/gemfiles/rails-5.0.gemfile +1 -0
  11. data/gemfiles/rails-5.1.gemfile +1 -0
  12. data/gemfiles/rails-5.2.gemfile +1 -0
  13. data/gemfiles/rails-6.0.gemfile +1 -0
  14. data/gemfiles/resque-1.gemfile +7 -0
  15. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  16. data/lib/appsignal.rb +21 -1
  17. data/lib/appsignal/capistrano.rb +2 -0
  18. data/lib/appsignal/config.rb +6 -2
  19. data/lib/appsignal/environment.rb +126 -0
  20. data/lib/appsignal/extension/jruby.rb +10 -0
  21. data/lib/appsignal/hooks.rb +2 -0
  22. data/lib/appsignal/hooks/active_job.rb +89 -0
  23. data/lib/appsignal/hooks/net_http.rb +2 -0
  24. data/lib/appsignal/hooks/puma.rb +2 -58
  25. data/lib/appsignal/hooks/redis.rb +2 -0
  26. data/lib/appsignal/hooks/resque.rb +60 -0
  27. data/lib/appsignal/hooks/sequel.rb +2 -0
  28. data/lib/appsignal/hooks/sidekiq.rb +18 -191
  29. data/lib/appsignal/integrations/object.rb +4 -0
  30. data/lib/appsignal/integrations/que.rb +1 -1
  31. data/lib/appsignal/integrations/resque.rb +9 -12
  32. data/lib/appsignal/integrations/resque_active_job.rb +9 -24
  33. data/lib/appsignal/probes/puma.rb +61 -0
  34. data/lib/appsignal/probes/sidekiq.rb +102 -0
  35. data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
  36. data/lib/appsignal/transaction.rb +32 -7
  37. data/lib/appsignal/utils/deprecation_message.rb +5 -1
  38. data/lib/appsignal/version.rb +1 -1
  39. data/lib/puma/plugin/appsignal.rb +2 -1
  40. data/spec/lib/appsignal/config_spec.rb +6 -1
  41. data/spec/lib/appsignal/environment_spec.rb +167 -0
  42. data/spec/lib/appsignal/hooks/activejob_spec.rb +458 -0
  43. data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
  44. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  45. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +292 -546
  46. data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
  47. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  48. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -137
  49. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  50. data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
  51. data/spec/lib/appsignal/probes/sidekiq_spec.rb +204 -0
  52. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
  53. data/spec/lib/appsignal/transaction_spec.rb +35 -20
  54. data/spec/lib/appsignal_spec.rb +22 -0
  55. data/spec/lib/puma/appsignal_spec.rb +1 -1
  56. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  57. data/spec/support/helpers/dependency_helper.rb +12 -0
  58. data/spec/support/helpers/env_helpers.rb +1 -1
  59. data/spec/support/helpers/environment_metdata_helper.rb +16 -0
  60. data/spec/support/helpers/transaction_helpers.rb +6 -0
  61. data/spec/support/stubs/sidekiq/api.rb +2 -2
  62. metadata +27 -7
@@ -0,0 +1,89 @@
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
+ # @todo Add queue time support for Rails 6's enqueued_at. For both
19
+ # existing and new transactions.
20
+ module ActiveJobClassInstrumentation
21
+ def execute(job)
22
+ current_transaction = Appsignal::Transaction.current
23
+ transaction =
24
+ if current_transaction.nil_transaction?
25
+ # No standalone integration started before ActiveJob integration.
26
+ # We don't have a separate integration for this QueueAdapter like
27
+ # we do for Sidekiq.
28
+ #
29
+ # Prefer job_id from provider, instead of ActiveJob's internal ID.
30
+ Appsignal::Transaction.create(
31
+ job["provider_job_id"] || job["job_id"],
32
+ Appsignal::Transaction::BACKGROUND_JOB,
33
+ Appsignal::Transaction::GenericRequest.new({})
34
+ )
35
+ else
36
+ current_transaction
37
+ end
38
+
39
+ super
40
+ rescue Exception => exception # rubocop:disable Lint/RescueException
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
+ tags = { :queue => job["queue_name"] }
52
+ provider_job_id = job["provider_job_id"]
53
+ tags[:provider_job_id] = provider_job_id if provider_job_id
54
+ transaction.set_tags(tags)
55
+
56
+ transaction.set_action_if_nil(ActiveJobHelpers.action_name(job))
57
+ enqueued_at = job["enqueued_at"]
58
+ if enqueued_at # Present in Rails 6 and up
59
+ transaction.set_queue_start((Time.parse(enqueued_at).to_f * 1_000).to_i)
60
+ end
61
+
62
+ if current_transaction.nil_transaction?
63
+ # Only complete transaction if ActiveJob is not wrapped in
64
+ # another supported integration, such as Sidekiq.
65
+ Appsignal::Transaction.complete_current!
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ module ActiveJobHelpers
72
+ ACTION_MAILER_CLASSES = [
73
+ "ActionMailer::DeliveryJob",
74
+ "ActionMailer::Parameterized::DeliveryJob",
75
+ "ActionMailer::MailDeliveryJob"
76
+ ].freeze
77
+
78
+ def self.action_name(job)
79
+ case job["job_class"]
80
+ when *ACTION_MAILER_CLASSES
81
+ job["arguments"][0..1].join("#")
82
+ else
83
+ "#{job["job_class"]}#perform"
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -25,6 +25,8 @@ module Appsignal
25
25
  end
26
26
  end
27
27
  end
28
+
29
+ Appsignal::Environment.report_enabled("net_http")
28
30
  end
29
31
  end
30
32
  end
@@ -24,7 +24,8 @@ 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
- Appsignal::Minutely.probes.register :puma, PumaProbe
27
+ require "appsignal/probes/puma"
28
+ Appsignal::Minutely.probes.register :puma, ::Appsignal::Probes::PumaProbe
28
29
  end
29
30
 
30
31
  return unless defined?(::Puma::Cluster)
@@ -39,62 +40,5 @@ module Appsignal
39
40
  end
40
41
  end
41
42
  end
42
-
43
- class PumaProbe
44
- def initialize
45
- @hostname = Appsignal.config[:hostname] || Socket.gethostname
46
- end
47
-
48
- def call
49
- puma_stats = fetch_puma_stats
50
- return unless puma_stats
51
-
52
- stats = JSON.parse puma_stats, :symbolize_names => true
53
- counts = {}
54
- count_keys = [:backlog, :running, :pool_capacity, :max_threads]
55
-
56
- if stats[:worker_status] # Multiple workers
57
- stats[:worker_status].each do |worker|
58
- stat = worker[:last_status]
59
- count_keys.each do |key|
60
- count_if_present counts, key, stat
61
- end
62
- end
63
-
64
- gauge(:workers, stats[:workers], :type => :count)
65
- gauge(:workers, stats[:booted_workers], :type => :booted)
66
- gauge(:workers, stats[:old_workers], :type => :old)
67
- else # Single worker
68
- count_keys.each do |key|
69
- count_if_present counts, key, stats
70
- end
71
- end
72
-
73
- gauge(:connection_backlog, counts[:backlog]) if counts[:backlog]
74
- gauge(:pool_capacity, counts[:pool_capacity]) if counts[:pool_capacity]
75
- gauge(:threads, counts[:running], :type => :running) if counts[:running]
76
- gauge(:threads, counts[:max_threads], :type => :max) if counts[:max_threads]
77
- end
78
-
79
- private
80
-
81
- attr_reader :hostname
82
-
83
- def gauge(field, count, tags = {})
84
- Appsignal.set_gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
85
- end
86
-
87
- def count_if_present(counts, key, stats)
88
- stat_value = stats[key]
89
- return unless stat_value
90
- counts[key] ||= 0
91
- counts[key] += stat_value
92
- end
93
-
94
- def fetch_puma_stats
95
- ::Puma.stats
96
- rescue NoMethodError # rubocop:disable Lint/HandleExceptions
97
- end
98
- end
99
43
  end
100
44
  end
@@ -26,6 +26,8 @@ module Appsignal
26
26
  end
27
27
  end
28
28
  end
29
+
30
+ Appsignal::Environment.report_enabled("redis")
29
31
  end
30
32
  end
31
33
  end
@@ -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
@@ -56,6 +56,8 @@ module Appsignal
56
56
 
57
57
  # ... and automatically add it to future instances.
58
58
  ::Sequel::Database.extension(:appsignal_integration)
59
+
60
+ Appsignal::Environment.report_enabled("sequel")
59
61
  end
60
62
  end
61
63
  end
@@ -12,7 +12,8 @@ module Appsignal
12
12
  end
13
13
 
14
14
  def install
15
- Appsignal::Minutely.probes.register :sidekiq, SidekiqProbe
15
+ require "appsignal/probes/sidekiq"
16
+ Appsignal::Minutely.probes.register :sidekiq, Appsignal::Probes::SidekiqProbe
16
17
 
17
18
  ::Sidekiq.configure_server do |config|
18
19
  config.server_middleware do |chain|
@@ -22,110 +23,11 @@ module Appsignal
22
23
  end
23
24
  end
24
25
 
25
- class SidekiqProbe
26
- attr_reader :config
27
-
28
- def self.dependencies_present?
29
- Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("3.3.5")
30
- end
31
-
32
- def initialize(config = {})
33
- @config = config
34
- @cache = {}
35
- config_string = " with config: #{config}" unless config.empty?
36
- Appsignal.logger.debug("Initializing Sidekiq probe#{config_string}")
37
- require "sidekiq/api"
38
- end
39
-
40
- def call
41
- track_redis_info
42
- track_stats
43
- track_queues
44
- end
45
-
46
- private
47
-
48
- attr_reader :cache
49
-
50
- def track_redis_info
51
- return unless ::Sidekiq.respond_to?(:redis_info)
52
- redis_info = ::Sidekiq.redis_info
53
-
54
- gauge "connection_count", redis_info.fetch("connected_clients")
55
- gauge "memory_usage", redis_info.fetch("used_memory")
56
- gauge "memory_usage_rss", redis_info.fetch("used_memory_rss")
57
- end
58
-
59
- def track_stats
60
- stats = ::Sidekiq::Stats.new
61
-
62
- gauge "worker_count", stats.workers_size
63
- gauge "process_count", stats.processes_size
64
- gauge_delta :jobs_processed, "job_count", stats.processed,
65
- :status => :processed
66
- gauge_delta :jobs_failed, "job_count", stats.failed, :status => :failed
67
- gauge "job_count", stats.retry_size, :status => :retry_queue
68
- gauge_delta :jobs_dead, "job_count", stats.dead_size, :status => :died
69
- gauge "job_count", stats.scheduled_size, :status => :scheduled
70
- gauge "job_count", stats.enqueued, :status => :enqueued
71
- end
72
-
73
- def track_queues
74
- ::Sidekiq::Queue.all.each do |queue|
75
- gauge "queue_length", queue.size, :queue => queue.name
76
- # Convert latency from seconds to milliseconds
77
- gauge "queue_latency", queue.latency * 1_000.0, :queue => queue.name
78
- end
79
- end
80
-
81
- # Track a gauge metric with the `sidekiq_` prefix
82
- def gauge(key, value, tags = {})
83
- tags[:hostname] = hostname if hostname
84
- Appsignal.set_gauge "sidekiq_#{key}", value, tags
85
- end
86
-
87
- # Track the delta of two values for a gauge metric
88
- #
89
- # First call will store the data for the metric and the second call will
90
- # set a gauge metric with the difference. This is used for absolute
91
- # counter values which we want to track as gauges.
92
- #
93
- # @example
94
- # gauge_delta :my_cache_key, "my_gauge", 10
95
- # gauge_delta :my_cache_key, "my_gauge", 15
96
- # # Creates a gauge with the value `5`
97
- # @see #gauge
98
- def gauge_delta(cache_key, key, value, tags = {})
99
- previous_value = cache[cache_key]
100
- cache[cache_key] = value
101
- return unless previous_value
102
- new_value = value - previous_value
103
- gauge key, new_value, tags
104
- end
105
-
106
- def hostname
107
- return @hostname if defined?(@hostname)
108
- if config.key?(:hostname)
109
- @hostname = config[:hostname]
110
- Appsignal.logger.debug "Sidekiq probe: Using hostname config " \
111
- "option #{@hostname.inspect} as hostname"
112
- return @hostname
113
- end
114
-
115
- host = nil
116
- ::Sidekiq.redis { |c| host = c.connection[:host] }
117
- Appsignal.logger.debug "Sidekiq probe: Using Redis server hostname " \
118
- "#{host.inspect} as hostname"
119
- @hostname = host
120
- end
121
- end
122
-
123
26
  # @api private
124
27
  class SidekiqPlugin # rubocop:disable Metrics/ClassLength
125
28
  include Appsignal::Hooks::Helpers
126
29
 
127
- UNKNOWN_ACTION_NAME = "unknown".freeze
128
- JOB_KEYS = %w[
30
+ EXCLUDED_JOB_KEYS = %w[
129
31
  args backtrace class created_at enqueued_at error_backtrace error_class
130
32
  error_message failed_at jid retried_at retry wrapped
131
33
  ].freeze
@@ -152,7 +54,10 @@ module Appsignal
152
54
  ensure
153
55
  if transaction
154
56
  transaction.set_action_if_nil(formatted_action_name(item))
155
- transaction.params = filtered_arguments(item)
57
+
58
+ params = filtered_arguments(item)
59
+ transaction.params = params if params
60
+
156
61
  formatted_metadata(item).each do |key, value|
157
62
  transaction.set_metadata key, value
158
63
  end
@@ -178,16 +83,20 @@ module Appsignal
178
83
 
179
84
  def formatted_action_name(job)
180
85
  sidekiq_action_name = parse_action_name(job)
86
+ return unless sidekiq_action_name
87
+
181
88
  complete_action = sidekiq_action_name =~ /\.|#/
182
- if complete_action || sidekiq_action_name == UNKNOWN_ACTION_NAME
183
- return sidekiq_action_name
184
- end
89
+ return sidekiq_action_name if complete_action
90
+
185
91
  "#{sidekiq_action_name}#perform"
186
92
  end
187
93
 
188
94
  def filtered_arguments(job)
95
+ arguments = parse_arguments(job)
96
+ return unless arguments
97
+
189
98
  Appsignal::Utils::HashSanitizer.sanitize(
190
- parse_arguments(job),
99
+ arguments,
191
100
  Appsignal.config[:filter_parameters]
192
101
  )
193
102
  end
@@ -195,7 +104,8 @@ module Appsignal
195
104
  def formatted_metadata(item)
196
105
  {}.tap do |hash|
197
106
  (item || {}).each do |key, value|
198
- next if JOB_KEYS.include?(key)
107
+ next if EXCLUDED_JOB_KEYS.include?(key)
108
+
199
109
  hash[key] = truncate(string_or_inspect(value))
200
110
  end
201
111
  end
@@ -214,81 +124,11 @@ module Appsignal
214
124
  safe_load(args[0], job_class) do |target, method, _|
215
125
  "#{target}.#{method}"
216
126
  end
217
- when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
218
- wrapped_job = job["wrapped"]
219
- if wrapped_job
220
- parse_active_job_action_name_from_wrapped job
221
- else
222
- parse_active_job_action_name_from_arguments job
223
- end
224
127
  else
225
128
  job_class
226
129
  end
227
130
  end
228
131
 
229
- # Return the ActiveJob wrapped job name.
230
- #
231
- # Returns "unknown" if no acceptable job class name could be found.
232
- #
233
- # @example Payload with "wrapped" value
234
- # {
235
- # "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
236
- # "wrapped" => "MyWrappedJob",
237
- # # ...
238
- # }
239
- def parse_active_job_action_name_from_wrapped(job)
240
- job_class = job["wrapped"]
241
- case job_class
242
- when "ActionMailer::DeliveryJob"
243
- extract_action_mailer_name job["args"]
244
- when String
245
- job_class
246
- else
247
- unknown_action_name_for job
248
- end
249
- end
250
-
251
- # Return the ActiveJob job name based on the job's arguments.
252
- #
253
- # Returns "unknown" if no acceptable job class name could be found.
254
- #
255
- # @example Payload without "wrapped" value
256
- # {
257
- # "class" => "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper",
258
- # "args" => [{
259
- # "job_class" => "MyWrappedJob",
260
- # # ...
261
- # }]
262
- # # ...
263
- # }
264
- def parse_active_job_action_name_from_arguments(job)
265
- args = job.fetch("args", [])
266
- first_arg = args[0]
267
- if first_arg == "ActionMailer::DeliveryJob"
268
- extract_action_mailer_name args
269
- elsif active_job_payload?(first_arg)
270
- first_arg["job_class"]
271
- else
272
- unknown_action_name_for job
273
- end
274
- end
275
-
276
- # Checks if the first argument in the job payload is an ActiveJob payload.
277
- def active_job_payload?(arg)
278
- arg.is_a?(Hash) && arg["job_class"].is_a?(String)
279
- end
280
-
281
- def unknown_action_name_for(job)
282
- Appsignal.logger.debug \
283
- "Unable to determine an action name from Sidekiq payload: #{job}"
284
- UNKNOWN_ACTION_NAME
285
- end
286
-
287
- def extract_action_mailer_name(args)
288
- # Returns in format: MailerClass#mailer_method
289
- args[0]["arguments"][0..1].join("#")
290
- end
291
-
292
132
  # Based on: https://github.com/mperham/sidekiq/blob/63ee43353bd3b753beb0233f64865e658abeb1c3/lib/sidekiq/api.rb#L336-L358
293
133
  def parse_arguments(job)
294
134
  args = job.fetch("args", [])
@@ -298,20 +138,7 @@ module Appsignal
298
138
  arg
299
139
  end
300
140
  when "ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper"
301
- is_wrapped = job["wrapped"]
302
- first_arg = args[0]
303
- job_args =
304
- if is_wrapped || active_job_payload?(first_arg)
305
- first_arg["arguments"]
306
- else
307
- []
308
- end
309
- if (is_wrapped || first_arg) == "ActionMailer::DeliveryJob"
310
- # Remove MailerClass, mailer_method and "deliver_now"
311
- job_args.drop(3)
312
- else
313
- job_args
314
- end
141
+ nil # Set in the ActiveJob integration
315
142
  else
316
143
  # Sidekiq Enterprise argument encryption.
317
144
  # More information: https://github.com/mperham/sidekiq/wiki/Ent-Encryption