Top Level Namespace

Defined Under Namespace

Classes: Stats

Constant Summary collapse

PID_COLUMN =
0
MEM_COLUMN =
5
CPU_COLUMN =
8
OPEN3_STDOUT =
1

Instance Method Summary collapse

Instance Method Details

#asciiThreadLoad(running, spawned, total) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
# File 'lib/helpers.rb', line 39

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) ⇒ Object



27
28
29
30
31
32
33
34
35
36
37
# File 'lib/helpers.rb', line 27

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) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/helpers.rb', line 13

def colorize(str, color_name)
  return str if ENV.key?('NO_COLOR')
  case color_name
  when :red
    "\e[0;31;49m#{str}\e[0m"
  when :yellow
    "\e[0;33;49m#{str}\e[0m"
  when :green
    "\e[0;32;49m#{str}\e[0m"
  else
    str
  end
end

#debug(str) ⇒ Object



1
2
3
# File 'lib/helpers.rb', line 1

def debug(str)
  puts str if ENV.key?('DEBUG')
end

#format_stats(stats) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/core.rb', line 78

def format_stats(stats)
  master_line = "#{stats.pid} (#{stats.state_file_path})"
  master_line += " Version: #{stats.version} |" if stats.version
  master_line += " Uptime: #{seconds_to_human(stats.uptime)}"
  master_line += " | Phase: #{stats.phase}" if stats.phase

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

    worker_line
  end

  output.join("\n")
end

#get_memory_from_top(raw_memory) ⇒ Object



36
37
38
39
40
41
42
43
44
45
# File 'lib/core.rb', line 36

def get_memory_from_top(raw_memory)
  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) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/core.rb', line 9

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) ⇒ Object



52
53
54
55
56
57
58
59
# File 'lib/core.rb', line 52

def get_top_stats(pids)
  pids.each_slice(19).inject({}) do |res, pids19|
    top_result = Open3.popen3({ 'LC_ALL' => 'C' }, "top -b -n 1 -p #{pids19.map(&:to_i).join(',')}")[OPEN3_STDOUT].read
    top_result.split("\n").last(pids19.length).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) ⇒ Object



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# 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

#red(str) ⇒ Object



9
10
11
# File 'lib/helpers.rb', line 9

def red(str)
  colorize(str, :red)
end

#runObject



5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 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 << "#{yellow(state_file_path)} doesn't exist"
      elsif e.message =~ /connect\(2\) for [^\/]/
        errors << "#{yellow("Relative Unix socket")}: the Unix socket of the control app has a relative path. Please, ensure you are running from the same folder as puma."
      else
        errors << "#{red(state_file_path)} an unhandled error occured: #{e.inspect}"
      end
      nil
    rescue Errno::EISDIR => e
      if e.message =~ /#{state_file_path}/
        errors << "#{yellow(state_file_path)} isn't a state file"
      else
        errors << "#{red(state_file_path)} an unhandled error occured: #{e.inspect}"
      end
      nil
    rescue => e
      errors << "#{red(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) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/helpers.rb', line 51

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

#yellow(str) ⇒ Object



5
6
7
# File 'lib/helpers.rb', line 5

def yellow(str)
  colorize(str, :yellow)
end