Skip to content

Commit

Permalink
Add Periodic Stats
Browse files Browse the repository at this point in the history
Add listener_address as Instance attributes of Promenade::Raindrops::Stats

Fix guard clauses

πŸ’‡β€β™€οΈ

Update dev dependencies

Update Bundler version
  • Loading branch information
robertomiranda authored and errm committed Aug 16, 2024
1 parent 328463b commit d2db529
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ GEM
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
raindrops (0.20.1)
rake (13.1.0)
rb_sys (0.9.87)
rdoc (6.6.3.1)
Expand Down Expand Up @@ -271,6 +272,7 @@ GEM
zeitwerk (2.6.13)

PLATFORMS
arm64-darwin-21
arm64-darwin-22
x86_64-darwin-22
x86_64-linux
Expand All @@ -281,6 +283,7 @@ DEPENDENCIES
climate_control
promenade!
rails (> 3.0, < 8.0)
raindrops
rake
rspec (~> 3.11)
rspec-rails (~> 5.1)
Expand Down
1 change: 1 addition & 0 deletions lib/promenade.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require "promenade/setup"
require "promenade/configuration"
require "promenade/prometheus"
require "promenade/periodic_stats"

module Promenade
class << self
Expand Down
68 changes: 68 additions & 0 deletions lib/promenade/periodic_stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require "singleton"

module Promenade
class PeriodicStats
include Singleton

def initialize
@thread_stopped = true
@thread = nil
end

def self.configure(frequency:, logger: nil, &block)
instance.configure(frequency: frequency, logger: logger, &block)
end

def self.start
instance.start
end

def self.stop
instance.stop
end

def configure(frequency:, logger: nil, &block)
@frequency = frequency
@block = block
@logger = logger
end

def start
stop

@thread_stopped = false
@thread = Thread.new do
while active?
block.call
sleep(frequency) # Ensure the sleep is inside the loop
end
end
rescue StandardError => e
logger&.error("Promenade: Error in periodic stats: #{e.message}")

Check warning on line 41 in lib/promenade/periodic_stats.rb

View check run for this annotation

Codecov / codecov/patch

lib/promenade/periodic_stats.rb#L41

Added line #L41 was not covered by tests
end

def stop
return unless thread

if started?
@thread_stopped = true
thread.kill
thread.join
end

@thread = nil
end

private

attr_reader :logger, :frequency, :block, :thread, :thread_stopped

def started?
thread&.alive?
end

def active?
!thread_stopped
end
end
end
62 changes: 62 additions & 0 deletions lib/promenade/pitchfork/stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require "promenade/raindrops/stats"

module Promenade
module Pitchfork
class Stats
Promenade.gauge :pitchfork_workers_count do
doc "Number of workers configured"
end

Promenade.gauge :pitchfork_live_workers_count do
doc "Number of live / booted workers"
end

Promenade.gauge :pitchfork_capacity do
doc "Number of workers that are currently idle"
end

Promenade.gauge :pitchfork_busy_percent do
doc "Percentage of workers that are currently busy"
end

def initialize
return unless defined?(::Pitchfork) && defined?(::Pitchfork::Info)

@workers_count = ::Pitchfork::Info.workers_count
@live_workers_count = ::Pitchfork::Info.live_workers_count

raindrops_stats = Raindrops::Stats.new

@active_workers = raindrops_stats.active_workers || 0
@queued_requests = raindrops_stats.queued_requests || 0
end

def instrument
Promenade.metric(:pitchfork_workers_count).set({}, workers_count)
Promenade.metric(:pitchfork_live_workers_count).set({}, live_workers_count)
Promenade.metric(:pitchfork_capacity).set({}, capacity)
Promenade.metric(:pitchfork_busy_percent).set({}, busy_percent)
end

def self.instrument
new.instrument
end

private

attr_reader :workers_count, :live_workers_count, :active_workers, :queued_requests

def capacity
return 0 if live_workers_count.nil? || live_workers_count == 0

live_workers_count - active_workers
end

def busy_percent
return 0 if live_workers_count == 0

(active_workers.to_f / live_workers_count) * 100
end
end
end
end
42 changes: 42 additions & 0 deletions lib/promenade/raindrops/stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
begin
require "raindrops"
rescue LoadError
# No raindrops available, dont do anything
end

module Promenade
module Raindrops
Promenade.gauge :rack_active_workers do
doc "Number of active workers in the Application Server"
end

Promenade.gauge :rack_queued_requests do
doc "Number of requests waiting to be processed by the Application Server"
end

class Stats
attr_reader :active_workers, :queued_requests, :listener_address

def initialize(listener_address: nil)
return unless defined?(::Raindrops)
return unless defined?(::Raindrops::Linux.tcp_listener_stats)

