appsignal 2.8.4 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rubocop_todo.yml +7 -16
  4. data/.travis.yml +4 -1
  5. data/CHANGELOG.md +16 -0
  6. data/README.md +23 -0
  7. data/Rakefile +10 -7
  8. data/appsignal.gemspec +3 -0
  9. data/build_matrix.yml +5 -1
  10. data/ext/Rakefile +23 -16
  11. data/ext/agent.yml +37 -37
  12. data/ext/base.rb +86 -24
  13. data/ext/extconf.rb +33 -26
  14. data/gemfiles/rails-6.0.gemfile +5 -0
  15. data/lib/appsignal.rb +14 -489
  16. data/lib/appsignal/cli/diagnose.rb +84 -4
  17. data/lib/appsignal/cli/diagnose/paths.rb +0 -5
  18. data/lib/appsignal/cli/diagnose/utils.rb +17 -0
  19. data/lib/appsignal/cli/helpers.rb +6 -0
  20. data/lib/appsignal/cli/install.rb +13 -7
  21. data/lib/appsignal/config.rb +1 -2
  22. data/lib/appsignal/event_formatter.rb +4 -5
  23. data/lib/appsignal/event_formatter/moped/query_formatter.rb +60 -59
  24. data/lib/appsignal/extension.rb +2 -2
  25. data/lib/appsignal/helpers/instrumentation.rb +485 -0
  26. data/lib/appsignal/helpers/metrics.rb +55 -0
  27. data/lib/appsignal/hooks.rb +9 -8
  28. data/lib/appsignal/hooks/puma.rb +65 -9
  29. data/lib/appsignal/hooks/sidekiq.rb +90 -0
  30. data/lib/appsignal/integrations/mongo_ruby_driver.rb +7 -0
  31. data/lib/appsignal/integrations/railtie.rb +2 -1
  32. data/lib/appsignal/marker.rb +2 -3
  33. data/lib/appsignal/minutely.rb +164 -14
  34. data/lib/appsignal/rack/sinatra_instrumentation.rb +1 -1
  35. data/lib/appsignal/system.rb +16 -18
  36. data/lib/appsignal/utils/rails_helper.rb +16 -0
  37. data/lib/appsignal/version.rb +1 -1
  38. data/spec/lib/appsignal/cli/diagnose_spec.rb +129 -22
  39. data/spec/lib/appsignal/cli/install_spec.rb +6 -1
  40. data/spec/lib/appsignal/config_spec.rb +3 -3
  41. data/spec/lib/appsignal/event_formatter/moped/query_formatter_spec.rb +6 -0
  42. data/spec/lib/appsignal/event_formatter_spec.rb +168 -69
  43. data/spec/lib/appsignal/hooks/puma_spec.rb +129 -0
  44. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +147 -0
  45. data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +24 -1
  46. data/spec/lib/appsignal/minutely_spec.rb +251 -21
  47. data/spec/lib/appsignal/system_spec.rb +0 -35
  48. data/spec/lib/appsignal/utils/hash_sanitizer_spec.rb +39 -31
  49. data/spec/lib/appsignal/utils/json_spec.rb +7 -3
  50. data/spec/lib/appsignal_spec.rb +27 -2
  51. data/spec/spec_helper.rb +13 -0
  52. data/spec/support/helpers/log_helpers.rb +6 -0
  53. data/spec/support/project_fixture/config/appsignal.yml +1 -0
  54. data/spec/support/stubs/sidekiq/api.rb +4 -0
  55. metadata +8 -2
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Helpers
5
+ # @api private
6
+ module Metrics
7
+ def set_gauge(key, value, tags = {})
8
+ Appsignal::Extension.set_gauge(
9
+ key.to_s,
10
+ value.to_f,
11
+ Appsignal::Utils::Data.generate(tags)
12
+ )
13
+ rescue RangeError
14
+ Appsignal.logger
15
+ .warn("Gauge value #{value} for key '#{key}' is too big")
16
+ end
17
+
18
+ def set_host_gauge(key, value)
19
+ Appsignal::Extension.set_host_gauge(key.to_s, value.to_f)
20
+ rescue RangeError
21
+ Appsignal.logger
22
+ .warn("Host gauge value #{value} for key '#{key}' is too big")
23
+ end
24
+
25
+ def set_process_gauge(key, value)
26
+ Appsignal::Extension.set_process_gauge(key.to_s, value.to_f)
27
+ rescue RangeError
28
+ Appsignal.logger
29
+ .warn("Process gauge value #{value} for key '#{key}' is too big")
30
+ end
31
+
32
+ def increment_counter(key, value = 1.0, tags = {})
33
+ Appsignal::Extension.increment_counter(
34
+ key.to_s,
35
+ value.to_f,
36
+ Appsignal::Utils::Data.generate(tags)
37
+ )
38
+ rescue RangeError
39
+ Appsignal.logger
40
+ .warn("Counter value #{value} for key '#{key}' is too big")
41
+ end
42
+
43
+ def add_distribution_value(key, value, tags = {})
44
+ Appsignal::Extension.add_distribution_value(
45
+ key.to_s,
46
+ value.to_f,
47
+ Appsignal::Utils::Data.generate(tags)
48
+ )
49
+ rescue RangeError
50
+ Appsignal.logger
51
+ .warn("Distribution value #{value} for key '#{key}' is too big")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -29,14 +29,15 @@ 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
+ Appsignal.logger.error("Error while installing #{name} hook: #{ex}")
40
41
  end
