Skip to content

Commit cb03437

Browse files
Add Periodic Stats
1 parent 5e70601 commit cb03437

File tree

9 files changed

+282
-1
lines changed

9 files changed

+282
-1
lines changed

Diff for: Gemfile

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ source "https://rubygems.org"
44
gemspec
55

66
gem "codecov"
7+
gem "raindrops"
78
gem "webrick"

Diff for: Gemfile.lock

+4-1
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ GEM
195195
thor (~> 1.0, >= 1.2.2)
196196
zeitwerk (~> 2.6)
197197
rainbow (3.1.1)
198+
raindrops (0.20.1)
198199
rake (13.1.0)
199200
rb_sys (0.9.87)
200201
rdoc (6.6.2)
@@ -274,6 +275,7 @@ GEM
274275
zeitwerk (2.6.13)
275276

276277
PLATFORMS
278+
arm64-darwin-21
277279
arm64-darwin-22
278280
arm64-linux
279281
x86_64-darwin-22
@@ -286,6 +288,7 @@ DEPENDENCIES
286288
codecov
287289
promenade!
288290
rails (> 3.0, < 8.0)
291+
raindrops
289292
rake
290293
rspec (~> 3.11)
291294
rspec-rails (~> 5.1)
@@ -297,4 +300,4 @@ DEPENDENCIES
297300
webrick
298301

299302
BUNDLED WITH
300-
2.4.17
303+
2.3.16

Diff for: lib/promenade.rb

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
require "promenade/setup"
33
require "promenade/configuration"
44
require "promenade/prometheus"
5+
require "promenade/periodic_stats"
56

67
module Promenade
78
class << self

Diff for: lib/promenade/periodic_stats.rb

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
require 'singleton'
2+
3+
module Promenade
4+
class PeriodicStats
5+
include Singleton
6+
7+
def initialize
8+
@thread_stopped = true
9+
@thread = nil
10+
end
11+
12+
def self.configure(frequency:, logger: nil, &block)
13+
instance.configure(frequency: frequency, logger: logger, &block)
14+
end
15+
16+
def self.start
17+
instance.start
18+
end
19+
20+
def self.stop
21+
instance.stop
22+
end
23+
24+
def configure(frequency:, logger: nil, &block)
25+
@frequency = frequency
26+
@block = block
27+
@logger = logger
28+
end
29+
30+
def start
31+
stop
32+
33+
@thread_stopped = false
34+
@thread = Thread.new do
35+
while active? do
36+
block.call
37+
sleep(frequency) # Ensure the sleep is inside the loop
38+
end
39+
end
40+
rescue StandardError => e
41+
logger&.error("Promenade: Error in periodic stats: #{e.message}")
42+
end
43+
44+
def stop
45+
return unless thread
46+
47+
if started?
48+
@thread_stopped = true
49+
thread.kill
50+
thread.join
51+
end
52+
53+
@thread = nil
54+
end
55+
56+
private
57+
58+
attr_reader :logger, :frequency, :block, :thread, :thread_stopped
59+
60+
def started?
61+
thread&.alive?
62+
end
63+
64+
def active?
65+
!thread_stopped
66+
end
67+
end
68+
end

Diff for: lib/promenade/pitchfork/stats.rb

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
require "promenade/raindrops/stats"
2+
3+
module Promenade
4+
module Pitchfork
5+
class Stats
6+
Promenade.gauge :pitchfork_workers_count do
7+
doc "Number of workers configured"
8+
end
9+
10+
Promenade.gauge :pitchfork_live_workers_count do
11+
doc "Number of live / booted workers"
12+
end
13+
14+
Promenade.gauge :pitchfork_capacity do
15+
doc "Number of workers that are currently idle"
16+
end
17+
18+
Promenade.gauge :pitchfork_busy_percent do
19+
doc "Percentage of workers that are currently busy"
20+
end
21+
22+
def initialize
23+
return unless defined?(::Pitchfork) && defined?(::Pitchfork::Info)
24+
25+
@workers_count = ::Pitchfork::Info.workers_count
26+
@live_workers_count = ::Pitchfork::Info.live_workers_count
27+
28+
raindrops_stats = Raindrops::Stats.new
29+
30+
@active_workers = raindrops_stats.active_workers
31+
@queued_requests = raindrops_stats.queued_requests
32+
end
33+
34+
def instrument
35+
Promenade.metric(:pitchfork_workers_count).set({}, workers_count)
36+
Promenade.metric(:pitchfork_live_workers_count).set({}, live_workers_count)
37+
Promenade.metric(:pitchfork_capacity).set({}, capacity)
38+
Promenade.metric(:pitchfork_busy_percent).set({}, busy_percent)
39+
end
40+
41+
def self.instrument
42+
new.instrument
43+
end
44+
45+
private
46+
47+
attr_reader :workers_count, :live_workers_count, :active_workers, :queued_requests
48+
49+
def capacity
50+
return 0 if live_workers_count.nil? || active_workers.nil?
51+
return 0 if live_workers_count&.zero?
52+
return 0 if active_workers&.zero?
53+
54+
live_workers_count - active_workers
55+
end
56+
57+
def busy_percent
58+
return 100 if live_workers_count.zero?
59+
return 100 if active_workers&.zero?
60+
61+
(active_workers.to_f / live_workers_count) * 100
62+
end
63+
end
64+
end
65+
end

