appsignal 2.10.6 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +138 -62
  4. data/CHANGELOG.md +56 -0
  5. data/README.md +4 -4
  6. data/Rakefile +16 -4
  7. data/appsignal.gemspec +1 -1
  8. data/build_matrix.yml +20 -9
  9. data/ext/Rakefile +2 -0
  10. data/ext/agent.yml +19 -19
  11. data/ext/appsignal_extension.c +10 -1
  12. data/ext/base.rb +22 -4
  13. data/ext/extconf.rb +2 -0
  14. data/gemfiles/padrino.gemfile +2 -2
  15. data/gemfiles/rails-4.2.gemfile +9 -2
  16. data/gemfiles/rails-5.0.gemfile +1 -0
  17. data/gemfiles/rails-5.1.gemfile +1 -0
  18. data/gemfiles/rails-5.2.gemfile +1 -0
  19. data/gemfiles/rails-6.0.gemfile +1 -0
  20. data/gemfiles/resque-1.gemfile +7 -0
  21. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  22. data/lib/appsignal.rb +22 -1
  23. data/lib/appsignal/auth_check.rb +4 -2
  24. data/lib/appsignal/capistrano.rb +2 -0
  25. data/lib/appsignal/cli/diagnose.rb +1 -1
  26. data/lib/appsignal/config.rb +85 -16
  27. data/lib/appsignal/environment.rb +126 -0
  28. data/lib/appsignal/extension.rb +6 -5
  29. data/lib/appsignal/extension/jruby.rb +16 -5
  30. data/lib/appsignal/hooks.rb +25 -0
  31. data/lib/appsignal/hooks/active_job.rb +137 -0
  32. data/lib/appsignal/hooks/net_http.rb +10 -13
  33. data/lib/appsignal/hooks/puma.rb +1 -58
  34. data/lib/appsignal/hooks/redis.rb +2 -0
  35. data/lib/appsignal/hooks/resque.rb +60 -0
  36. data/lib/appsignal/hooks/sequel.rb +2 -0
  37. data/lib/appsignal/hooks/sidekiq.rb +18 -192
  38. data/lib/appsignal/integrations/delayed_job_plugin.rb +16 -3
  39. data/lib/appsignal/integrations/object.rb +4 -0
  40. data/lib/appsignal/integrations/que.rb +1 -1
  41. data/lib/appsignal/integrations/resque.rb +9 -12
  42. data/lib/appsignal/integrations/resque_active_job.rb +9 -24
  43. data/lib/appsignal/probes.rb +7 -0
  44. data/lib/appsignal/probes/puma.rb +61 -0
  45. data/lib/appsignal/probes/sidekiq.rb +104 -0
  46. data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
  47. data/lib/appsignal/system.rb +0 -6
  48. data/lib/appsignal/transaction.rb +32 -7
  49. data/lib/appsignal/utils/deprecation_message.rb +6 -2
  50. data/lib/appsignal/version.rb +1 -1
  51. data/lib/puma/plugin/appsignal.rb +2 -1
  52. data/spec/lib/appsignal/auth_check_spec.rb +23 -0
  53. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  54. data/spec/lib/appsignal/capistrano3_spec.rb +1 -1
  55. data/spec/lib/appsignal/cli/diagnose_spec.rb +44 -1
  56. data/spec/lib/appsignal/config_spec.rb +44 -1
  57. data/spec/lib/appsignal/environment_spec.rb +167 -0
  58. data/spec/lib/appsignal/extension/jruby_spec.rb +31 -28
  59. data/spec/lib/appsignal/extension_install_failure_spec.rb +23 -0
  60. data/spec/lib/appsignal/hooks/activejob_spec.rb +591 -0
  61. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +187 -166
  62. data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
  63. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  64. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +297 -549
  65. data/spec/lib/appsignal/hooks_spec.rb +57 -0
  66. data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
  67. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  68. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -137
  69. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  70. data/spec/lib/appsignal/marker_spec.rb +1 -1
  71. data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
  72. data/spec/lib/appsignal/probes/sidekiq_spec.rb +204 -0
  73. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
  74. data/spec/lib/appsignal/system_spec.rb +0 -36
  75. data/spec/lib/appsignal/transaction_spec.rb +35 -20
  76. data/spec/lib/appsignal_spec.rb +22 -0
  77. data/spec/lib/puma/appsignal_spec.rb +1 -1
  78. data/spec/spec_helper.rb +5 -0
  79. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  80. data/spec/support/helpers/config_helpers.rb +3 -2
  81. data/spec/support/helpers/dependency_helper.rb +12 -0
  82. data/spec/support/helpers/env_helpers.rb +1 -1
  83. data/spec/support/helpers/environment_metdata_helper.rb +16 -0
  84. data/spec/support/helpers/transaction_helpers.rb +6 -0
  85. data/spec/support/stubs/sidekiq/api.rb +2 -2
  86. data/spec/support/testing.rb +19 -19
  87. metadata +31 -9
  88. data/lib/appsignal/integrations/net_http.rb +0 -16