41
42
  end
42
43
 
@@ -7,20 +7,24 @@ 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
14
+ if ::Puma.respond_to?(:stats)
15
+ Appsignal::Minutely.probes.register :puma, PumaProbe
19
16
  end
20
17
 
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")
18
+ if ::Puma.respond_to?(:cli_config) && ::Puma.cli_config
19
+ ::Puma.cli_config.options[:before_worker_boot] ||= []
20
+ ::Puma.cli_config.options[:before_worker_boot] << proc do |_id|
21
+ Appsignal.forked
22
+ end
23
+
24
+ ::Puma.cli_config.options[:before_worker_shutdown] ||= []
25
+ ::Puma.cli_config.options[:before_worker_shutdown] << proc do |_id|
26
+ Appsignal.stop("puma before_worker_shutdown")
27
+ end
24
28
  end
25
29
 
26
30
  ::Puma::Cluster.class_eval do
@@ -33,5 +37,57 @@ module Appsignal
33
37
  end
34
38
  end
35
39
  end
40
+
41
+ class PumaProbe
42
+ def initialize
43
+ @hostname = Appsignal.config[:hostname] || Socket.gethostname
44
+ end
45
+
46
+ def call
47
+ return unless ::Puma.stats
48
+
49
+ stats = JSON.parse Puma.stats, :symbolize_names => true
50
+ counts = {
51
+ :backlog => 0,
52
+ :running => 0,
53
+ :pool_capacity => 0,
54
+ :max_threads => 0
55
+ }
56
+
57
+ if stats[:worker_status] # Multiple workers
58
+ stats[:worker_status].each do |worker|
59
+ stat = worker[:last_status]
60
+
61
+ counts[:backlog] += stat[:backlog]
62
+ counts[:running] += stat[:running]
63
+ counts[:pool_capacity] += stat[:pool_capacity]
64
+ counts[:max_threads] += stat[:max_threads]
65
+ end
66
+
67
+ gauge(:workers, stats[:workers], :type => :count)
68
+ gauge(:workers, stats[:booted_workers], :type => :booted)
69
+ gauge(:workers, stats[:old_workers], :type => :old)
70
+
71
+ else # Single worker
72
+ counts[:backlog] += stats[:backlog]
73
+ counts[:running] += stats[:running]
74
+ counts[:pool_capacity] += stats[:pool_capacity]
75
+ counts[:max_threads] += stats[:max_threads]
76
+ end
77
+
78
+ gauge(:connection_backlog, counts[:backlog])
79
+ gauge(:pool_capacity, counts[:pool_capacity])
80
+ gauge(:threads, counts[:running], :type => :running)
81
+ gauge(:threads, counts[:max_threads], :type => :max)
82
+ end
83
+
84
+ private
85
+
86
+ attr_reader :hostname
87
+
88
+ def gauge(field, count, tags = {})
89
+ Appsignal.set_gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
90
+ end
91
+ end
36
92
  end
37
93
  end
@@ -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,79 @@ module Appsignal
20
22
  end
21
23
  end
22
24
 