Diff for: lib/promenade/raindrops/stats.rb

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
begin
2+
require "raindrops"
3+
rescue LoadError
4+
# No raindrops available, dont do anything
5+
end
6+
7+
module Promenade
8+
module Raindrops
9+
Promenade.gauge :rack_active_workers do
10+
doc "Number of active workers in the Application Server"
11+
end
12+
13+
Promenade.gauge :rack_queued_requests do
14+
doc "Number of requests waiting to be processed by the Application Server"
15+
end
16+
17+
class Stats
18+
19+
attr_reader :active_workers, :queued_requests
20+
21+
def initialize
22+
return unless defined?(::Raindrops)
23+
return unless defined?(::Raindrops::Linux.tcp_listener_stats)
24+
25+
listener_address = "0.0.0.0:#{ENV.fetch('PORT', 3000)}"
26+
stats = ::Raindrops::Linux.tcp_listener_stats([listener_address])[listener_address]
27+
28+
@active_workers = stats.active
29+
@queued_requests = stats.queued
30+
end
31+
32+
def instrument
33+
Promenade.metric(:rack_active_workers).set({}, active_workers)
34+
Promenade.metric(:rack_queued_requests).set({}, queued_requests)
35+
end
36+
37+
def self.instrument
38+
new.instrument
39+
end
40+
end
41+
end
42+
end

Diff for: spec/promenade/periodic_stats_spec.rb

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
require "spec_helper"
2+
3+
RSpec.describe Promenade::PeriodicStats do
4+
describe "#start" do
5+
it "executes the block at the specified frequency" do
6+
counter = 0
7+
Promenade::PeriodicStats.configure(frequency: 0.1) { counter += 1 }
8+
Promenade::PeriodicStats.start
9+
10+
sleep(0.2)
11+
Promenade::PeriodicStats.stop
12+
13+
expect(counter).to be > 1
14+
end
15+
end
16+
end

Diff for: spec/promenade/pitchfork/stats_spec.rb

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
require "spec_helper"
2+
require "promenade/pitchfork/stats"
3+
4+
RSpec.describe Promenade::Pitchfork::Stats do
5+
let(:pitchfork_info) { class_double("Pitchfork::Info") }
6+
let(:raindrops_stats) { instance_double("Promenade::Raindrops::Stats", active_workers: 6, queued_requests: 2) }
7+
8+
before do
9+
stub_const("Pitchfork::Info", pitchfork_info)
10+
allow(pitchfork_info).to receive(:workers_count).and_return(10)
11+
allow(pitchfork_info).to receive(:live_workers_count).and_return(8)
12+
13+
allow(Promenade::Raindrops::Stats).to receive(:new).and_return(raindrops_stats)
14+
end
15+
16+
describe "#instrument" do
17+
let(:metric) { instance_double("Promenade::Metric") }
18+
19+
before do
20+
allow(Promenade).to receive(:metric).and_return(metric)
21+
allow(metric).to receive(:set)
22+
end
23+
24+
it "sets the metrics correctly" do
25+
stats = Promenade::Pitchfork::Stats.new
26+
27+
expect(Promenade).to receive(:metric).with(:pitchfork_workers_count).and_return(metric)
28+
expect(Promenade).to receive(:metric).with(:pitchfork_live_workers_count).and_return(metric)
29+
expect(Promenade).to receive(:metric).with(:pitchfork_capacity).and_return(metric)
30+
expect(Promenade).to receive(:metric).with(:pitchfork_busy_percent).and_return(metric)
31+
32+
expect(metric).to receive(:set).with({}, 10)
33+
expect(metric).to receive(:set).with({}, 8)
34+
expect(metric).to receive(:set).with({}, 2)
35+
expect(metric).to receive(:set).with({}, 75.0)
36+
37+
stats.instrument
38+
end
39+
end
40+
41+
describe ".instrument" do
42+
it "calls the instance method instrument" do
43+
stats_instance = instance_double("Promenade::Pitchfork::Stats")
44+
allow(Promenade::Pitchfork::Stats).to receive(:new).and_return(stats_instance)
45+
allow(stats_instance).to receive(:instrument)
46+
47+
Promenade::Pitchfork::Stats.instrument
48+
49+
expect(stats_instance).to have_received(:instrument)
50+
end
51+
end
52+
end
53+

Diff for: spec/promenade/raindrops/stats_spec.rb

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
require "spec_helper"
2+
require "promenade/raindrops/stats"
3+
4+
RSpec.describe Promenade::Raindrops::Stats do
5+
let(:listen_stats) { instance_double("Raindrops::Linux::ListenStats", active: 1, queued: 1) }
6+
let(:listener_address) { "0.0.0.0:#{ENV.fetch('PORT', 3000)}" }
7+
8+
before do
9+
allow(Raindrops::Linux).to receive(:tcp_listener_stats).and_return({ listener_address => listen_stats })
10+
end
11+
12+
describe "#instrument" do
13+
let(:metric) { instance_double("Promenade::Metric") }
14+
15+
before do
16+
allow(Promenade).to receive(:metric).and_return(metric)
17+
allow(metric).to receive(:set)
18+
end
19+
20+
it "sets the metrics correctly" do
21+
stats = Promenade::Raindrops::Stats.new
22+
23+
expect(Promenade).to receive(:metric).with(:rack_active_workers).and_return(metric)
24+
expect(Promenade).to receive(:metric).with(:rack_queued_requests).and_return(metric)
25+
26+
expect(metric).to receive(:set).with({}, 1)
27+
expect(metric).to receive(:set).with({}, 1)
28+
29+
stats.instrument
30+
end
31+
end
32+
end

0 commit comments

Comments
 (0)