Skip to content

Commit 59195bf

Browse files
Add Periodic Stats
1 parent befb107 commit 59195bf

File tree

9 files changed

+286
-1
lines changed

9 files changed

+286
-1
lines changed

Diff for: Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ source "https://rubygems.org"
33
# Specify your gem's dependencies in promenade.gemspec
44
gemspec
55

6+
gem "codecov"
7+
gem "raindrops"
68
gem "webrick"

Diff for: Gemfile.lock

+7-1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ GEM
9191
builder (3.2.4)
9292
byebug (11.1.3)
9393
climate_control (1.2.0)
94+
codecov (0.6.0)
95+
simplecov (>= 0.15, < 0.22)
9496
concurrent-ruby (1.2.3)
9597
connection_pool (2.4.1)
9698
crass (1.0.6)
@@ -193,6 +195,7 @@ GEM
193195
thor (~> 1.0, >= 1.2.2)
194196
zeitwerk (~> 2.6)
195197
rainbow (3.1.1)
198+
raindrops (0.20.1)
196199
rake (13.1.0)
197200
rb_sys (0.9.87)
198201
rdoc (6.6.2)
@@ -275,6 +278,7 @@ GEM
275278
zeitwerk (2.6.13)
276279

277280
PLATFORMS
281+
arm64-darwin-21
278282
arm64-darwin-22
279283
arm64-linux
280284
x86_64-darwin-22
@@ -284,8 +288,10 @@ DEPENDENCIES
284288
bundler (~> 2.0)
285289
byebug
286290
climate_control
291+
codecov
287292
promenade!
288293
rails (> 3.0, < 8.0)
294+
raindrops
289295
rake
290296
rspec (~> 3.11)
291297
rspec-rails (~> 5.1)
@@ -298,4 +304,4 @@ DEPENDENCIES
298304
webrick
299305

300306
BUNDLED WITH
301-
2.4.17
307+
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)