appsignal 2.9.2.alpha.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 (76) 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 +1 -2
  5. data/.rubocop.yml +3 -0
  6. data/.travis.yml +25 -27
  7. data/CHANGELOG.md +632 -535
  8. data/README.md +8 -3
  9. data/Rakefile +118 -122
  10. data/SUPPORT.md +16 -0
  11. data/appsignal.gemspec +14 -4
  12. data/build_matrix.yml +16 -8
  13. data/ext/Rakefile +2 -3
  14. data/ext/agent.yml +40 -37
  15. data/ext/base.rb +37 -14
  16. data/ext/extconf.rb +3 -4
  17. data/gemfiles/capistrano2.gemfile +5 -0
  18. data/gemfiles/capistrano3.gemfile +5 -0
  19. data/gemfiles/grape.gemfile +5 -0
  20. data/gemfiles/no_dependencies.gemfile +5 -0
  21. data/gemfiles/padrino.gemfile +5 -0
  22. data/gemfiles/que.gemfile +5 -0
  23. data/gemfiles/que_beta.gemfile +10 -0
  24. data/gemfiles/rails-3.2.gemfile +5 -0
  25. data/gemfiles/rails-4.0.gemfile +5 -0
  26. data/gemfiles/rails-4.1.gemfile +5 -0
  27. data/gemfiles/rails-4.2.gemfile +5 -0
  28. data/gemfiles/rails-6.0.gemfile +1 -1
  29. data/gemfiles/resque.gemfile +5 -0
  30. data/lib/appsignal.rb +1 -4
  31. data/lib/appsignal/cli/demo.rb +5 -2
  32. data/lib/appsignal/cli/diagnose/utils.rb +2 -0
  33. data/lib/appsignal/cli/install.rb +34 -10
  34. data/lib/appsignal/cli/notify_of_deploy.rb +10 -0
  35. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +10 -8
  36. data/lib/appsignal/helpers/instrumentation.rb +18 -9
  37. data/lib/appsignal/helpers/metrics.rb +0 -1
  38. data/lib/appsignal/hooks.rb +3 -1
  39. data/lib/appsignal/hooks/active_support_notifications.rb +2 -5
  40. data/lib/appsignal/hooks/puma.rb +15 -13
  41. data/lib/appsignal/hooks/sequel.rb +1 -1
  42. data/lib/appsignal/hooks/sidekiq.rb +33 -8
  43. data/lib/appsignal/integrations/que.rb +9 -8
  44. data/lib/appsignal/minutely.rb +38 -19
  45. data/lib/appsignal/transaction.rb +5 -0
  46. data/lib/appsignal/utils/rails_helper.rb +4 -0
  47. data/lib/appsignal/version.rb +1 -1
  48. data/lib/puma/plugin/appsignal.rb +26 -0
  49. data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +40 -0
  50. data/spec/lib/appsignal/cli/install_spec.rb +51 -7
  51. data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +10 -0
  52. data/spec/lib/appsignal/config_spec.rb +10 -8
  53. data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +38 -28
  54. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +104 -25
  55. data/spec/lib/appsignal/hooks/puma_spec.rb +69 -39
  56. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +65 -3
  57. data/spec/lib/appsignal/hooks_spec.rb +4 -0
  58. data/spec/lib/appsignal/minutely_spec.rb +150 -88
  59. data/spec/lib/appsignal/transaction_spec.rb +27 -4
  60. data/spec/lib/appsignal_spec.rb +62 -11
  61. data/spec/lib/puma/appsignal_spec.rb +91 -0
  62. data/spec/support/{project_fixture → fixtures/projects/valid}/config/application.rb +0 -0
  63. data/spec/support/{project_fixture → fixtures/projects/valid}/config/appsignal.yml +0 -0
  64. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/development.rb +0 -0
  65. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/production.rb +0 -0
  66. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/test.rb +0 -0
  67. data/spec/support/{project_fixture → fixtures/projects/valid}/log/.gitkeep +0 -0
  68. data/spec/support/helpers/config_helpers.rb +1 -1
  69. data/spec/support/helpers/wait_for_helper.rb +28 -0
  70. data/spec/support/mocks/mock_probe.rb +11 -0
  71. metadata +37 -30
  72. data/spec/support/fixtures/containers/cgroups/docker +0 -14
  73. data/spec/support/fixtures/containers/cgroups/docker_systemd +0 -8
  74. data/spec/support/fixtures/containers/cgroups/lxc +0 -10
  75. data/spec/support/fixtures/containers/cgroups/no_permission +0 -0
  76. data/spec/support/fixtures/containers/cgroups/none +0 -1