@@ -27,9 +27,8 @@ module Appsignal
27
27
  method_name = "perform"
28
28
  else
29
29
  # Delayed Job
30
- args = extract_value(job.payload_object, :args, {})
31
- class_and_method_name = extract_value(job.payload_object, :appsignal_name, job.name)
32
- class_name, method_name = class_and_method_name.split("#")
30
+ args = extract_value(payload, :args, {})
31
+ class_name, method_name = class_and_method_name_from_object_or_hash(payload, job.name)
33
32
  end
34
33
 
35
34
  params = Appsignal::Utils::HashSanitizer.sanitize(
@@ -54,6 +53,20 @@ module Appsignal
54
53
  end
55
54
  end
56
55
 
56
+ def self.class_and_method_name_from_object_or_hash(payload, default_name)
57
+ # Attempt to find appsignal_name override
58
+ class_and_method_name = extract_value(payload, :appsignal_name, nil)
59
+ return class_and_method_name.split("#") if class_and_method_name.is_a?(String)
60
+
61
+ pound_split = default_name.split("#")
62
+ return pound_split if pound_split.length == 2
63
+
64
+ dot_split = default_name.split(".")
65
+ return default_name if dot_split.length == 2
66
+
67
+ "#{default_name}#perform"
68
+ end
69
+
57
70
  def self.extract_value(object_or_hash, field, default_value = nil, convert_to_s = false)
58
71
  value = nil
59
72
 
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ if defined?(Appsignal)
4
+ Appsignal::Environment.report_enabled("object_instrumentation")
5
+ end
6
+
3
7
  class Object
4
8
  def self.appsignal_instrument_class_method(method_name, options = {})
5
9
  singleton_class.send \
@@ -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
@@ -4,30 +4,15 @@ module Appsignal
4
4
  module Integrations
5
5
  # @api private
6
6
  module ResqueActiveJobPlugin
7
- include Appsignal::Hooks::Helpers
8
-
9
- def self.included(base)
10
- base.class_eval do
11
- around_perform do |job, block|
12
- params = Appsignal::Utils::HashSanitizer.sanitize(
13
- job.arguments,
14
- Appsignal.config[:filter_parameters]
15
- )
16
-
17
- Appsignal.monitor_single_transaction(
18
- "perform_job.resque",
19
- :class => job.class.to_s,
20
- :method => "perform",
21
- :params => params,
22
- :metadata => {
23
- :id => job.job_id,
24
- :queue => job.queue_name
25
- }
26
- ) do
27
- block.call
28
- end
29
- end
30
- end
7
+ def self.included(_)
8
+ callers = caller
9
+ Appsignal::Utils::DeprecationMessage.message \
10
+ "The AppSignal ResqueActiveJobPlugin 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: include Appsignal::Integrations::ResqueActiveJobPlugin\n" \
15
+ "#{callers.first}"
31
16
  end
32
17
  end
33
18
  end
@@ -0,0 +1,7 @@
1
+ module Appsignal
2
+ module Probes
3
+ end
4
+ end
5
+
6
+ require "appsignal/probes/puma"
7
+ require "appsignal/probes/sidekiq"
@@ -0,0 +1,61 @@
1
+ module Appsignal
2
+ module Probes
3
+ class PumaProbe
4
+ def initialize
5
+ @hostname = Appsignal.config[:hostname] || Socket.gethostname
6
+ end
7
+
8
+ # @api private
9
+ def call
10
+ puma_stats = fetch_puma_stats
11
+ return unless puma_stats
12
+
13
+ stats = JSON.parse puma_stats, :symbolize_names => true
14
+ counts = {}
15
+ count_keys = [:backlog, :running, :pool_capacity, :max_threads]
16
+
17
+ if stats[:worker_status] # Multiple workers
18
+ stats[:worker_status].each do |worker|
19
+ stat = worker[:last_status]
20
+ count_keys.each do |key|
21
+ count_if_present counts, key, stat
22
+ end
23
+ end
24
+
25
+ gauge(:workers, stats[:workers], :type => :count)
26
+ gauge(:workers, stats[:booted_workers], :type => :booted)
27
+ gauge(:workers, stats[:old_workers], :type => :old)
28
+ else # Single worker
29
+ count_keys.each do |key|
30
+ count_if_present counts, key, stats
31
+ end
32
+ end
33
+
34
+ gauge(:connection_backlog, counts[:backlog]) if counts[:backlog]
35
+ gauge(:pool_capacity, counts[:pool_capacity]) if counts[:pool_capacity]
36
+ gauge(:threads, counts[:running], :type => :running) if counts[:running]
37
+ gauge(:threads, counts[:max_threads], :type => :max) if counts[:max_threads]
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :hostname
43
+
44
+ def gauge(field, count, tags = {})
45
+ Appsignal.set_gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
46
+ end
47
+
48
+ def count_if_present(counts, key, stats)
49
+ stat_value = stats[key]
50
+ return unless stat_value
51
+ counts[key] ||= 0
52
+ counts[key] += stat_value
53
+ end
54
+
55
+ def fetch_puma_stats
56
+ ::Puma.stats
57
+ rescue NoMethodError # rubocop:disable Lint/HandleExceptions
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,104 @@
1
+ module Appsignal
2
+ module Probes
3
+ class SidekiqProbe
4
+ # @api private
5
+ attr_reader :config
6
+
7
+ # @api private
8
+ def self.dependencies_present?
9
+ Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("3.3.5")
10
+ end
11
+
12
+ def initialize(config = {})
13
+ @config = config
14
+ @cache = {}
15
+ config_string = " with config: #{config}" unless config.empty?
16
+ Appsignal.logger.debug("Initializing Sidekiq probe#{config_string}")
17
+ require "sidekiq/api"
18
+ end
19
+
20
+ # @api private
21
+ def call
22
+ track_redis_info
23
+ track_stats
24
+ track_queues
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :cache
30
+
31
+ def track_redis_info
32
+ return unless ::Sidekiq.respond_to?(:redis_info)
33
+ redis_info = ::Sidekiq.redis_info
34
+
35
+ gauge "connection_count", redis_info.fetch("connected_clients")
36
+ gauge "memory_usage", redis_info.fetch("used_memory")
37
+ gauge "memory_usage_rss", redis_info.fetch("used_memory_rss")
38
+ end
39
+
40
+ def track_stats
41
+ stats = ::Sidekiq::Stats.new
42
+
43
+ gauge "worker_count", stats.workers_size
44
+ gauge "process_count", stats.processes_size
45
+ gauge_delta :jobs_processed, "job_count", stats.processed,
46
+ :status => :processed
47
+ gauge_delta :jobs_failed, "job_count", stats.failed, :status => :failed
48
+ gauge "job_count", stats.retry_size, :status => :retry_queue
49
+ gauge_delta :jobs_dead, "job_count", stats.dead_size, :status => :died
50
+ gauge "job_count", stats.scheduled_size, :status => :scheduled
51
+ gauge "job_count", stats.enqueued, :status => :enqueued
52
+ end
53
+
54
+ def track_queues
55
+ ::Sidekiq::Queue.all.each do |queue|
56
+ gauge "queue_length", queue.size, :queue => queue.name
57
+ # Convert latency from seconds to milliseconds
58
+ gauge "queue_latency", queue.latency * 1_000.0, :queue => queue.name
59
+ end
60
+ end
61
+
62
+ # Track a gauge metric with the `sidekiq_` prefix
63
+ def gauge(key, value, tags = {})
64
+ tags[:hostname] = hostname if hostname
65
+ Appsignal.set_gauge "sidekiq_#{key}", value, tags
66
+ end
67
+
68
+ # Track the delta of two values for a gauge metric
69
+ #
70
+ # First call will store the data for the metric and the second call will
71
+ # set a gauge metric with the difference. This is used for absolute
72
+ # counter values which we want to track as gauges.
73
+ #
74
+ # @example
75
+ # gauge_delta :my_cache_key, "my_gauge", 10
76
+ # gauge_delta :my_cache_key, "my_gauge", 15
77
+ # # Creates a gauge with the value `5`
78
+ # @see #gauge
79
+ def gauge_delta(cache_key, key, value, tags = {})
80
+ previous_value = cache[cache_key]
81
+ cache[cache_key] = value
82
+ return unless previous_value
83
+ new_value = value - previous_value
84
+ gauge key, new_value, tags
85
+ end
86
+
87
+ def hostname
88
+ return @hostname if defined?(@hostname)
89
+ if config.key?(:hostname)
90
+ @hostname = config[:hostname]
91
+ Appsignal.logger.debug "Sidekiq probe: Using hostname config " \
92
+ "option #{@hostname.inspect} as hostname"
93
+ return @hostname
94
+ end
95
+
96
+ host = nil
97
+ ::Sidekiq.redis { |c| host = c.connection[:host] }
98
+ Appsignal.logger.debug "Sidekiq probe: Using Redis server hostname " \
99
+ "#{host.inspect} as hostname"
100
+ @hostname = host
101
+ end
102
+ end
103
+ end
104
+ end
@@ -29,8 +29,11 @@ module Appsignal
29
29
  Appsignal.logger.debug \
30
30
  "Initializing Appsignal::Rack::JSExceptionCatcher"
31
31
  deprecation_message "The Appsignal::Rack::JSExceptionCatcher is " \
32
- "deprecated. Please use the official AppSignal JavaScript " \
33
- "integration instead. https://docs.appsignal.com/front-end/"
32
+ "deprecated and will be removed in a future version. Please use " \
33
+ "the official AppSignal JavaScript integration by disabling " \
34
+ "`enable_frontend_error_catching` in your configuration and " \
35
+ "installing AppSignal for JavaScript instead. " \
36
+ "(https://docs.appsignal.com/front-end/)"
34
37
  @app = app
35
38
  end
36
39
 
@@ -76,14 +76,8 @@ module Appsignal
76
76
  ldd_version && ldd_version[0]
77
77
  end
78
78
 
79
- # @api private
80
79
  def self.jruby?
81
80
  RUBY_PLATFORM == "java"
82
81
  end
83
-
84
- # @api private
85
- def self.ruby_2_or_up?
86
- versionify(RUBY_VERSION) >= versionify("2.0")
87
- end
88
82
  end
89
83
  end
@@ -221,6 +221,16 @@ module Appsignal
221
221
  set_action_if_nil(group_and_action.compact.join("#"))
222
222
  end
223
223
 
224
+ # Set queue start time for transaction.
225
+ #
226
+ # Most commononly called by {set_http_or_background_queue_start}.
227
+ #
228
+ # @param start [Integer] Queue start time in milliseconds.
229
+ # @raise [RangeError] When the queue start time value is too big, this
230
+ # method raises a RangeError.
231
+ # @raise [TypeError] Raises a TypeError when the given `start` argument is
232
+ # not an Integer.
233
+ # @return [void]
224
234
  def set_queue_start(start)
225
235
  return unless start
226
236
  @ext.set_queue_start(start)
@@ -228,12 +238,27 @@ module Appsignal
228
238
  Appsignal.logger.warn("Queue start value #{start} is too big")
229
239
  end
230
240
 
241
+ # Set the queue time based on the HTTP header or `:queue_start` env key
242
+ # value.
243
+ #
244
+ # This method will first try to read the queue time from the HTTP headers
245
+ # `X-Request-Start` or `X-Queue-Start`. Which are parsed by Rack as
246
+ # `HTTP_X_QUEUE_START` and `HTTP_X_REQUEST_START`.
247
+ # The header value is parsed by AppSignal as either milliseconds or
248
+ # microseconds.
249
+ #
250
+ # If no headers are found, or the value could not be parsed, it falls back
251
+ # on the `:queue_start` env key on this Transaction's {request} environment
252
+ # (called like `request.env[:queue_start]`). This value is parsed by
253
+ # AppSignal as seconds.
254
+ #
255
+ # @see https://docs.appsignal.com/ruby/instrumentation/request-queue-time.html
256
+ # @return [void]
231
257
  def set_http_or_background_queue_start
232
- if namespace == HTTP_REQUEST
233
- set_queue_start(http_queue_start)
234
- elsif namespace == BACKGROUND_JOB
235
- set_queue_start(background_queue_start)
236
- end
258
+ start = http_queue_start || background_queue_start
259
+ return unless start
260
+
261
+ set_queue_start(start)
237
262
  end
238
263
 
239
264
  def set_metadata(key, value)
@@ -346,14 +371,14 @@ module Appsignal
346
371
  #
347
372
  # @return [nil] if no {#environment} is present.
348
373
  # @return [nil] if there is no `:queue_start` in the {#environment}.
349
- # @return [Integer]
374
+ # @return [Integer] `:queue_start` time (in seconds) converted to milliseconds
350
375
  def background_queue_start
351
376
  env = environment
352
377
  return unless env
353
378
  queue_start = env[:queue_start]
354
379
  return unless queue_start
355
380
 
356
- (queue_start.to_f * 1000.0).to_i
381
+ (queue_start.to_f * 1000.0).to_i # Convert seconds to milliseconds
357
382
  end
358
383
 
359
384
  # Returns HTTP queue start time in milliseconds.
@@ -1,10 +1,14 @@
1
1
  module Appsignal
2
2
  module Utils
3
3
  module DeprecationMessage
4
- def deprecation_message(message, logger = Appsignal.logger)
5
- $stderr.puts "appsignal WARNING: #{message}"
4
+ def self.message(message, logger = Appsignal.logger)
5
+ Kernel.warn "appsignal WARNING: #{message}"
6
6
  logger.warn message
7
7
  end
8
+
9
+ def deprecation_message(message, logger = Appsignal.logger)
10
+ Appsignal::Utils::DeprecationMessage.message(message, logger)
11
+ end
8
12
  end
9
13
  end
10
14
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.10.6".freeze
4
+ VERSION = "2.11.0".freeze
5
5
  end
@@ -17,7 +17,8 @@ Puma::Plugin.create do
17
17
  launcher.events.on_booted do
18
18
  require "appsignal"
19
19
  if ::Puma.respond_to?(:stats)
20
- Appsignal::Minutely.probes.register :puma, Appsignal::Hooks::PumaProbe
20
+ require "appsignal/probes/puma"
21
+ Appsignal::Minutely.probes.register :puma, Appsignal::Probes::PumaProbe
21
22
  end
22
23
  Appsignal.start
23
24
  Appsignal.start_logger
@@ -28,6 +28,29 @@ describe Appsignal::AuthCheck do
28
28
  end.join("&")
29
29
  end
30
30
 
31
+ describe ".new" do
32
+ describe "with logger argument" do
33
+ let(:err_stream) { std_stream }
34
+ let(:stderr) { err_stream.read }
35
+ let(:log_stream) { std_stream }
36
+ let(:log) { log_contents(log_stream) }
37
+
38
+ it "logs and prints a deprecation message" do
39
+ Appsignal.logger = test_logger(log_stream)
40
+
41
+ capture_std_streams(std_stream, err_stream) do
42
+ Appsignal::AuthCheck.new(config, Appsignal.logger)
43
+ end
44
+
45
+ deprecation_message =
46
+ "`Appsignal::AuthCheck.new`'s `logger` argument " \
47
+ "will be removed in the next major version."
48
+ expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
49
+ expect(log).to contains_log :warn, deprecation_message
50
+ end
51
+ end
52
+ end
53
+
31
54
  describe "#perform" do
32
55
  subject { auth_check.perform }
33
56