class Object

Constants

CPU_COLUMN
MEM_COLUMN
PID_COLUMN

Public Instance Methods

asciiThreadLoad(running, spawned, total) click to toggle source
# File lib/helpers.rb, line 32
def asciiThreadLoad(running, spawned, total)
  full = "█"
  half= "░"
  empty = " "

  full_count = running
  half_count = [spawned - running, 0].max
  empty_count = total - half_count - full_count

  "#{running}[#{full*full_count}#{half*half_count}#{empty*empty_count}]#{total}"
end
color(critical, warn, value, str = nil) click to toggle source
# File lib/helpers.rb, line 20
def color(critical, warn, value, str = nil)
  str = value unless str
  color_level = if value >= critical
            :red
          elsif value < critical && value >= warn
            :yellow
          else
            :green
          end
  colorize(str, color_level)
end
colorize(str, color_name) click to toggle source
# File lib/helpers.rb, line 15
def colorize(str, color_name)
  return str if ENV.key?('NO_COLOR')
  str.to_s.colorize(color_name)
end
debug(str) click to toggle source
# File lib/helpers.rb, line 3
def debug(str)
  puts str if ENV.key?('DEBUG')
end
error(str) click to toggle source
# File lib/helpers.rb, line 11
def error(str)
  colorize(str, :red)
end
format_stats(stats) click to toggle source
# File lib/core.rb, line 78
def format_stats(stats)
  master_line = "#{stats.pid} (#{stats.state_file_path}) Uptime: #{seconds_to_human(stats.uptime)}"
  master_line += " | Phase: #{stats.phase}" if stats.phase

  if stats.booting?
    master_line += " #{warn("booting")}"
  else
    master_line += " | Load: #{color(75, 50, stats.load, asciiThreadLoad(stats.running_threads, stats.spawned_threads, stats.max_threads))}"
    master_line += " | Req: #{stats.requests_count}" if stats.requests_count
  end

  output = [master_line] + stats.workers.map do |wstats|
    worker_line = " └ #{wstats.pid.to_s.rjust(5, ' ')} CPU: #{color(75, 50, wstats.pcpu, wstats.pcpu.to_s.rjust(5, ' '))}% Mem: #{color(1000, 750, wstats.mem, wstats.mem.to_s.rjust(4, ' '))} MB Uptime: #{seconds_to_human(wstats.uptime)}"

    if wstats.booting?
      worker_line += " #{warn("booting")}"
    elsif wstats.killed?
      worker_line += " #{error("killed")}"
    else
      worker_line += " | Load: #{color(75, 50, wstats.load, asciiThreadLoad(wstats.running_threads, wstats.spawned_threads, wstats.max_threads))}"
      worker_line += " | Phase: #{error(wstats.phase)}" if wstats.phase != stats.phase
      worker_line += " | Req: #{wstats.requests_count}" if wstats.requests_count
      worker_line += " Queue: #{error(wstats.backlog.to_s)}" if wstats.backlog > 0
      worker_line += " Last checkin: #{error(wstats.last_checkin)}" if wstats.last_checkin >= 10
    end

    worker_line
  end

  output.join("\n")
end
get_memory_from_top(raw_memory) click to toggle source
# File lib/core.rb, line 35
def get_memory_from_top(raw_memory)
  raw_memory.tr!(',', '.') # because of LC_NUMERIC separator can be ,

  case raw_memory[-1].downcase
  when 'g'
    (raw_memory[0...-1].to_f*1024).to_i
  when 'm'
    raw_memory[0...-1].to_i
  else
    raw_memory.to_i/1024
  end
end
get_stats(state_file_path) click to toggle source
# File lib/core.rb, line 8
def get_stats(state_file_path)
  puma_state = YAML.load_file(state_file_path)

  uri = URI.parse(puma_state["control_url"])

  address = if uri.scheme =~ /unix/i
              [uri.scheme, '://', uri.host, uri.path].join
            else
              [uri.host, uri.path].join
            end

  client = NetX::HTTPUnix.new(address, uri.port)

  if uri.scheme =~ /ssl/i
    client.use_ssl = true
    client.verify_mode = OpenSSL::SSL::VERIFY_NONE if ENV['SSL_NO_VERIFY'] == '1'
  end

  req = Net::HTTP::Get.new("/stats?token=#{puma_state["control_auth_token"]}")
  resp = client.request(req)
  raw_stats = JSON.parse(resp.body)
  debug raw_stats
  stats = Stats.new(raw_stats)

  hydrate_stats(stats, puma_state, state_file_path)
