appsignal 2.8.4-java → 2.9.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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