appsignal 2.11.0.alpha.1 → 2.11.0.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -12,7 +12,8 @@ module Appsignal
12
12
  end
13
13
 
14
14
  def install
15
- Appsignal::Minutely.probes.register :sidekiq, SidekiqProbe
15
+ require "appsignal/probes/sidekiq"
16
+ Appsignal::Minutely.probes.register :sidekiq, Appsignal::Probes::SidekiqProbe
16
17
 
17
18
  ::Sidekiq.configure_server do |config|
18
19
  config.server_middleware do |chain|
@@ -22,104 +23,6 @@ module Appsignal
22
23
  end
23
24
  end
24
25
 
25
- class SidekiqProbe
26
- attr_reader :config
27
-
28
- def self.dependencies_present?
29
- Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("3.3.5")
30
- end
31
-
32
- def initialize(config = {})
33
- @config = config
34
- @cache = {}
35
- config_string = " with config: #{config}" unless config.empty?
36
- Appsignal.logger.debug("Initializing Sidekiq probe#{config_string}")
37
- require "sidekiq/api"
38
- end
39
-
40
- def call
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)
52
- redis_info = ::Sidekiq.redis_info
53
-
54
- gauge "connection_count", redis_info.fetch("connected_clients")
55
- gauge "memory_usage", redis_info.fetch("used_memory")
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
64
- gauge_delta :jobs_processed, "job_count", stats.processed,
65
- :status => :processed
66
- gauge_delta :jobs_failed, "job_count", stats.failed, :status => :failed
67
- gauge "job_count", stats.retry_size, :status => :retry_queue
68
- gauge_delta :jobs_dead, "job_count", stats.dead_size, :status => :died
69
- gauge "job_count", stats.scheduled_size, :status => :scheduled
70
- gauge "job_count", stats.enqueued, :status => :enqueued
71
- end
72
-
73
- def track_queues
74
- ::Sidekiq::Queue.all.each do |queue|
75
- gauge "queue_length", queue.size, :queue => queue.name
76
- # Convert latency from seconds to milliseconds
77
- gauge "queue_latency", queue.latency * 1_000.0, :queue => queue.name
78
- end
79
- end
80
-
81
- # Track a gauge metric with the `sidekiq_` prefix
82
- def gauge(key, value, tags = {})
83
- tags[:hostname] = hostname if hostname
84
- Appsignal.set_gauge "sidekiq_#{key}", value, tags
85
- end
86
-
87
- # Track the delta of two values for a gauge metric
88
- #
89
- # First call will store the data for the metric and the second call will
90
- # set a gauge metric with the difference. This is used for absolute
91
- # counter values which we want to track as gauges.
92
- #
93
- # @example
94
- # gauge_delta :my_cache_key, "my_gauge", 10
95
- # gauge_delta :my_cache_key, "my_gauge", 15
96
- # # Creates a gauge with the value `5`
97
- # @see #gauge
98
- def gauge_delta(cache_key, key, value, tags = {})
99
- previous_value = cache[cache_key]
100
- cache[cache_key] = value
101
- return unless previous_value
102
- new_value = value - previous_value
103
- gauge key, new_value, tags
104
- end
105
-
106
- def hostname
107
- return @hostname if defined?(@hostname)
108
- if config.key?(:hostname)
109
- @hostname = config[:hostname]
110
- Appsignal.logger.debug "Sidekiq probe: Using hostname config " \
111
- "option #{@hostname.inspect} as hostname"
112
- return @hostname
113
- end
114
-
115
- host = nil
116
- ::Sidekiq.redis { |c| host = c.connection[:host] }
117
- Appsignal.logger.debug "Sidekiq probe: Using Redis server hostname " \
118
- "#{host.inspect} as hostname"
119
- @hostname = host
120
- end
121
- end
122
-
123
26
  # @api private
124
27
  class SidekiqPlugin # rubocop:disable Metrics/ClassLength
125
28
  include Appsignal::Hooks::Helpers
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.11.0.alpha.1".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
@@ -55,7 +55,7 @@ describe Appsignal::Hooks::PumaHook do
55
55
 
56
56
  Appsignal::Hooks::PumaHook.new.install
57
57
  probe = Appsignal::Minutely.probes[:puma]
58
- expect(probe).to eql(Appsignal::Hooks::PumaProbe)
58
+ expect(probe).to eql(Appsignal::Probes::PumaProbe)
59
59
  end
60
60
  end
61
61
  end
@@ -101,7 +101,7 @@ describe Appsignal::Hooks::PumaHook do
101
101
 
102
102
  Appsignal::Hooks::PumaHook.new.install
103
103
  probe = Appsignal::Minutely.probes[:puma]
104
- expect(probe).to eql(Appsignal::Hooks::PumaProbe)
104
+ expect(probe).to eql(Appsignal::Probes::PumaProbe)
105
105
  end
106
106
  end
107
107
  end
@@ -116,182 +116,3 @@ describe Appsignal::Hooks::PumaHook do
116
116
  end
117
117
  end
118
118
  end
