appsignal 2.10.7-java → 2.11.0.alpha.2-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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/.semaphore/semaphore.yml +45 -53
  3. data/CHANGELOG.md +18 -0
  4. data/build_matrix.yml +13 -6
  5. data/ext/agent.yml +19 -19
  6. data/ext/appsignal_extension.c +10 -1
  7. data/ext/base.rb +15 -4
  8. data/gemfiles/padrino.gemfile +2 -2
  9. data/lib/appsignal.rb +21 -1
  10. data/lib/appsignal/capistrano.rb +2 -0
  11. data/lib/appsignal/config.rb +6 -2
  12. data/lib/appsignal/environment.rb +126 -0
  13. data/lib/appsignal/extension/jruby.rb +10 -0
  14. data/lib/appsignal/hooks/net_http.rb +2 -0
  15. data/lib/appsignal/hooks/puma.rb +2 -58
  16. data/lib/appsignal/hooks/redis.rb +2 -0
  17. data/lib/appsignal/hooks/sequel.rb +2 -0
  18. data/lib/appsignal/hooks/sidekiq.rb +2 -99
  19. data/lib/appsignal/integrations/delayed_job_plugin.rb +16 -3
  20. data/lib/appsignal/integrations/object.rb +4 -0
  21. data/lib/appsignal/integrations/resque_active_job.rb +12 -4
  22. data/lib/appsignal/probes/puma.rb +61 -0
  23. data/lib/appsignal/probes/sidekiq.rb +102 -0
  24. data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
  25. data/lib/appsignal/transaction.rb +22 -7
  26. data/lib/appsignal/version.rb +1 -1
  27. data/lib/puma/plugin/appsignal.rb +2 -1
  28. data/spec/lib/appsignal/cli/diagnose_spec.rb +2 -1
  29. data/spec/lib/appsignal/config_spec.rb +6 -1
  30. data/spec/lib/appsignal/environment_spec.rb +167 -0
  31. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +198 -166
  32. data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
  33. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +256 -462
  34. data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
  35. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +55 -13
  36. data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
  37. data/spec/lib/appsignal/probes/sidekiq_spec.rb +201 -0
  38. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
  39. data/spec/lib/appsignal/transaction_spec.rb +30 -13
  40. data/spec/lib/appsignal_spec.rb +22 -0
  41. data/spec/lib/puma/appsignal_spec.rb +1 -1
  42. data/spec/support/helpers/dependency_helper.rb +5 -0
  43. data/spec/support/helpers/env_helpers.rb +1 -1
  44. data/spec/support/helpers/environment_metdata_helper.rb +16 -0
  45. data/spec/support/stubs/sidekiq/api.rb +1 -1
  46. metadata +19 -8
@@ -27,9 +27,8 @@ module Appsignal
27
27
  method_name = "perform"
28
28
  else
29
29
  # Delayed Job
30
- args = extract_value(job.payload_object, :args, {})
31
- class_and_method_name = extract_value(job.payload_object, :appsignal_name, job.name)
32
- class_name, method_name = class_and_method_name.split("#")
30
+ args = extract_value(payload, :args, {})
31
+ class_name, method_name = class_and_method_name_from_object_or_hash(payload, job.name)
33
32
  end
34
33
 
35
34
  params = Appsignal::Utils::HashSanitizer.sanitize(
@@ -54,6 +53,20 @@ module Appsignal
54
53
  end
55
54
  end
56
55
 
56
+ def self.class_and_method_name_from_object_or_hash(payload, default_name)
57
+ # Attempt to find appsignal_name override
58
+ class_and_method_name = extract_value(payload, :appsignal_name, nil)
59
+ return class_and_method_name.split("#") if class_and_method_name.is_a?(String)
60
+
61
+ pound_split = default_name.split("#")
62
+ return pound_split if pound_split.length == 2
63
+
64
+ dot_split = default_name.split(".")
65
+ return default_name if dot_split.length == 2
66
+
67
+ ["unknown"]
68
+ end
69
+
57
70
  def self.extract_value(object_or_hash, field, default_value = nil, convert_to_s = false)
58
71
  value = nil
59
72
 
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ if defined?(Appsignal)
4
+ Appsignal::Environment.report_enabled("object_instrumentation")
5
+ end
6
+
3
7
  class Object
4
8
  def self.appsignal_instrument_class_method(method_name, options = {})
5
9
  singleton_class.send \
@@ -14,12 +14,18 @@ module Appsignal
14
14
  Appsignal.config[:filter_parameters]
15
15
  )
16
16
 
