Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Promenade::PeriodicStats for Periodical Stats Instrumentation #63

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
70 changes: 70 additions & 0 deletions lib/promenade/periodic_stats.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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?
begin
block.call
rescue StandardError => e
logger&.error("Promenade: Error in periodic stats: #{e.message}")
end
sleep(frequency) # Ensure the sleep is inside the loop
end
end
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
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
27 changes: 27 additions & 0 deletions spec/promenade/periodic_stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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

it "swalows any errors, and logs them" do
logger = double(:logger, error: nil)
expect(logger).to receive(:error).with("Promenade: Error in periodic stats: Intentionally Broken")

Promenade::PeriodicStats.configure(frequency: 0.1, logger: logger) { fail "Intentionally Broken" }
Promenade::PeriodicStats.start

sleep(0.2)
Promenade::PeriodicStats.stop
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
30 changes: 30 additions & 0 deletions spec/promenade/raindrops/stats_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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
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)

described_class.instrument
end
end
end