appsignal 2.8.4.beta.1 → 2.9.18.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
  3. data/.github/ISSUE_TEMPLATE/chore.md +14 -0
  4. data/.gitignore +2 -3
  5. data/.rubocop.yml +3 -0
  6. data/.rubocop_todo.yml +7 -16
  7. data/.travis.yml +28 -27
  8. data/CHANGELOG.md +657 -533
  9. data/README.md +31 -3
  10. data/Rakefile +128 -129
  11. data/SUPPORT.md +16 -0
  12. data/appsignal.gemspec +17 -4
  13. data/build_matrix.yml +21 -9
  14. data/ext/Rakefile +23 -17
  15. data/ext/agent.yml +40 -37
  16. data/ext/base.rb +116 -31
  17. data/ext/extconf.rb +34 -28
  18. data/gemfiles/capistrano2.gemfile +5 -0
  19. data/gemfiles/capistrano3.gemfile +5 -0
  20. data/gemfiles/grape.gemfile +5 -0
  21. data/gemfiles/no_dependencies.gemfile +5 -0
  22. data/gemfiles/padrino.gemfile +5 -0
  23. data/gemfiles/que.gemfile +5 -0
  24. data/gemfiles/que_beta.gemfile +10 -0
  25. data/gemfiles/rails-3.2.gemfile +5 -0
  26. data/gemfiles/rails-4.0.gemfile +5 -0
  27. data/gemfiles/rails-4.1.gemfile +5 -0
  28. data/gemfiles/rails-4.2.gemfile +5 -0
  29. data/gemfiles/rails-6.0.gemfile +5 -0
  30. data/gemfiles/resque.gemfile +5 -0
  31. data/lib/appsignal.rb +14 -492
  32. data/lib/appsignal/cli/demo.rb +5 -2
  33. data/lib/appsignal/cli/diagnose.rb +84 -4
  34. data/lib/appsignal/cli/diagnose/paths.rb +0 -5
  35. data/lib/appsignal/cli/diagnose/utils.rb +19 -0
  36. data/lib/appsignal/cli/helpers.rb +6 -0
  37. data/lib/appsignal/cli/install.rb +45 -15
  38. data/lib/appsignal/cli/notify_of_deploy.rb +10 -0
  39. data/lib/appsignal/config.rb +1 -2
  40. data/lib/appsignal/event_formatter.rb +4 -5
  41. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +10 -8
  42. data/lib/appsignal/event_formatter/moped/query_formatter.rb +60 -59
  43. data/lib/appsignal/extension.rb +2 -2
  44. data/lib/appsignal/helpers/instrumentation.rb +494 -0
  45. data/lib/appsignal/helpers/metrics.rb +54 -0
  46. data/lib/appsignal/hooks.rb +11 -8
  47. data/lib/appsignal/hooks/active_support_notifications.rb +2 -5
  48. data/lib/appsignal/hooks/puma.rb +74 -11
  49. data/lib/appsignal/hooks/sequel.rb +1 -1
  50. data/lib/appsignal/hooks/sidekiq.rb +115 -0
  51. data/lib/appsignal/integrations/mongo_ruby_driver.rb +7 -0
  52. data/lib/appsignal/integrations/que.rb +9 -8
  53. data/lib/appsignal/integrations/railtie.rb +2 -1
  54. data/lib/appsignal/marker.rb +2 -3
  55. data/lib/appsignal/minutely.rb +188 -19
  56. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -1
  57. data/lib/appsignal/system.rb +16 -18
  58. data/lib/appsignal/transaction.rb +8 -0
  59. data/lib/appsignal/utils/rails_helper.rb +20 -0
  60. data/lib/appsignal/version.rb +1 -1
  61. data/lib/puma/plugin/appsignal.rb +26 -0
  62. data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +40 -0
  63. data/spec/lib/appsignal/cli/diagnose_spec.rb +129 -22
  64. data/spec/lib/appsignal/cli/install_spec.rb +57 -8
  65. data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +10 -0
  66. data/spec/lib/appsignal/config_spec.rb +13 -11
  67. data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +38 -28
  68. data/spec/lib/appsignal/event_formatter/moped/query_formatter_spec.rb +6 -0
  69. data/spec/lib/appsignal/event_formatter_spec.rb +168 -69
  70. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +104 -25
  71. data/spec/lib/appsignal/hooks/puma_spec.rb +251 -34
  72. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +209 -0
  73. data/spec/lib/appsignal/hooks_spec.rb +4 -0
  74. data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +24 -1
  75. data/spec/lib/appsignal/minutely_spec.rb +318 -26
  76. data/spec/lib/appsignal/system_spec.rb +0 -35
  77. data/spec/lib/appsignal/transaction_spec.rb +68 -10
  78. data/spec/lib/appsignal/utils/hash_sanitizer_spec.rb +39 -31
  79. data/spec/lib/appsignal/utils/json_spec.rb +7 -3
  80. data/spec/lib/appsignal_spec.rb +98 -22
  81. data/spec/lib/puma/appsignal_spec.rb +91 -0
  82. data/spec/spec_helper.rb +13 -0
  83. data/spec/support/{project_fixture → fixtures/projects/valid}/config/application.rb +0 -0
  84. data/spec/support/{project_fixture → fixtures/projects/valid}/config/appsignal.yml +1 -0
  85. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/development.rb +0 -0
  86. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/production.rb +0 -0
  87. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/test.rb +0 -0
  88. data/spec/support/{project_fixture → fixtures/projects/valid}/log/.gitkeep +0 -0
  89. data/spec/support/helpers/config_helpers.rb +1 -1
  90. data/spec/support/helpers/log_helpers.rb +6 -0
  91. data/spec/support/helpers/wait_for_helper.rb +28 -0
  92. data/spec/support/mocks/mock_probe.rb +11 -0
  93. data/spec/support/stubs/sidekiq/api.rb +4 -0
  94. metadata +43 -31
  95. data/spec/support/fixtures/containers/cgroups/docker +0 -14
  96. data/spec/support/fixtures/containers/cgroups/docker_systemd +0 -8
  97. data/spec/support/fixtures/containers/cgroups/lxc +0 -10
  98. data/spec/support/fixtures/containers/cgroups/no_permission +0 -0
  99. data/spec/support/fixtures/containers/cgroups/none +0 -1
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Helpers
5
+ module Metrics
6
+ def set_gauge(key, value, tags = {})
7
+ Appsignal::Extension.set_gauge(
8
+ key.to_s,
9
+ value.to_f,
10
+ Appsignal::Utils::Data.generate(tags)
11
+ )
12
+ rescue RangeError
13
+ Appsignal.logger
14
+ .warn("Gauge value #{value} for key '#{key}' is too big")
15
+ end
16
+
17
+ def set_host_gauge(key, value)
18
+ Appsignal::Extension.set_host_gauge(key.to_s, value.to_f)
19
+ rescue RangeError
20
+ Appsignal.logger
21
+ .warn("Host gauge value #{value} for key '#{key}' is too big")
22
+ end
23
+
24
+ def set_process_gauge(key, value)
25
+ Appsignal::Extension.set_process_gauge(key.to_s, value.to_f)
26
+ rescue RangeError
27
+ Appsignal.logger
28
+ .warn("Process gauge value #{value} for key '#{key}' is too big")
29
+ end
30
+
31
+ def increment_counter(key, value = 1.0, tags = {})
32
+ Appsignal::Extension.increment_counter(
33
+ key.to_s,
34
+ value.to_f,
35
+ Appsignal::Utils::Data.generate(tags)
36
+ )
37
+ rescue RangeError
38
+ Appsignal.logger
39
+ .warn("Counter value #{value} for key '#{key}' is too big")
40
+ end
41
+
42
+ def add_distribution_value(key, value, tags = {})
43
+ Appsignal::Extension.add_distribution_value(
44
+ key.to_s,
45
+ value.to_f,
46
+ Appsignal::Utils::Data.generate(tags)
47
+ )
48
+ rescue RangeError
49
+ Appsignal.logger
50
+ .warn("Distribution value #{value} for key '#{key}' is too big")
51
+ end
52
+ end
53
+ end
54
+ end
@@ -29,14 +29,17 @@ module Appsignal
29
29
  end