25
+ class SidekiqProbe
26
+ attr_reader :config
27
+
28
+ def initialize(config = {})
29
+ @config = config
30
+ @cache = {}
31
+ require "sidekiq/api"
32
+ end
33
+
34
+ def call
35
+ stats = ::Sidekiq::Stats.new
36
+ redis_info = ::Sidekiq.redis_info
37
+ gauge "worker_count", stats.workers_size
38
+ gauge "process_count", stats.processes_size
39
+ gauge "connection_count", redis_info.fetch("connected_clients")
40
+ gauge "memory_usage", redis_info.fetch("used_memory")
41
+ gauge "memory_usage_rss", redis_info.fetch("used_memory_rss")
42
+ gauge_delta :jobs_processed, "job_count", stats.processed,
43
+ :status => :processed
44
+ gauge_delta :jobs_failed, "job_count", stats.failed, :status => :failed
45
+ gauge "job_count", stats.retry_size, :status => :retry_queue
46
+ gauge_delta :jobs_dead, "job_count", stats.dead_size, :status => :died
47
+ gauge "job_count", stats.scheduled_size, :status => :scheduled
48
+ gauge "job_count", stats.enqueued, :status => :enqueued
49
+
50
+ ::Sidekiq::Queue.all.each do |queue|
51
+ gauge "queue_length", queue.size, :queue => queue.name
52
+ gauge "queue_latency", queue.latency, :queue => queue.name
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :cache
59
+
60
+ # Track a gauge metric with the `sidekiq_` prefix
61
+ def gauge(key, value, tags = {})
62
+ tags[:hostname] = hostname if hostname
63
+ Appsignal.set_gauge "sidekiq_#{key}", value, tags
64
+ end
65
+
66
+ # Track the delta of two values for a gauge metric
67
+ #
68
+ # First call will store the data for the metric and the second call will
69
+ # set a gauge metric with the difference. This is used for absolute
70
+ # counter values which we want to track as gauges.
71
+ #
72
+ # @example
73
+ # gauge_delta :my_cache_key, "my_gauge", 10
74
+ # gauge_delta :my_cache_key, "my_gauge", 15
75
+ # # Creates a gauge with the value `5`
76
+ # @see #gauge
77
+ def gauge_delta(cache_key, key, value, tags = {})
78
+ previous_value = cache[cache_key]
79
+ cache[cache_key] = value
80
+ return unless previous_value
81
+ new_value = value - previous_value
82
+ gauge key, new_value, tags
83
+ end
84
+
85
+ def hostname
86
+ return @hostname if defined?(@hostname)
87
+ if config.key?(:hostname)
88
+ @hostname = config[:hostname]
89
+ return @hostname
90
+ end
91
+
92
+ host = nil
93
+ ::Sidekiq.redis { |c| host = c.connection[:host] }
94
+ @hostname = host
95
+ end
96
+ end
97
+
23
98
  # @api private
24
99
  class SidekiqPlugin # rubocop:disable Metrics/ClassLength
25
100
  include Appsignal::Hooks::Helpers
@@ -31,6 +106,7 @@ module Appsignal
31
106
  ].freeze
32
107
 
33
108
  def call(_worker, item, _queue)
109
+ job_status = nil
34
110
  transaction = Appsignal::Transaction.create(
35
111
  SecureRandom.uuid,
36
112
  Appsignal::Transaction::BACKGROUND_JOB,
@@ -43,6 +119,7 @@ module Appsignal
43
119
  begin
44
120
  yield
45
121
  rescue Exception => exception # rubocop:disable Lint/RescueException
122
+ job_status = :failed
46
123
  transaction.set_error(exception)
47
124
  raise exception
48
125
  end
@@ -56,11 +133,24 @@ module Appsignal
56
133
  end
57
134
  transaction.set_http_or_background_queue_start
58
135
  Appsignal::Transaction.complete_current!
136
+ queue = item["queue"] || "unknown"
137
+ if job_status
138
+ increment_counter "queue_job_count", 1,
139
+ :queue => queue,
140
+ :status => job_status
141
+ end
142
+ increment_counter "queue_job_count", 1,
143
+ :queue => queue,
144
+ :status => :processed
59
145
  end
60
146
  end
61
147
 
62
148
  private
63
149
 
150
+ def increment_counter(key, value, tags = {})
151
+ Appsignal.increment_counter "sidekiq_#{key}", value, tags
152
+ end
153
+
64
154
  def formatted_action_name(job)
65
155
  sidekiq_action_name = parse_action_name(job)
66
156
  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
@@ -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,35 +1,185 @@
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
+ initialize_probes
139
+ @@thread = Thread.new do
140
+ loop do
141
+ logger = Appsignal.logger
142
+ logger.debug("Gathering minutely metrics with #{probes.count} probes")
143
+ probe_instances.each do |name, probe|
144
+ begin
145
+ logger.debug("Gathering minutely metrics with '#{name}' probe")
146
+ probe.call
147
+ rescue => ex
148
+ logger.error("Error in minutely probe '#{name}': #{ex}")
149
+ end
20
150
  end
21
- rescue => ex
22
- Appsignal.logger.error("Error in minutely thread: #{ex}")
151
+ sleep(Appsignal::Minutely.wait_time)
23
152
  end
24
153
  end
25
154
  end
26
155
 
156
+ # @api private
157
+ def stop
158
+ defined?(@@thread) && @@thread.kill
159
+ probe_instances.clear
160
+ end
161
+
162
+ # @api private
27
163
  def wait_time
28
164
  60 - Time.now.sec
29
165
  end
30
166
 
31
- def add_gc_probe
32
- probes << GCProbe.new
167
+ # @api private
168
+ def register_garbage_collection_probe
169
+ probes.register :garbage_collection, GCProbe.new
170
+ end
171
+
172
+ private
173
+
174
+ def initialize_probes
175
+ probes.each do |name, probe|
176
+ instance = probe.respond_to?(:new) ? probe.new : probe
177
+ probe_instances[name] = instance
178
+ end
179
+ end
180
+
181
+ def probe_instances
182
+ @@probe_instances ||= {}
33
183
  end
34
184
  end
35
185