119
-
120
- describe Appsignal::Hooks::PumaProbe do
121
- before(:context) do
122
- Appsignal.config = project_fixture_config
123
- end
124
- after(:context) do
125
- Appsignal.config = nil
126
- end
127
-
128
- let(:probe) { Appsignal::Hooks::PumaProbe.new }
129
-
130
- describe "hostname" do
131
- it "returns the socket hostname" do
132
- expect(probe.send(:hostname)).to eql(Socket.gethostname)
133
- end
134
-
135
- context "with overridden hostname" do
136
- around do |sample|
137
- Appsignal.config[:hostname] = "frontend1"
138
- sample.run
139
- Appsignal.config[:hostname] = nil
140
- end
141
- it "returns the configured host" do
142
- expect(probe.send(:hostname)).to eql("frontend1")
143
- end
144
- end
145
- end
146
-
147
- describe "#call" do
148
- let(:expected_default_tags) { { :hostname => Socket.gethostname } }
149
-
150
- context "with multiple worker stats" do
151
- before(:context) do
152
- class Puma
153
- def self.stats
154
- {
155
- "workers" => 2,
156
- "booted_workers" => 2,
157
- "old_workers" => 0,
158
- "worker_status" => [
159
- {
160
- "last_status" => {
161
- "backlog" => 0,
162
- "running" => 5,
163
- "pool_capacity" => 5,
164
- "max_threads" => 5
165
- }
166
- },
167
- {
168
- "last_status" => {
169
- "backlog" => 0,
170
- "running" => 5,
171
- "pool_capacity" => 5,
172
- "max_threads" => 5
173
- }
174
- }
175
- ]
176
- }.to_json
177
- end
178
- end
179
- end
180
- after(:context) { Object.send(:remove_const, :Puma) }
181
-
182
- it "calls `puma_gauge` with the (summed) worker metrics" do
183
- expect_gauge(:workers, 2, :type => :count)
184
- expect_gauge(:workers, 2, :type => :booted)
185
- expect_gauge(:workers, 0, :type => :old)
186
-
187
- expect_gauge(:connection_backlog, 0)
188
- expect_gauge(:pool_capacity, 10)
189
- expect_gauge(:threads, 10, :type => :running)
190
- expect_gauge(:threads, 10, :type => :max)
191
-
192
- probe.call
193
- end
194
- end
195
-
196
- context "with single worker stats" do
197
- before(:context) do
198
- class Puma
199
- def self.stats
200
- {
201
- "backlog" => 0,
202
- "running" => 5,
203
- "pool_capacity" => 5,
204
- "max_threads" => 5
205
- }.to_json
206
- end
207
- end
208
- end
209
- after(:context) { Object.send(:remove_const, :Puma) }
210
-
211
- it "calls `puma_gauge` with the (summed) worker metrics" do
212
- expect_gauge(:connection_backlog, 0)
213
- expect_gauge(:pool_capacity, 5)
214
- expect_gauge(:threads, 5, :type => :running)
215
- expect_gauge(:threads, 5, :type => :max)
216
- probe.call
217
- end
218
- end
219
-
220
- context "without stats" do
221
- before(:context) do
222
- class Puma
223
- def self.stats
224
- end
225
- end
226
- end
227
- after(:context) { Object.send(:remove_const, :Puma) }
228
-
229
- context "when it returns nil" do
230
- it "does not track metrics" do
231
- expect(probe).to_not receive(:puma_gauge)
232
- probe.call
233
- end
234
- end
235
-
236
- # Puma.stats raises a NoMethodError on a nil object on the first call.
237
- context "when it returns a NoMethodError on the first call" do
238
- let(:log) { StringIO.new }
239
-
240
- it "ignores the first call and tracks the second call" do
241
- use_logger_with log do
242
- expect(Puma).to receive(:stats)
243
- .and_raise(NoMethodError.new("undefined method `stats' for nil:NilClass"))
244
- probe.call
245
-
246
- expect(Puma).to receive(:stats).and_return({
247
- "backlog" => 1,
248
- "running" => 5,
249
- "pool_capacity" => 4,
250
- "max_threads" => 6
251
- }.to_json)
252
-
253
- expect_gauge(:connection_backlog, 1)
254
- expect_gauge(:pool_capacity, 4)
255
- expect_gauge(:threads, 5, :type => :running)
256
- expect_gauge(:threads, 6, :type => :max)
257
- probe.call
258
- end
259
-
260
- expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
261
- end
262
- end
263
-
264
- context "when it does not have a complete stats payload" do
265
- let(:log) { StringIO.new }
266
-
267
- it "tracks whatever metrics we do have" do
268
- use_logger_with log do
269
- expect(Puma).to receive(:stats).and_return({
270
- "backlog" => 1,
271
- "running" => 5
272
- }.to_json)
273
-
274
- expect_gauge(:connection_backlog, 1)
275
- expect_no_gauge(:pool_capacity)
276
- expect_gauge(:threads, 5, :type => :running)
277
- expect_no_gauge(:threads, :type => :max)
278
- probe.call
279
- end
280
-
281
- expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
282
- end
283
- end
284
- end
285
-
286
- def expect_gauge(key, value, tags = {})
287
- expect(Appsignal).to receive(:set_gauge)
288
- .with("puma_#{key}", value, expected_default_tags.merge(tags))
289
- .and_call_original
290
- end
291
-
292
- def expect_no_gauge(key, tags = {})
293
- expect(Appsignal).to_not receive(:set_gauge)
294
- .with("puma_#{key}", anything, tags)
295
- end
296
- end
297
- end