30
30
 
31
31
  def try_to_install(name)
32
- if dependencies_present? && !installed?
33
- Appsignal.logger.info("Installing #{name} hook")
34
- begin
35
- install
36
- @installed = true
37
- rescue => ex
38
- Appsignal.logger.error("Error while installing #{name} hook: #{ex}")
39
- end
32
+ return unless dependencies_present?
33
+ return if installed?
34
+
35
+ Appsignal.logger.info("Installing #{name} hook")
36
+ begin
37
+ install
38
+ @installed = true
39
+ rescue => ex
40
+ logger = Appsignal.logger
41
+ logger.error("Error while installing #{name} hook: #{ex}")
42
+ logger.debug ex.backtrace.join("\n")
40
43
  end
41
44
  end
42
45
 
@@ -30,16 +30,13 @@ module Appsignal
30
30
  # Events that start with a bang are internal to Rails
31
31
  instrument_this = name[0] != BANG
32
32
 
33
- if instrument_this
34
- transaction = Appsignal::Transaction.current
35
- transaction.start_event
36
- end
33
+ Appsignal::Transaction.current.start_event if instrument_this
37
34
 
38
35
  instrument_without_appsignal(name, payload, &block)
39
36
  ensure
40
37
  if instrument_this
41
38
  title, body, body_format = Appsignal::EventFormatter.format(name, payload)
