appsignal 2.9.2.alpha.1 → 2.9.18.beta.2

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 (77) hide show
  1. checksums.yaml +5 -5
  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/config.rb +5 -2
  36. data/lib/appsignal/event_formatter/action_view/render_formatter.rb +10 -8
  37. data/lib/appsignal/helpers/instrumentation.rb +18 -9
  38. data/lib/appsignal/helpers/metrics.rb +0 -1
  39. data/lib/appsignal/hooks.rb +3 -1
  40. data/lib/appsignal/hooks/active_support_notifications.rb +2 -5
  41. data/lib/appsignal/hooks/puma.rb +15 -13
  42. data/lib/appsignal/hooks/sequel.rb +1 -1
  43. data/lib/appsignal/hooks/sidekiq.rb +33 -8
  44. data/lib/appsignal/integrations/que.rb +9 -8
  45. data/lib/appsignal/minutely.rb +38 -19
  46. data/lib/appsignal/transaction.rb +5 -0
  47. data/lib/appsignal/utils/rails_helper.rb +4 -0
  48. data/lib/appsignal/version.rb +1 -1
  49. data/lib/puma/plugin/appsignal.rb +26 -0
  50. data/spec/lib/appsignal/cli/diagnose/utils_spec.rb +40 -0
  51. data/spec/lib/appsignal/cli/install_spec.rb +51 -7
  52. data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +10 -0
  53. data/spec/lib/appsignal/config_spec.rb +13 -8
  54. data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +38 -28
  55. data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +104 -25
  56. data/spec/lib/appsignal/hooks/puma_spec.rb +69 -39
  57. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +65 -3
  58. data/spec/lib/appsignal/hooks_spec.rb +4 -0
  59. data/spec/lib/appsignal/minutely_spec.rb +150 -88
  60. data/spec/lib/appsignal/transaction_spec.rb +27 -4
  61. data/spec/lib/appsignal_spec.rb +62 -11
  62. data/spec/lib/puma/appsignal_spec.rb +91 -0
  63. data/spec/support/{project_fixture → fixtures/projects/valid}/config/application.rb +0 -0
  64. data/spec/support/{project_fixture → fixtures/projects/valid}/config/appsignal.yml +0 -0
  65. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/development.rb +0 -0
  66. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/production.rb +0 -0
  67. data/spec/support/{project_fixture → fixtures/projects/valid}/config/environments/test.rb +0 -0
  68. data/spec/support/{project_fixture → fixtures/projects/valid}/log/.gitkeep +0 -0
  69. data/spec/support/helpers/config_helpers.rb +1 -1
  70. data/spec/support/helpers/wait_for_helper.rb +28 -0
  71. data/spec/support/mocks/mock_probe.rb +11 -0
  72. metadata +38 -30
  73. data/spec/support/fixtures/containers/cgroups/docker +0 -14
  74. data/spec/support/fixtures/containers/cgroups/docker_systemd +0 -8
  75. data/spec/support/fixtures/containers/cgroups/lxc +0 -10
  76. data/spec/support/fixtures/containers/cgroups/no_permission +0 -0
  77. 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
@@ -39,7 +39,8 @@ module Appsignal
39
39
  :enable_minutely_probes => true,
40
40
  :ca_file_path => File.expand_path(File.join("../../../resources/cacert.pem"), __FILE__),
41
41
  :dns_servers => [],
42
- :files_world_accessible => true
42
+ :files_world_accessible => true,
43
+ :transaction_debug_mode => false
43
44
  }.freeze
44
45
 
45
46
  ENV_TO_KEY_MAPPING = {
@@ -75,6 +76,7 @@ module Appsignal
75
76
  "APPSIGNAL_DNS_SERVERS" => :dns_servers,
76
77
  "APPSIGNAL_FILES_WORLD_ACCESSIBLE" => :files_world_accessible,
77
78
  "APPSIGNAL_REQUEST_HEADERS" => :request_headers,
79
+ "APPSIGNAL_TRANSACTION_DEBUG_MODE" => :transaction_debug_mode,
78
80
  "APP_REVISION" => :revision
79
81
  }.freeze
80
82
 
@@ -219,6 +221,7 @@ module Appsignal
219
221
  ENV["_APPSIGNAL_CA_FILE_PATH"] = config_hash[:ca_file_path].to_s
220
222
  ENV["_APPSIGNAL_DNS_SERVERS"] = config_hash[:dns_servers].join(",")
221
223
  ENV["_APPSIGNAL_FILES_WORLD_ACCESSIBLE"] = config_hash[:files_world_accessible].to_s
224
+ ENV["_APPSIGNAL_TRANSACTION_DEBUG_MODE"] = config_hash[:transaction_debug_mode].to_s
222
225
  ENV["_APP_REVISION"] = config_hash[:revision].to_s
223
226
  end
224
227
 
@@ -324,7 +327,7 @@ module Appsignal
324
327
  APPSIGNAL_ENABLE_ALLOCATION_TRACKING APPSIGNAL_ENABLE_GC_INSTRUMENTATION
325
328
  APPSIGNAL_RUNNING_IN_CONTAINER APPSIGNAL_ENABLE_HOST_METRICS
326
329
  APPSIGNAL_SEND_PARAMS APPSIGNAL_ENABLE_MINUTELY_PROBES
327
- APPSIGNAL_FILES_WORLD_ACCESSIBLE].each do |var|
330
+ APPSIGNAL_FILES_WORLD_ACCESSIBLE APPSIGNAL_TRANSACTION_DEBUG_MODE].each do |var|
328
331
  env_var = ENV[var]
329
332
  next unless env_var
330
333
  config[ENV_TO_KEY_MAPPING[var]] = env_var.casecmp("true").zero?
@@ -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