17
+ queue_start =
18
+ if job.respond_to?(:enqueued_at) && job.enqueued_at
19
+ Time.parse(job.enqueued_at).utc
20
+ end
21
+
17
22
  Appsignal.monitor_single_transaction(
18
23
  "perform_job.resque",
19
- :class => job.class.to_s,
20
- :method => "perform",
21
- :params => params,
22
- :metadata => {
24
+ :class => job.class.to_s,
25
+ :method => "perform",
26
+ :params => params,
27
+ :queue_start => queue_start,
28
+ :metadata => {
23
29
  :id => job.job_id,
24
30
  :queue => job.queue_name
25
31
  }
@@ -28,6 +34,8 @@ module Appsignal
28
34
  end
29
35
  end
30
36
  end
37
+
38
+ Appsignal::Environment.report("ruby_active_job_resque_enabled") { true }
31
39
  end
32
40
  end
33
41
  end
@@ -0,0 +1,61 @@
1
+ module Appsignal
2
+ module Probes
3
+ # @api private
4
+ class PumaProbe
5
+ def initialize
6
+ @hostname = Appsignal.config[:hostname] || Socket.gethostname
7
+ end
8
+
9
+ def call
10
+ puma_stats = fetch_puma_stats
11
+ return unless puma_stats
12
+
13
+ stats = JSON.parse puma_stats, :symbolize_names => true
14
+ counts = {}
15
+ count_keys = [:backlog, :running, :pool_capacity, :max_threads]
16
+
17
+ if stats[:worker_status] # Multiple workers
18
+ stats[:worker_status].each do |worker|
19
+ stat = worker[:last_status]
20
+ count_keys.each do |key|
21
+ count_if_present counts, key, stat
22
+ end
23
+ end
24
+
25
+ gauge(:workers, stats[:workers], :type => :count)
26
+ gauge(:workers, stats[:booted_workers], :type => :booted)
27
+ gauge(:workers, stats[:old_workers], :type => :old)
28
+ else # Single worker
29
+ count_keys.each do |key|
30
+ count_if_present counts, key, stats
31
+ end
32
+ end
33
+
34
+ gauge(:connection_backlog, counts[:backlog]) if counts[:backlog]
35
+ gauge(:pool_capacity, counts[:pool_capacity]) if counts[:pool_capacity]
36
+ gauge(:threads, counts[:running], :type => :running) if counts[:running]
37
+ gauge(:threads, counts[:max_threads], :type => :max) if counts[:max_threads]
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :hostname
43
+
44
+ def gauge(field, count, tags = {})
45
+ Appsignal.set_gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
46
+ end
47
+
48
+ def count_if_present(counts, key, stats)
49
+ stat_value = stats[key]
50
+ return unless stat_value
51
+ counts[key] ||= 0
52
+ counts[key] += stat_value
53
+ end
54
+
55
+ def fetch_puma_stats
56
+ ::Puma.stats
57
+ rescue NoMethodError # rubocop:disable Lint/HandleExceptions
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,102 @@
1
+ module Appsignal
2
+ module Probes
3
+ # @api private
4
+ class SidekiqProbe
5
+ attr_reader :config
6
+
7
+ def self.dependencies_present?
8
+ Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("3.3.5")
9
+ end
10
+
11
+ def initialize(config = {})
12
+ @config = config
13
+ @cache = {}
14
+ config_string = " with config: #{config}" unless config.empty?
15
+ Appsignal.logger.debug("Initializing Sidekiq probe#{config_string}")
16
+ require "sidekiq/api"
17
+ end
18
+
19
+ def call
20
+ track_redis_info
21
+ track_stats
22
+ track_queues
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :cache
28
+
29
+ def track_redis_info
30
+ return unless ::Sidekiq.respond_to?(:redis_info)
31
+ redis_info = ::Sidekiq.redis_info
32
+
33
+ gauge "connection_count", redis_info.fetch("connected_clients")
34
+ gauge "memory_usage", redis_info.fetch("used_memory")
35
+ gauge "memory_usage_rss", redis_info.fetch("used_memory_rss")
36
+ end
37
+
38
+ def track_stats
39
+ stats = ::Sidekiq::Stats.new
40
+
41
+ gauge "worker_count", stats.workers_size
42
+ gauge "process_count", stats.processes_size
43
+ gauge_delta :jobs_processed, "job_count", stats.processed,
44
+ :status => :processed
45
+ gauge_delta :jobs_failed, "job_count", stats.failed, :status => :failed
46
+ gauge "job_count", stats.retry_size, :status => :retry_queue
47
+ gauge_delta :jobs_dead, "job_count", stats.dead_size, :status => :died
48
+ gauge "job_count", stats.scheduled_size, :status => :scheduled
49
+ gauge "job_count", stats.enqueued, :status => :enqueued
50
+ end
51
+
52
+ def track_queues
53
+ ::Sidekiq::Queue.all.each do |queue|
54
+ gauge "queue_length", queue.size, :queue => queue.name
55
+ # Convert latency from seconds to milliseconds
56
+ gauge "queue_latency", queue.latency * 1_000.0, :queue => queue.name
57
+ end
58
+ end
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
+ Appsignal.logger.debug "Sidekiq probe: Using hostname config " \
90
+ "option #{@hostname.inspect} as hostname"
91
+ return @hostname
92
+ end
93
+
94
+ host = nil
95
+ ::Sidekiq.redis { |c| host = c.connection[:host] }
96
+ Appsignal.logger.debug "Sidekiq probe: Using Redis server hostname " \
97
+ "#{host.inspect} as hostname"
98
+ @hostname = host
99
+ end
100
+ end
101
+ end
102
+ end
@@ -29,8 +29,11 @@ module Appsignal
29
29
  Appsignal.logger.debug \
30
30
  "Initializing Appsignal::Rack::JSExceptionCatcher"
31
31
  deprecation_message "The Appsignal::Rack::JSExceptionCatcher is " \
32
- "deprecated. Please use the official AppSignal JavaScript " \
33
- "integration instead. https://docs.appsignal.com/front-end/"
32
+ "deprecated and will be removed in a future version. Please use " \
33
+ "the official AppSignal JavaScript integration by disabling " \
34
+ "`enable_frontend_error_catching` in your configuration and " \
35
+ "installing AppSignal for JavaScript instead. " \
36
+ "(https://docs.appsignal.com/front-end/)"
34
37
  @app = app
35
38
  end
36
39
 
@@ -228,12 +228,27 @@ module Appsignal
228
228
  Appsignal.logger.warn("Queue start value #{start} is too big")
229
229
  end
230
230
 
231
+ # Set the queue time based on the HTTP header or `:queue_start` env key
232
+ # value.
233
+ #
234
+ # This method will first try to read the queue time from the HTTP headers
235
+ # `X-Request-Start` or `X-Queue-Start`. Which are parsed by Rack as
236
+ # `HTTP_X_QUEUE_START` and `HTTP_X_REQUEST_START`.
237
+ # The header value is parsed by AppSignal as either milliseconds or
238
+ # microseconds.
239
+ #
240
+ # If no headers are found, or the value could not be parsed, it falls back
241
+ # on the `:queue_start` env key on this Transaction's {request} environment
242
+ # (called like `request.env[:queue_start]`). This value is parsed by
243
+ # AppSignal as seconds.
244
+ #
245
+ # @see https://docs.appsignal.com/ruby/instrumentation/request-queue-time.html
246
+ # @return [void]
231
247
  def set_http_or_background_queue_start
232
- if namespace == HTTP_REQUEST
233
- set_queue_start(http_queue_start)
234
- elsif namespace == BACKGROUND_JOB
235
- set_queue_start(background_queue_start)
236
- end
248
+ start = http_queue_start || background_queue_start
249
+ return unless start
250
+
251
+ set_queue_start(start)
237
252
  end
238
253
 
239
254
  def set_metadata(key, value)
@@ -346,14 +361,14 @@ module Appsignal
346
361
  #
347
362
  # @return [nil] if no {#environment} is present.
348
363
  # @return [nil] if there is no `:queue_start` in the {#environment}.
349
- # @return [Integer]
364
+ # @return [Integer] `:queue_start` time (in seconds) converted to milliseconds
350
365
  def background_queue_start
351
366
  env = environment
352
367
  return unless env
353
368
  queue_start = env[:queue_start]
354
369
  return unless queue_start
355
370
 
356
- (queue_start.to_f * 1000.0).to_i
371
+ (queue_start.to_f * 1000.0).to_i # Convert seconds to milliseconds
357
372
  end
358
373
 
359
374
  # Returns HTTP queue start time in milliseconds.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.10.7".freeze
4
+ VERSION = "2.11.0.alpha.2".freeze
5
5
  end
@@ -17,7 +17,8 @@ Puma::Plugin.create do
17
17
  launcher.events.on_booted do
18
18
  require "appsignal"
19
19
  if ::Puma.respond_to?(:stats)
20
- Appsignal::Minutely.probes.register :puma, Appsignal::Hooks::PumaProbe
20
+ require "appsignal/probes/puma"
21
+ Appsignal::Minutely.probes.register :puma, Appsignal::Probes::PumaProbe
21
22
  end
22
23
  Appsignal.start
23
24
  Appsignal.start_logger
@@ -263,7 +263,8 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
263
263
  },
264
264
  "download" => {
265
265
  "download_url" => kind_of(String),
266
- "checksum" => "verified"
266
+ "checksum" => "verified",
267
+ "http_proxy" => nil
267
268
  },
268
269
  "build" => {
269
270
  "time" => kind_of(String),
@@ -148,6 +148,7 @@ describe Appsignal::Config do
148
148
  :instrument_redis => true,
149
149
  :instrument_sequel => true,
150
150
  :skip_session_data => false,
151
+ :send_environment_metadata => true,
151
152
  :send_params => true,
152
153
  :endpoint => "https://push.appsignal.com",
153
154
  :push_api_key => "abc",
@@ -411,7 +412,8 @@ describe Appsignal::Config do
411
412
  :instrument_sequel => false,
412
413
  :files_world_accessible => false,
413
414
  :request_headers => %w[accept accept-charset],
414
- :revision => "v2.5.1"
415
+ :revision => "v2.5.1",
416
+ :send_environment_metadata => false
415
417
  }
416
418
  end
417
419
  before do
@@ -428,6 +430,7 @@ describe Appsignal::Config do
428
430
  ENV["APPSIGNAL_INSTRUMENT_SEQUEL"] = "false"
429
431
  ENV["APPSIGNAL_FILES_WORLD_ACCESSIBLE"] = "false"
430
432
  ENV["APPSIGNAL_REQUEST_HEADERS"] = "accept,accept-charset"
433
+ ENV["APPSIGNAL_SEND_ENVIRONMENT_METADATA"] = "false"
431
434
  ENV["APP_REVISION"] = "v2.5.1"
432
435
  end
433
436
 
@@ -527,6 +530,7 @@ describe Appsignal::Config do
527
530
  config[:running_in_container] = false
528
531
  config[:dns_servers] = ["8.8.8.8", "8.8.4.4"]
529
532
  config[:transaction_debug_mode] = true
533
+ config[:send_environment_metadata] = false
530
534
  config[:revision] = "v2.5.1"
531
535
  config.write_to_environment
532
536
  end
@@ -555,6 +559,7 @@ describe Appsignal::Config do
555
559
  expect(ENV["_APPSIGNAL_DNS_SERVERS"]).to eq "8.8.8.8,8.8.4.4"
556
560
  expect(ENV["_APPSIGNAL_FILES_WORLD_ACCESSIBLE"]).to eq "true"
557
561
  expect(ENV["_APPSIGNAL_TRANSACTION_DEBUG_MODE"]).to eq "true"
562
+ expect(ENV["_APPSIGNAL_SEND_ENVIRONMENT_METADATA"]).to eq "false"
558
563
  expect(ENV["_APP_REVISION"]).to eq "v2.5.1"
559
564
  expect(ENV).to_not have_key("_APPSIGNAL_WORKING_DIR_PATH")
560
565
  expect(ENV).to_not have_key("_APPSIGNAL_WORKING_DIRECTORY_PATH")
@@ -0,0 +1,167 @@
1
+ describe Appsignal::Environment do
2
+ include EnvironmentMetadataHelper
3
+
4
+ before(:context) { start_agent }
5
+ before { capture_environment_metadata_report_calls }
6
+
7
+ def report(key, &value_block)
8
+ described_class.report(key, &value_block)
9
+ end
10
+
11
+ describe ".report" do
12
+ it "sends environment metadata to the extension" do
13
+ logs =
14
+ capture_logs do
15
+ report("_test_ruby_version") { "1.0.0" }
16
+ expect_environment_metadata("_test_ruby_version", "1.0.0")
17
+ end
18
+ expect(logs).to be_empty
19
+ end
20
+
21
+ context "when the key is a non String type" do
22
+ it "does not set the value" do
23
+ logs =
24
+ capture_logs do
25
+ report(:_test_symbol) { "1.0.0" }
26
+ expect_not_environment_metadata(:_test_symbol)
27
+ expect_not_environment_metadata("_test_symbol")
28
+ end
29
+ expect(logs).to contains_log(
30
+ :error,
31
+ "Unable to report on environment metadata: Unsupported value type for :_test_symbol"
32
+ )
33
+ end
34
+ end
35
+
36
+ context "when the key is nil" do
37
+ it "does not set the value" do
38
+ logs =
39
+ capture_logs do
40
+ report(nil) { "1" }
41
+ expect_not_environment_metadata(nil)
42
+ end
43
+ expect(logs).to contains_log(
44
+ :error,
45
+ "Unable to report on environment metadata: Unsupported value type for nil"
46
+ )
47
+ end
48
+ end
49
+
50
+ context "when the value is true or false" do
51
+ it "reports true or false as Strings" do
52
+ logs =
53
+ capture_logs do
54
+ report("_test_true") { true }
55
+ report("_test_false") { false }
56
+ expect_environment_metadata("_test_true", "true")
57
+ expect_environment_metadata("_test_false", "false")
58
+ end
59
+ expect(logs).to be_empty
60
+ end
61
+ end
62
+
63
+ context "when the value is nil" do
64
+ it "does not set the value" do
65
+ logs =
66
+ capture_logs do
67
+ report("_test_ruby_version") { nil }
68
+ expect_not_environment_metadata("_test_ruby_version")
69
+ end
70
+ expect(logs).to contains_log(
71
+ :error,
72
+ "Unable to report on environment metadata \"_test_ruby_version\": " \
73
+ "Unsupported value type for nil"
74
+ )
75
+ end
76
+ end
77
+
78
+ context "when the value block raises an error" do
79
+ it "does not re-raise the error and writes it to the log" do
80
+ logs =
81
+ capture_logs do
82
+ report("_test_error") { raise "uh oh" }
83
+ expect_not_environment_metadata("_test_error")
84
+ end
85
+ expect(logs).to contains_log(
86
+ :error,
87
+ "Unable to report on environment metadata \"_test_error\":\n" \
88
+ "RuntimeError: uh oh"
89
+ )
90
+ end
91
+ end
92
+
93
+ context "when something unforseen errors" do
94
+ it "does not re-raise the error and writes it to the log" do
95
+ klass = Class.new do
96
+ def inspect
97
+ raise "inspect error"
98
+ end
99
+ end
100
+
101
+ logs =
102
+ capture_logs do
103
+ report(klass.new) { raise "value error" }
104
+ expect(Appsignal::Extension).to_not have_received(:set_environment_metadata)
105
+ end
106
+ expect(logs).to contains_log(
107
+ :error,
108
+ "Unable to report on environment metadata:\n" \
109
+ "RuntimeError: inspect error"
110
+ )
111
+ end
112
+ end
113
+ end
114
+
115
+ describe ".report_supported_gems" do
116
+ it "reports about all AppSignal supported gems in the bundle" do
117
+ logs = capture_logs { described_class.report_supported_gems }
118
+
119
+ expect(logs).to be_empty
120
+
121
+ bundle_gem_specs = ::Bundler.rubygems.all_specs
122
+ rack_spec = bundle_gem_specs.find { |s| s.name == "rack" }
123
+ rake_spec = bundle_gem_specs.find { |s| s.name == "rake" }
124
+ expect_environment_metadata("ruby_rack_version", rack_spec.version.to_s)
125
+ expect_environment_metadata("ruby_rake_version", rake_spec.version.to_s)
126
+ expect(rack_spec.version.to_s).to_not be_empty
127
+ expect(rake_spec.version.to_s).to_not be_empty
128
+ end
129
+
130
+ context "when something unforseen errors" do
131
+ it "does not re-raise the error and writes it to the log" do
132
+ expect(Bundler).to receive(:rubygems).and_raise(RuntimeError, "bundler error")
133
+
134
+ logs = capture_logs { described_class.report_supported_gems }
135
+ expect(logs).to contains_log(
136
+ :error,
137
+ "Unable to report supported gems:\nRuntimeError: bundler error"
138
+ )
139
+ end
140
+ end
141
+ end
142
+
143
+ describe ".report_enabled" do
144
+ it "reports a feature being enabled" do
145
+ logs = capture_logs { described_class.report_enabled("a_test") }
146
+
147
+ expect(logs).to be_empty
148
+ expect_environment_metadata("ruby_a_test_enabled", "true")
149
+ end
150
+
151
+ context "when something unforseen errors" do
152
+ it "does not re-raise the error and writes it to the log" do
153
+ klass = Class.new do
154
+ def to_s
155
+ raise "to_s error"
156
+ end
157
+ end
158
+
159
+ logs = capture_logs { described_class.report_enabled(klass.new) }
160
+ expect(logs).to contains_log(
161
+ :error,
162
+ "Unable to report integration enabled:\nRuntimeError: to_s error"
163
+ )
164
+ end
165
+ end
166
+ end
167
+ end