@@ -60,6 +60,8 @@ module Appsignal
60
60
  # Terminology: Deploy marker
61
61
  class NotifyOfDeploy
62
62
  class << self
63
+ include Appsignal::Utils::DeprecationMessage
64
+
63
65
  # @param options [Hash]
64
66
  # @option options :environment [String] environment to load
65
67
  # configuration for.
@@ -85,6 +87,14 @@ module Appsignal
85
87
  },
86
88
  config
87
89
  ).transmit
90
+
91
+ puts
92
+ message = "This command (appsignal notify_of_deploy) has been " \
93
+ "deprecated in favor of the `revision` config option. Please " \
94
+ "see our documentation for more information on the recommended " \
95
+ "method: " \
96
+ "https://docs.appsignal.com/application/markers/deploy-markers.html"
97
+ deprecation_message message, Appsignal.logger
88
98
  end
89
99
 
90
100
  private
@@ -22,11 +22,13 @@ module Appsignal
22
22
  end
23
23
  end
24
24
 
25
- Appsignal::EventFormatter.register(
26
- "render_partial.action_view",
27
- Appsignal::EventFormatter::ActionView::RenderFormatter
28
- )
29
- Appsignal::EventFormatter.register(
30
- "render_template.action_view",
31
- Appsignal::EventFormatter::ActionView::RenderFormatter
32
- )
25
+ if defined?(Rails)
26
+ Appsignal::EventFormatter.register(
27
+ "render_partial.action_view",
28
+ Appsignal::EventFormatter::ActionView::RenderFormatter
29
+ )
30
+ Appsignal::EventFormatter.register(
31
+ "render_template.action_view",
32
+ Appsignal::EventFormatter::ActionView::RenderFormatter
33
+ )
34
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Appsignal
4
4
  module Helpers
5
- # @api private
6
5
  module Instrumentation # rubocop:disable Metrics/ModuleLength
7
6
  # Creates an AppSignal transaction for the given block.
8
7
  #
@@ -57,8 +56,9 @@ module Appsignal
57
56
  # @return [Object] the value of the given block is returned.
58
57
  # @since 0.10.0
59
58
  def monitor_transaction(name, env = {})
60
- return yield unless active?
61
-
59
+ # Always verify input, even when Appsignal is not active.
60
+ # This makes it more likely invalid arguments get flagged in test/dev
61
+ # environments.
62
62
  if name.start_with?("perform_job".freeze)
63
63
  namespace = Appsignal::Transaction::BACKGROUND_JOB
64
64
  request = Appsignal::Transaction::GenericRequest.new(env)
@@ -66,9 +66,14 @@ module Appsignal
66
66
  namespace = Appsignal::Transaction::HTTP_REQUEST
67
67
  request = ::Rack::Request.new(env)
68
68
  else
69
- logger.error("Unrecognized name '#{name}'")
70
- return
69
+ logger.error "Unrecognized name '#{name}': names must start with " \
70
+ "either 'perform_job' (for jobs and tasks) or 'process_action' " \
71
+ "(for HTTP requests)"
72
+ return yield
71
73
  end
74
+
75
+ return yield unless active?
76
+
72
77
  transaction = Appsignal::Transaction.create(
73
78
  SecureRandom.uuid,
74
79
  namespace,
@@ -196,7 +201,8 @@ module Appsignal
196
201
  )
197
202
  return unless active?
198
203
  unless error.is_a?(Exception)
199
- logger.error("Can't send error, given value is not an exception")
204
+ logger.error "Appsignal.send_error: Cannot send error. The given " \
205
+ "value is not an exception: #{error.inspect}"
200
206
  return
201
207
  end