42
- transaction.finish_event(
39
+ Appsignal::Transaction.current.finish_event(
43
40
  name.to_s,
44
41
  title,
45
42
  body,
@@ -7,22 +7,28 @@ module Appsignal
7
7
  register :puma
8
8
 
9
9
  def dependencies_present?
10
- defined?(::Puma) &&
11
- ::Puma.respond_to?(:cli_config) &&
12
- ::Puma.cli_config
10
+ defined?(::Puma)
13
11
  end
14
12
 
15
13
  def install
16
- ::Puma.cli_config.options[:before_worker_boot] ||= []
17
- ::Puma.cli_config.options[:before_worker_boot] << proc do |_id|
18
- Appsignal.forked
19
- end
20
-
21
- ::Puma.cli_config.options[:before_worker_shutdown] ||= []
22
- ::Puma.cli_config.options[:before_worker_shutdown] << proc do |_id|
23
- Appsignal.stop("puma before_worker_shutdown")
14
+ if ::Puma.respond_to?(:stats) && !defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)
15
+ # Only install the minutely probe if a user isn't using our Puma
16
+ # plugin, which lives in `lib/puma/appsignal.rb`. This plugin defines
17
+ # the {APPSIGNAL_PUMA_PLUGIN_LOADED} constant.
18
+ #
19
+ # We prefer people use the AppSignal Puma plugin. This fallback is
20
+ # only there when users relied on our *magic* integration.
21
+ #
22
+ # Using the Puma plugin, the minutely probe thread will still run in
23
+ # Puma workers, for other non-Puma probes, but the Puma probe only
24
+ # runs in the Puma main process.
25
+ # For more information:
26
+ # https://docs.appsignal.com/ruby/integrations/puma.html
27
+ Appsignal::Minutely.probes.register :puma, PumaProbe
24
28
  end
25
29
 
30
+ return unless defined?(::Puma::Cluster)
31
+ # For clustered mode with multiple workers
26
32
  ::Puma::Cluster.class_eval do
27
33
  alias stop_workers_without_appsignal stop_workers
28
34
 
@@ -33,5 +39,62 @@ module Appsignal
33
39
  end
34
40
  end
35
41
  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
36
99
  end
37
100
  end
@@ -42,7 +42,7 @@ module Appsignal
42
42
 
43
43
  def install
44
44
  # Register the extension...
45
- if ::Sequel::MAJOR >= 4 && ::Sequel::MINOR >= 35
45
+ if (::Sequel::MAJOR >= 4 && ::Sequel::MINOR >= 35) || ::Sequel::MAJOR >= 5
46
46
  ::Sequel::Database.register_extension(
47
47
  :appsignal_integration,
48
48
  Appsignal::Hooks::SequelLogConnectionExtension
@@ -12,6 +12,8 @@ module Appsignal
12
12
  end
13
13
 
14
14
  def install
15
+ Appsignal::Minutely.probes.register :sidekiq, SidekiqProbe
16
+
15
17
  ::Sidekiq.configure_server do |config|
16
18
  config.server_middleware do |chain|
17
19
  chain.add Appsignal::Hooks::SidekiqPlugin
@@ -20,6 +22,104 @@ module Appsignal
20
22
  end
21
23
  end
22
24
 
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
+
23
123
  # @api private
24
124
  class SidekiqPlugin # rubocop:disable Metrics/ClassLength
25
125
  include Appsignal::Hooks::Helpers
@@ -31,6 +131,7 @@ module Appsignal
31
131
  ].freeze
32
132
 
33
133
  def call(_worker, item, _queue)
134
+ job_status = nil
34
135
  transaction = Appsignal::Transaction.create(
35
136
  SecureRandom.uuid,
36
137
  Appsignal::Transaction::BACKGROUND_JOB,
@@ -43,6 +144,7 @@ module Appsignal
43
144
  begin
44
145
  yield
45
146
  rescue Exception => exception # rubocop:disable Lint/RescueException
147
+ job_status = :failed
46
148
  transaction.set_error(exception)
47
149
  raise exception
48
150
  end
@@ -56,11 +158,24 @@ module Appsignal
56
158
  end
57
159
  transaction.set_http_or_background_queue_start
58
160
  Appsignal::Transaction.complete_current!
161
+ queue = item["queue"] || "unknown"
162
+ if job_status
163
+ increment_counter "queue_job_count", 1,
164
+ :queue => queue,
165
+ :status => job_status
166
+ end
167
+ increment_counter "queue_job_count", 1,
168
+ :queue => queue,
169
+ :status => :processed
59
170
  end
60
171
  end
61
172
 
62
173
  private
63
174
 
175
+ def increment_counter(key, value, tags = {})
176
+ Appsignal.increment_counter "sidekiq_#{key}", value, tags
177
+ end
178
+
64
179
  def formatted_action_name(job)
65
180
  sidekiq_action_name = parse_action_name(job)
66
181
  complete_action = sidekiq_action_name =~ /\.|#/
@@ -51,6 +51,13 @@ module Appsignal
51
51
  Appsignal::Utils::Data.generate(command),
52
52
  Appsignal::EventFormatter::DEFAULT
53
53
  )
54
+
55
+ # Send global query metrics
56
+ Appsignal.add_distribution_value(
57
+ "mongodb_query_duration",
58
+ event.duration,
59
+ :database => event.database_name
60
+ )
54
61
  end
55
62
  end
56
63
  end
@@ -5,16 +5,17 @@ module Appsignal
5
5
  module QuePlugin
6
6
  def self.included(base)
7
7
  base.class_eval do
8
- def _run_with_appsignal
8
+ def _run_with_appsignal(*)
9
+ local_attrs = respond_to?(:que_attrs) ? que_attrs : attrs
9
10
  env = {
10
11
  :metadata => {
11
- :id => attrs[:job_id],
12
- :queue => attrs[:queue],
13
- :run_at => attrs[:run_at].to_s,
14
- :priority => attrs[:priority],
15
- :attempts => attrs[:error_count].to_i
12
+ :id => local_attrs[:job_id] || local_attrs[:id],
13
+ :queue => local_attrs[:queue],
14
+ :run_at => local_attrs[:run_at].to_s,
15
+ :priority => local_attrs[:priority],
16
+ :attempts => local_attrs[:error_count].to_i
16
17
  },
17
- :params => attrs[:args]
18
+ :params => local_attrs[:args]
18
19
  }
19
20
 
20
21
  request = Appsignal::Transaction::GenericRequest.new(env)
@@ -31,7 +32,7 @@ module Appsignal
31
32
  transaction.set_error(error)
32
33
  raise error
33
34
  ensure
34
- transaction.set_action "#{attrs[:job_class]}#run"
35
+ transaction.set_action "#{local_attrs[:job_class]}#run"
35
36
  Appsignal::Transaction.complete_current!
36
37
  end
37
38
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  Appsignal.logger.info("Loading Rails (#{Rails.version}) integration")
4
4
 
5
+ require "appsignal/utils/rails_helper"
5
6
  require "appsignal/rack/rails_instrumentation"
6
7
 
7
8
  module Appsignal
@@ -17,7 +18,7 @@ module Appsignal
17
18
  Appsignal.config = Appsignal::Config.new(
18
19
  Rails.root,
19
20
  Rails.env,
20
- :name => Rails.application.class.parent_name,
21
+ :name => Appsignal::Utils::RailsHelper.detected_rails_app_name,
21
22
  :log_path => Rails.root.join("log")
22
23
  )
23
24
 
@@ -53,11 +53,10 @@ module Appsignal
53
53
  "revision: #{marker_data[:revision]}, user: #{marker_data[:user]}"
54
54
 
55
55
  response = transmitter.transmit(marker_data)
56
- if response.code == "200"
57
- puts "AppSignal has been notified of this deploy!"
58
- else
56
+ unless response.code == "200"
59
57
  raise "#{response.code} at #{transmitter.uri}"
60
58
  end
59
+ puts "AppSignal has been notified of this deploy!"
61
60
  rescue => e
62
61
  puts "Something went wrong while trying to notify AppSignal: #{e}"
63
62
  end
@@ -1,44 +1,213 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- # @api private
5
4
  class Minutely
5
+ class ProbeCollection
6
+ include Appsignal::Utils::DeprecationMessage
7
+
8
+ def initialize
9
+ @probes = {}
10
+ end
11
+
12
+ # @return [Integer] Number of probes that are registered.
13
+ def count
14
+ probes.count
15
+ end
16
+
17
+ # Clears all probes from the list.
18
+ # @return [void]
19
+ def clear
20
+ probes.clear
21
+ end
22
+
23
+ # Fetch a probe using its name.
24
+ # @param key [Symbol/String] The name of the probe to fetch.
25
+ # @return [Object] Returns the registered probe.
26
+ def [](key)
27
+ probes[key]
28
+ end
29
+
30
+ # @param probe [Object] Any object that listens to the `call` method will
31
+ # be used as a probe.
32
+ # @deprecated Use {#register} instead.
33
+ # @return [void]
34
+ def <<(probe)
35
+ deprecation_message "Deprecated `Appsignal::Minute.probes <<` " \
36
+ "call. Please use `Appsignal::Minutely.probes.register` instead.",
37
+ logger
38
+ register probe.object_id, probe
39
+ end
40
+
41
+ # Register a new minutely probe.
42
+ #
43
+ # Supported probe types are:
44
+ #
45
+ # - Lambda - A lambda is an object that listens to a `call` method call.
46
+ # This `call` method is called every minute.
47
+ # - Class - A class object is an object that listens to a `new` and
48
+ # `call` method call. The `new` method is called when the Minutely
49
+ # probe thread is started to initialize all probes. This allows probes
50
+ # to load dependencies once beforehand. Their `call` method is called
51
+ # every minute.
52
+ # - Class instance - A class instance object is an object that listens to
53
+ # a `call` method call. The `call` method is called every minute.
54
+ #
55
+ # @example Register a new probe
56
+ # Appsignal::Minutely.probes.register :my_probe, lambda {}
57
+ #
58
+ # @example Overwrite an existing registered probe
59
+ # Appsignal::Minutely.probes.register :my_probe, lambda {}
60
+ # Appsignal::Minutely.probes.register :my_probe, lambda { puts "hello" }
61
+ #
62
+ # @example Add a lambda as a probe
63
+ # Appsignal::Minutely.probes.register :my_probe, lambda { puts "hello" }
64
+ # # "hello" # printed every minute
65
+ #
66
+ # @example Add a probe instance
67
+ # class MyProbe
68
+ # def initialize
69
+ # puts "started"
70
+ # end
71
+ #
72
+ # def call
73
+ # puts "called"
74
+ # end
75
+ # end
76
+ #
77
+ # Appsignal::Minutely.probes.register :my_probe, MyProbe.new
78
+ # # "started" # printed immediately
79
+ # # "called" # printed every minute
80
+ #
81
+ # @example Add a probe class
82
+ # class MyProbe
83
+ # def initialize
84
+ # # Add things that only need to be done on start up for this probe
85
+ # require "some/library/dependency"
86
+ # @cache = {} # initialize a local cache variable
87
+ # puts "started"
88
+ # end
89
+ #
90
+ # def call
91
+ # puts "called"
92
+ # end
93
+ # end
94
+ #
95
+ # Appsignal::Minutely.probes.register :my_probe, MyProbe
96
+ # Appsignal::Minutely.start # This is called for you
97
+ # # "started" # Printed on Appsignal::Minutely.start
98
+ # # "called" # Repeated every minute
99
+ #
100
+ # @param name [Symbol/String] Name of the probe. Can be used with {[]}.
101
+ # This name will be used in errors in the log and allows overwriting of
102
+ # probes by registering new ones with the same name.
103
+ # @param probe [Object] Any object that listens to the `call` method will
104
+ # be used as a probe.
105
+ # @return [void]
106
+ def register(name, probe)
107
+ if probes.key?(name)
108
+ logger.debug "A probe with the name `#{name}` is already " \
109
+ "registered. Overwriting the entry with the new probe."
110
+ end
111
+ probes[name] = probe
112
+ end
113
+
114
+ # @api private
115
+ def each(&block)
116
+ probes.each(&block)
117
+ end
118
+
119
+ private
120
+
121
+ attr_reader :probes
122
+
123
+ def logger
124
+ Appsignal.logger
125
+ end
126
+ end
127
+
6
128
  class << self
7
- # List of probes. Probes can be lamdba's or objects that
8
- # respond to call.
129
+ # @see ProbeCollection
130
+ # @return [ProbeCollection] Returns list of probes.
9
131
  def probes
10
- @@probes ||= []
132
+ @@probes ||= ProbeCollection.new
11
133
  end
12
134
 
135
+ # @api private
13
136
  def start
14
- Thread.new do
15
- begin
16
- loop do
17
- Appsignal.logger.debug("Gathering minutely metrics with #{probes.count} probe(s)")
18
- probes.each(&:call)
19
- sleep(wait_time)
137
+ stop
138
+ @@thread = Thread.new do
139
+ sleep initial_wait_time
140
+ initialize_probes
141
+ loop do
142
+ logger = Appsignal.logger
143
+ logger.debug("Gathering minutely metrics with #{probe_instances.count} probes")
144
+ probe_instances.each do |name, probe|
145
+ begin
146
+ logger.debug("Gathering minutely metrics with '#{name}' probe")
147
+ probe.call
148
+ rescue => ex
149
+ logger.error "Error in minutely probe '#{name}': #{ex}"
150
+ logger.debug ex.backtrace.join("\n")
151
+ end
20
152
  end
21
- rescue => ex
22
- Appsignal.logger.error("Error in minutely thread: #{ex}")
153
+ sleep wait_time
23
154
  end
24
155
  end
25
156
  end
26
157
 
158
+ # @api private
159
+ def stop
160
+ defined?(@@thread) && @@thread.kill
161
+ probe_instances.clear
162
+ end
163
+
164
+ # @api private
27
165
  def wait_time
28
166
  60 - Time.now.sec
29
167
  end
30
168
 
31
- def add_gc_probe
32
- probes << GCProbe.new
169
+ private
170
+
171
+ def initial_wait_time
172
+ remaining_seconds = 60 - Time.now.sec
173
+ return remaining_seconds if remaining_seconds > 30
174
+ remaining_seconds + 60
33
175
  end
34
- end
35
176
 
36
- class GCProbe
37
- def call
38
- GC.stat.each do |key, value|
39
- Appsignal.set_process_gauge("gc.#{key}", value)
177
+ def initialize_probes
178
+ probes.each do |name, probe|
179
+ initialize_probe(name, probe)
40
180
  end
41
181
  end
182
+
183
+ def initialize_probe(name, probe)
184
+ if probe.respond_to? :new
185
+ instance = probe.new
186
+ klass = probe
187
+ else
188
+ instance = probe
189
+ klass = instance.class
190
+ end
191
+ unless dependencies_present?(klass)
192
+ Appsignal.logger.debug "Skipping '#{name}' probe, " \
193
+ "#{klass}.dependency_present? returned falsy"
194
+ return
195
+ end
196
+ probe_instances[name] = instance
197
+ rescue => error
198
+ logger = Appsignal.logger
199
+ logger.error "Error while initializing minutely probe '#{name}': #{error}"
200
+ logger.debug error.backtrace.join("\n")
201
+ end
202
+
203
+ def dependencies_present?(probe)
204
+ return true unless probe.respond_to? :dependencies_present?
205
+ probe.dependencies_present?
206
+ end
207
+
208
+ def probe_instances
209
+ @@probe_instances ||= {}
210
+ end
42
211
  end
43
212
  end
44
213
  end