@listener_address = listener_address || "127.0.0.1:#{ENV.fetch('PORT', 3000)}"

stats = ::Raindrops::Linux.tcp_listener_stats([@listener_address])[@listener_address]

@active_workers = stats.active
@queued_requests = stats.queued
end

def instrument
Promenade.metric(:rack_active_workers).set({}, active_workers)
Promenade.metric(:rack_queued_requests).set({}, queued_requests)
end

def self.instrument(listener_address: nil)
new(listener_address: listener_address).instrument

Check warning on line 38 in lib/promenade/raindrops/stats.rb

View check run for this annotation

Codecov / codecov/patch

lib/promenade/raindrops/stats.rb#L38

Added line #L38 was not covered by tests
end
end
end
end
1 change: 1 addition & 0 deletions promenade.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "byebug"
spec.add_development_dependency "climate_control"
spec.add_development_dependency "rails", "> 3.0", "< 8.0"
spec.add_development_dependency "raindrops"
spec.add_development_dependency "rake"
spec.add_development_dependency "rspec", "~> 3.11"
spec.add_development_dependency "rspec-rails", "~> 5.1"
Expand Down
16 changes: 16 additions & 0 deletions spec/promenade/periodic_stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require "spec_helper"

RSpec.describe Promenade::PeriodicStats do
describe "#start" do
it "executes the block at the specified frequency" do
counter = 0
Promenade::PeriodicStats.configure(frequency: 0.1) { counter += 1 }
Promenade::PeriodicStats.start

sleep(0.2)
Promenade::PeriodicStats.stop

expect(counter).to be > 1
end
end
end
52 changes: 52 additions & 0 deletions spec/promenade/pitchfork/stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "spec_helper"
require "promenade/pitchfork/stats"

RSpec.describe Promenade::Pitchfork::Stats do
let(:pitchfork_info) { class_double("Pitchfork::Info") }
let(:raindrops_stats) { instance_double("Promenade::Raindrops::Stats", active_workers: 6, queued_requests: 2) }

before do
stub_const("Pitchfork::Info", pitchfork_info)
allow(pitchfork_info).to receive(:workers_count).and_return(10)
allow(pitchfork_info).to receive(:live_workers_count).and_return(8)

allow(Promenade::Raindrops::Stats).to receive(:new).and_return(raindrops_stats)
end

describe "#instrument" do
let(:metric) { instance_double("Promenade::Metric") }

before do
allow(Promenade).to receive(:metric).and_return(metric)
allow(metric).to receive(:set)
end

it "sets the metrics correctly" do
stats = Promenade::Pitchfork::Stats.new

expect(Promenade).to receive(:metric).with(:pitchfork_workers_count).and_return(metric)
expect(Promenade).to receive(:metric).with(:pitchfork_live_workers_count).and_return(metric)
expect(Promenade).to receive(:metric).with(:pitchfork_capacity).and_return(metric)
expect(Promenade).to receive(:metric).with(:pitchfork_busy_percent).and_return(metric)

expect(metric).to receive(:set).with({}, 10)
expect(metric).to receive(:set).with({}, 8)
expect(metric).to receive(:set).with({}, 2)
expect(metric).to receive(:set).with({}, 75.0)

stats.instrument
end
end

describe ".instrument" do
it "calls the instance method instrument" do
stats_instance = instance_double("Promenade::Pitchfork::Stats")
allow(Promenade::Pitchfork::Stats).to receive(:new).and_return(stats_instance)
allow(stats_instance).to receive(:instrument)

Promenade::Pitchfork::Stats.instrument

expect(stats_instance).to have_received(:instrument)
end
end
end
32 changes: 32 additions & 0 deletions spec/promenade/raindrops/stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require "spec_helper"
require "promenade/raindrops/stats"

RSpec.describe Promenade::Raindrops::Stats do
let(:listen_stats) { instance_double("Raindrops::Linux::ListenStats", active: 1, queued: 1) }
let(:listener_address) { "127.0.0.1:#{ENV.fetch('PORT', 3000)}" }

before do
allow(Raindrops::Linux).to receive(:tcp_listener_stats).and_return({ listener_address => listen_stats })
end

describe "#instrument" do
let(:metric) { instance_double("Promenade::Metric") }

before do
allow(Promenade).to receive(:metric).and_return(metric)
allow(metric).to receive(:set)
end

it "sets the metrics correctly" do
stats = Promenade::Raindrops::Stats.new

expect(Promenade).to receive(:metric).with(:rack_active_workers).and_return(metric)
expect(Promenade).to receive(:metric).with(:rack_queued_requests).and_return(metric)

expect(metric).to receive(:set).with({}, 1)
expect(metric).to receive(:set).with({}, 1)

stats.instrument
end
end
end

0 comments on commit d2db529

Please sign in to comment.