202
208
  transaction = Appsignal::Transaction.new(
@@ -251,9 +257,12 @@ module Appsignal
251
257
  # Exception handling guide
252
258
  # @since 0.6.6
253
259
  def set_error(exception, tags = nil, namespace = nil)
254
- return if !active? ||
255
- Appsignal::Transaction.current.nil? ||
256
- exception.nil?
260
+ unless exception.is_a?(Exception)
261
+ logger.error "Appsignal.set_error: Cannot set error. The given " \
262
+ "value is not an exception: #{exception.inspect}"
263
+ return
264
+ end
265
+ return if !active? || Appsignal::Transaction.current.nil?
257
266
  transaction = Appsignal::Transaction.current
258
267
  transaction.set_error(exception)
259
268
  transaction.set_tags(tags) if tags
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Appsignal
4
4
  module Helpers
5
- # @api private
6
5
  module Metrics
7
6
  def set_gauge(key, value, tags = {})
8
7
  Appsignal::Extension.set_gauge(
@@ -37,7 +37,9 @@ module Appsignal
37
37
  install
38
38
  @installed = true
39
39
  rescue => ex
40
- Appsignal.logger.error("Error while installing #{name} hook: #{ex}")
40
+ logger = Appsignal.logger
41
+ logger.error("Error while installing #{name} hook: #{ex}")
42
+ logger.debug ex.backtrace.join("\n")
41
43
  end
42
44
  end
43
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,
@@ -11,22 +11,24 @@ module Appsignal
11
11
  end
12
12
 
13
13
  def install
14
- if ::Puma.respond_to?(:stats)
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
15
27
  Appsignal::Minutely.probes.register :puma, PumaProbe
16
28
  end
17
29
 
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
28
- end
29
-
30
+ return unless defined?(::Puma::Cluster)
31
+ # For clustered mode with multiple workers
30
32
  ::Puma::Cluster.class_eval do
31
33
  alias stop_workers_without_appsignal stop_workers
32
34
 
@@ -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
@@ -25,20 +25,42 @@ module Appsignal
25
25
  class SidekiqProbe
26
26
  attr_reader :config
27
27
 
28
+ def self.dependencies_present?
29
+ Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("3.3.5")
30
+ end
31
+
28
32
  def initialize(config = {})
29
33
  @config = config
30
34
  @cache = {}
35
+ config_string = " with config: #{config}" unless config.empty?
36
+ Appsignal.logger.debug("Initializing Sidekiq probe#{config_string}")
31
37
  require "sidekiq/api"
32
38
  end
33
39
 
34
40
  def call
35
- stats = ::Sidekiq::Stats.new
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)
36
52
  redis_info = ::Sidekiq.redis_info
37
- gauge "worker_count", stats.workers_size
38
- gauge "process_count", stats.processes_size
53
+
39
54
  gauge "connection_count", redis_info.fetch("connected_clients")
40
55
  gauge "memory_usage", redis_info.fetch("used_memory")
41
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
42
64
  gauge_delta :jobs_processed, "job_count", stats.processed,
43
65
  :status => :processed
44
66
  gauge_delta :jobs_failed, "job_count", stats.failed, :status => :failed
@@ -46,17 +68,16 @@ module Appsignal
46
68
  gauge_delta :jobs_dead, "job_count", stats.dead_size, :status => :died
47
69
  gauge "job_count", stats.scheduled_size, :status => :scheduled
48
70
  gauge "job_count", stats.enqueued, :status => :enqueued
71
+ end
49
72
 
73
+ def track_queues
50
74
  ::Sidekiq::Queue.all.each do |queue|
51
75
  gauge "queue_length", queue.size, :queue => queue.name
52
- gauge "queue_latency", queue.latency, :queue => queue.name
76
+ # Convert latency from seconds to milliseconds
77
+ gauge "queue_latency", queue.latency * 1_000.0, :queue => queue.name
53
78
  end
54
79
  end
55
80
 
56
- private
57
-
58
- attr_reader :cache
59
-
60
81
  # Track a gauge metric with the `sidekiq_` prefix
61
82
  def gauge(key, value, tags = {})
62
83
  tags[:hostname] = hostname if hostname
@@ -86,11 +107,15 @@ module Appsignal
86
107
  return @hostname if defined?(@hostname)
87
108
  if config.key?(:hostname)
88
109
  @hostname = config[:hostname]
110
+ Appsignal.logger.debug "Sidekiq probe: Using hostname config " \
111
+ "option #{@hostname.inspect} as hostname"
89
112
  return @hostname
90
113
  end
91
114
 
92
115
  host = nil
93
116
  ::Sidekiq.redis { |c| host = c.connection[:host] }
117
+ Appsignal.logger.debug "Sidekiq probe: Using Redis server hostname " \
118
+ "#{host.inspect} as hostname"
94
119
  @hostname = host
95
120
  end
96
121
  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
@@ -135,20 +135,22 @@ module Appsignal
135
135
  # @api private
136
136
  def start
137
137
  stop
138
- initialize_probes
139
138
  @@thread = Thread.new do
139
+ sleep initial_wait_time
140
+ initialize_probes
140
141
  loop do
141
142
  logger = Appsignal.logger
142
- logger.debug("Gathering minutely metrics with #{probes.count} probes")
143
+ logger.debug("Gathering minutely metrics with #{probe_instances.count} probes")
143
144
  probe_instances.each do |name, probe|
144
145
  begin
145
146
  logger.debug("Gathering minutely metrics with '#{name}' probe")
146
147
  probe.call
147
148
  rescue => ex
148
- logger.error("Error in minutely probe '#{name}': #{ex}")
149
+ logger.error "Error in minutely probe '#{name}': #{ex}"
150
+ logger.debug ex.backtrace.join("\n")
149
151
  end
150
152
  end
151
- sleep(Appsignal::Minutely.wait_time)
153
+ sleep wait_time
152
154
  end
153
155
  end
154
156
  end
@@ -164,30 +166,47 @@ module Appsignal
164
166
  60 - Time.now.sec
165
167
  end
166
168
 
167
- # @api private
168
- def register_garbage_collection_probe
169
- probes.register :garbage_collection, GCProbe.new
170
- end
171
-
172
169
  private
173
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
175
+ end
176
+
174
177
  def initialize_probes
175
178
  probes.each do |name, probe|
176
- instance = probe.respond_to?(:new) ? probe.new : probe
177
- probe_instances[name] = instance
179
+ initialize_probe(name, probe)
178
180
  end
179
181
  end
180
182
 
181
- def probe_instances
182
- @@probe_instances ||= {}
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")
183
201
  end
184
- end
185
202
 
186
- class GCProbe
187
- def call
188
- GC.stat.each do |key, value|
189
- Appsignal.set_process_gauge("gc.#{key}", value)
190
- end
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 ||= {}
191
210
  end
192
211
  end
193
212
  end
@@ -264,6 +264,11 @@ module Appsignal
264
264
  end
265
265
 
266
266
  def set_error(error)
267
+ unless error.is_a?(Exception)
268
+ Appsignal.logger.error "Appsignal::Transaction#set_error: Cannot set error. " \
269
+ "The given value is not an exception: #{error.inspect}"
270
+ return
271
+ end
267
272
  return unless error
268
273
  return unless Appsignal.active?
269
274
 
@@ -11,6 +11,10 @@ module Appsignal
11
11
  rails_class.parent_name
12
12
  end
13
13
  end
14
+
15
+ def self.application_config_path
16
+ File.expand_path(File.join(Dir.pwd, "config/application.rb"))
17
+ end
14
18
  end
15
19
  end
16
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.9.2.alpha.1".freeze
4
+ VERSION = "2.9.18.beta.1".freeze
5
5
  end
@@ -0,0 +1,26 @@
1
+ APPSIGNAL_PUMA_PLUGIN_LOADED = true
2
+
3
+ # AppSignal Puma plugin
4
+ #
5
+ # This plugin ensures the minutely probe thread is started with the Puma
6
+ # minutely probe in the Puma master process.
7
+ #
8
+ # The constant {APPSIGNAL_PUMA_PLUGIN_LOADED} is here to mark the Plugin as
9
+ # loaded by the rest of the AppSignal gem. This ensures that the Puma minutely
10
+ # probe is not also started in every Puma workers, which was the old behavior.
11
+ # See {Appsignal::Hooks::PumaHook#install} for more information.
12
+ #
13
+ # For even more information:
14
+ # https://docs.appsignal.com/ruby/integrations/puma.html
15
+ Puma::Plugin.create do
16
+ def start(launcher = nil)
17
+ launcher.events.on_booted do
18
+ require "appsignal"
19
+ if ::Puma.respond_to?(:stats)
20
+ Appsignal::Minutely.probes.register :puma, Appsignal::Hooks::PumaProbe
21
+ end
22
+ Appsignal.start
23
+ Appsignal.start_logger
24
+ end
25
+ end
26
+ end