end
get_top_stats(pids) click to toggle source
# File lib/core.rb, line 52
def get_top_stats(pids)
  pids.each_slice(19).inject({}) do |res, pids19|
    top_result = `top -b -n 1 -p #{pids19.join(',')} | tail -n #{pids19.length}`
    top_result.split("\n").map { |row| r = row.split(' '); [r[PID_COLUMN].to_i, get_memory_from_top(r[MEM_COLUMN]), r[CPU_COLUMN].to_f] }
      .inject(res) { |hash, row| hash[row[0]] = { mem: row[1], pcpu: row[2] }; hash }
    res
  end
end
hydrate_stats(stats, puma_state, state_file_path) click to toggle source
# File lib/core.rb, line 61
def hydrate_stats(stats, puma_state, state_file_path)
  stats.pid = puma_state['pid']
  stats.state_file_path = state_file_path

  workers_pids = stats.workers.map(&:pid)

  top_stats = get_top_stats(workers_pids)

  stats.tap do |s|
    stats.workers.map do |wstats|
      wstats.mem = top_stats.dig(wstats.pid, :mem) || 0
      wstats.pcpu = top_stats.dig(wstats.pid, :pcpu) || 0
      wstats.killed = !top_stats.key?(wstats.pid) || (wstats.mem <=0 && wstats.pcpu <= 0)
    end
  end
end
run() click to toggle source
# File lib/puma-status.rb, line 5
def run
  debug "puma-status"

  if ARGV.count < 1
    puts "Call with:"
    puts "\tpuma-status path/to/puma.state"
    exit -1
  end

  errors = []
  
  outputs = Parallel.map(ARGV, in_threads: ARGV.count) do |state_file_path|
    begin
      debug "State file: #{state_file_path}"
      format_stats(get_stats(state_file_path))
    rescue Errno::ENOENT => e
      if e.message =~ /#{state_file_path}/
        errors << "#{warn(state_file_path)} doesn't exists"
      elsif e.message =~ /connect\(2\) for [^\/]/
        errors << "#{warn("Relative Unix socket")}: the Unix socket of the control app has a relative path. Please, ensure you are running from the same folder has puma."
      else
        errors << "#{error(state_file_path)} an unhandled error occured: #{e.inspect}"
      end
      nil
    rescue Errno::EISDIR => e
      if e.message =~ /#{state_file_path}/
        errors << "#{warn(state_file_path)} isn't a state file"
      else
        errors << "#{error(state_file_path)} an unhandled error occured: #{e.inspect}"
      end
      nil
    rescue => e
      errors << "#{error(state_file_path)} an unhandled error occured: #{e.inspect}"
      nil
    end
  end

  outputs.compact.each { |output| puts output }

  if errors.any?
    puts ""
    errors.each { |error| puts error }
  end
end
seconds_to_human(seconds) click to toggle source
# File lib/helpers.rb, line 44
def seconds_to_human(seconds)

  #=>  0m 0s
  #=> 59m59s
  #=>  1h 0m
  #=> 23h59m
  #=>  1d 0h
  #=>    24d

  if seconds <= 0
    "--m--s"
  elsif seconds < 60*60
    "#{(seconds/60).to_s.rjust(2, ' ')}m#{(seconds%60).to_s.rjust(2, ' ')}s"
  elsif seconds >= 60*60*1 && seconds < 60*60*24
    "#{(seconds/(60*60*1)).to_s.rjust(2, ' ')}h#{((seconds%(60*60*1))/60).to_s.rjust(2, ' ')}m"
  elsif seconds > 60*60*24 && seconds < 60*60*24*10
    "#{(seconds/(60*60*24)).to_s.rjust(2, ' ')}d#{((seconds%(60*60*24))/(60*60*1)).to_s.rjust(2, ' ')}h"
  else
    "#{seconds/(60*60*24)}d".rjust(6, ' ')
  end
end
warn(str) click to toggle source
# File lib/helpers.rb, line 7
def warn(str)
  colorize(str, :yellow)
end