puma-status 0.2 → 1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46a19b27c114c23f83f652c395c9157d2581a0c688a42c2784c3660bef0f3277
4
- data.tar.gz: 94a7a2cbee9aa0a082ddc38b61a32a4d2db3c3f8ad44a70b546a67b4a884dde8
3
+ metadata.gz: ded7bd0b4bca01b8b07ca16c7380453cf96f49d0c00667d024a6317d7307cea0
4
+ data.tar.gz: a391f836ca1c5d1a40acbab75178fd4c569d25005fac3c83bfd981051e1a7d66
5
5
  SHA512:
6
- metadata.gz: f3b70ede2ef3c33c2436efe7da75faa5e858c5042572d07d26204487ea61a60d31cd2f9497502a5f8ae00bf00ecbdae0ac5ea6bceba1bfeec28036e9e18dc39c
7
- data.tar.gz: f930e67b9deff2945d5f3a02c5635caf808b17a927527e533a3b7894e7b91ac59670c114c6fbb066a3be325fc9a78dc62cbfad12d26020a8e588b2a152d5e436
6
+ metadata.gz: c3968446849e0078c9cb557c721a1f5a2226a5ca1533016b319d15ec4c2e769a88062c8f354de28451fad533ba6502707a79660bf9a0469c677482f270ab1c65
7
+ data.tar.gz: e186aefc643f0ce7bfaf9515d5a36fc71558c0a871d792914e55e5867d5d0d9e4e32d04a71f27429ab1ae19f43054d06ae00c47911f209bcf423c197e4c586b6
data/lib/core.rb CHANGED
@@ -1,13 +1,28 @@
1
1
  require 'yaml'
2
2
  require 'json'
3
3
  require 'net_x/http_unix'
4
+ require 'openssl'
4
5
  require 'time'
5
6
  require_relative 'stats'
6
7
 
7
8
  def get_stats(state_file_path)
8
9
  puma_state = YAML.load_file(state_file_path)
9
10
 
10
- client = NetX::HTTPUnix.new(puma_state["control_url"])
11
+ uri = URI.parse(puma_state["control_url"])
12
+
13
+ address = if uri.scheme =~ /unix/i
14
+ [uri.scheme, '://', uri.host, uri.path].join
15
+ else
16
+ [uri.host, uri.path].join
17
+ end
18
+
19
+ client = NetX::HTTPUnix.new(address, uri.port)
20
+
21
+ if uri.scheme =~ /ssl/i
22
+ client.use_ssl = true
23
+ client.verify_mode = OpenSSL::SSL::VERIFY_NONE if ENV['SSL_NO_VERIFY'] == '1'
24
+ end
25
+
11
26
  req = Net::HTTP::Get.new("/stats?token=#{puma_state["control_auth_token"]}")
12
27
  resp = client.request(req)
13
28
  raw_stats = JSON.parse(resp.body)
@@ -17,10 +32,27 @@ def get_stats(state_file_path)
17
32
  hydrate_stats(stats, puma_state, state_file_path)
18
33
  end
19
34
 
35
+ def get_memory_from_top(raw_memory)
36
+ raw_memory.tr!(',', '.') # because of LC_NUMERIC separator can be ,
37
+
38
+ case raw_memory[-1].downcase
39
+ when 'g'
40
+ (raw_memory[0...-1].to_f*1024).to_i
41
+ when 'm'
42
+ raw_memory[0...-1].to_i
43
+ else
44
+ raw_memory.to_i/1024
45
+ end
46
+ end
47
+
48
+ PID_COLUMN = 0
49
+ MEM_COLUMN = 5
50
+ CPU_COLUMN = 8
51
+
20
52
  def get_top_stats(pids)
21
53
  pids.each_slice(19).inject({}) do |res, pids19|
22
54
  top_result = `top -b -n 1 -p #{pids19.join(',')} | tail -n #{pids19.length}`
23
- top_result.split("\n").map { |row| r = row.split(' '); [r[0].to_i, r[5].to_i/1024, r[8].to_f] }
55
+ 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] }
24
56
  .inject(res) { |hash, row| hash[row[0]] = { mem: row[1], pcpu: row[2] }; hash }
25
57
  res
26
58
  end
@@ -36,22 +68,39 @@ def hydrate_stats(stats, puma_state, state_file_path)
36
68
 
37
69
  stats.tap do |s|
38
70
  stats.workers.map do |wstats|
39
- wstats.mem = top_stats[wstats.pid][:mem]
40
- wstats.pcpu = top_stats[wstats.pid][:pcpu]
71
+ wstats.mem = top_stats.dig(wstats.pid, :mem) || 0
72
+ wstats.pcpu = top_stats.dig(wstats.pid, :pcpu) || 0
73
+ wstats.killed = !top_stats.key?(wstats.pid) || (wstats.mem <=0 && wstats.pcpu <= 0)
41
74
  end
42
75
  end
43
76
  end
44
77
 
45
78
  def format_stats(stats)
46
- master_line = "#{stats.pid} (#{stats.state_file_path}) Uptime: #{seconds_to_human(stats.uptime)} "
47
- master_line += "| Phase: #{stats.phase} " if stats.phase
48
- master_line += "| Load: #{color(75, 50, stats.load, asciiThreadLoad(stats.running_threads, stats.max_threads))}"
79
+ master_line = "#{stats.pid} (#{stats.state_file_path}) Uptime: #{seconds_to_human(stats.uptime)}"
80
+ master_line += " | Phase: #{stats.phase}" if stats.phase
81
+
82
+ if stats.booting?
83
+ master_line += " #{warn("booting")}"
84
+ else
85
+ master_line += " | Load: #{color(75, 50, stats.load, asciiThreadLoad(stats.running_threads, stats.spawned_threads, stats.max_threads))}"
86
+ master_line += " | Req: #{stats.requests_count}" if stats.requests_count
87
+ end
49
88
 
50
89
  output = [master_line] + stats.workers.map do |wstats|
51
- 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)} | Load: #{color(75, 50, wstats.load, asciiThreadLoad(wstats.running_threads, wstats.max_threads))}"
52
- worker_line += " #{("Queue: " + wstats.backlog.to_s).colorize(:red)}" if wstats.backlog > 0
53
- worker_line += " Last checkin: #{wstats.last_checkin}" if wstats.last_checkin >= 10
54
- worker_line += " Phase: #{wstats.phase}" if wstats.phase != stats.phase
90
+ 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)}"
91
+
92
+ if wstats.booting?
93
+ worker_line += " #{warn("booting")}"
94
+ elsif wstats.killed?
95
+ worker_line += " #{error("killed")}"
96
+ else
97
+ worker_line += " | Load: #{color(75, 50, wstats.load, asciiThreadLoad(wstats.running_threads, wstats.spawned_threads, wstats.max_threads))}"
98
+ worker_line += " | Phase: #{error(wstats.phase)}" if wstats.phase != stats.phase
99
+ worker_line += " | Req: #{wstats.requests_count}" if wstats.requests_count
100
+ worker_line += " Queue: #{error(wstats.backlog.to_s)}" if wstats.backlog > 0
101
+ worker_line += " Last checkin: #{error(wstats.last_checkin)}" if wstats.last_checkin >= 10
102
+ end
103
+
55
104
  worker_line
56
105
  end
57
106
 
data/lib/helpers.rb CHANGED
@@ -17,7 +17,8 @@ def colorize(str, color_name)
17
17
  str.to_s.colorize(color_name)
18
18
  end
19
19
 
20
- def color(critical, warn, value, str)
20
+ def color(critical, warn, value, str = nil)
21
+ str = value unless str
21
22
  color_level = if value >= critical
22
23
  :red
23
24
  elsif value < critical && value >= warn
@@ -28,11 +29,16 @@ def color(critical, warn, value, str)
28
29
  colorize(str, color_level)
29
30
  end
30
31
 
31
- def asciiThreadLoad(idx, total)
32
+ def asciiThreadLoad(running, spawned, total)
32
33
  full = "█"
33
- empty= "░"
34
+ half= "░"
35
+ empty = " "
34
36
 
35
- "#{idx}[#{full*idx}#{empty*(total-idx)}]#{total}"
37
+ full_count = running
38
+ half_count = [spawned - running, 0].max
39
+ empty_count = total - half_count - full_count
40
+
41
+ "#{running}[#{full*full_count}#{half*half_count}#{empty*empty_count}]#{total}"
36
42
  end
37
43
 
38
44
  def seconds_to_human(seconds)
@@ -43,8 +49,10 @@ def seconds_to_human(seconds)
43
49
  #=> 23h59m
44
50
  #=> 1d 0h
45
51
  #=> 24d
46
-
47
- if seconds < 60*60
52
+
53
+ if seconds <= 0
54
+ "--m--s"
55
+ elsif seconds < 60*60
48
56
  "#{(seconds/60).to_s.rjust(2, ' ')}m#{(seconds%60).to_s.rjust(2, ' ')}s"
49
57
  elsif seconds >= 60*60*1 && seconds < 60*60*24
50
58
  "#{(seconds/(60*60*1)).to_s.rjust(2, ' ')}h#{((seconds%(60*60*1))/60).to_s.rjust(2, ' ')}m"
data/lib/puma-status.rb CHANGED
@@ -17,11 +17,21 @@ def run
17
17
  begin
18
18
  debug "State file: #{state_file_path}"
19
19
  format_stats(get_stats(state_file_path))
20
- rescue Errno::ENOENT
21
- errors << "#{warn(state_file_path)} doesn't exists"
20
+ rescue Errno::ENOENT => e
21
+ if e.message =~ /#{state_file_path}/
22
+ errors << "#{warn(state_file_path)} doesn't exists"
23
+ elsif e.message =~ /connect\(2\) for [^\/]/
24
+ 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."
25
+ else
26
+ errors << "#{error(state_file_path)} an unhandled error occured: #{e.inspect}"
27
+ end
22
28
  nil
23
- rescue Errno::EISDIR
24
- errors << "#{warn(state_file_path)} isn't a state file"
29
+ rescue Errno::EISDIR => e
30
+ if e.message =~ /#{state_file_path}/
31
+ errors << "#{warn(state_file_path)} isn't a state file"
32
+ else
33
+ errors << "#{error(state_file_path)} an unhandled error occured: #{e.inspect}"
34
+ end
25
35
  nil
26
36
  rescue => e
27
37
  errors << "#{error(state_file_path)} an unhandled error occured: #{e.inspect}"
data/lib/stats.rb CHANGED
@@ -9,6 +9,14 @@ class Stats
9
9
  @wstats['pid']
10
10
  end
11
11
 
12
+ def killed=(killed)
13
+ @wstats['killed'] = killed
14
+ end
15
+
16
+ def killed?
17
+ !!@wstats['killed']
18
+ end
19
+
12
20
  def mem=(mem)
13
21
  @wstats['mem'] = mem
14
22
  end
@@ -25,14 +33,20 @@ class Stats
25
33
  @wstats['pcpu']
26
34
  end
27
35
 
36
+ def booting?
37
+ @wstats.key?('last_status') && @wstats['last_status'].empty?
38
+ end
39
+
28
40
  def running
29
41
  @wstats.dig('last_status', 'running') || @wstats['running'] || 0
30
42
  end
31
43
  alias :total_threads :running
44
+ alias :spawned_threads :running
32
45
 
33
46
  def max_threads
34
47
  @wstats.dig('last_status', 'max_threads') || @wstats['max_threads'] || 0
35
48
  end
49
+ alias :total_threads :max_threads
36
50
 
37
51
  def pool_capacity
38
52
  @wstats.dig('last_status', 'pool_capacity') || @wstats['pool_capacity'] || 0
@@ -55,6 +69,10 @@ class Stats
55
69
  (Time.now - Time.parse(@wstats['started_at'])).to_i
56
70
  end
57
71
 
72
+ def requests_count
73
+ @wstats.dig('last_status', 'requests_count') || @wstats['requests_count']
74
+ end
75
+
58
76
  def backlog
59
77
  @wstats.dig('last_status', 'backlog') || 0
60
78
  end
@@ -71,7 +89,7 @@ class Stats
71
89
  end
72
90
 
73
91
  def workers
74
- (@stats['worker_status'] || [@stats]).map { |wstats| Worker.new(wstats) }
92
+ @workers ||= (@stats['worker_status'] || [@stats]).map { |wstats| Worker.new(wstats) }
75
93
  end
76
94
 
77
95
  def pid=(pid)
@@ -95,6 +113,10 @@ class Stats
95
113
  (Time.now - Time.parse(@stats['started_at'])).to_i
96
114
  end
97
115
 
116
+ def booting?
117
+ workers.all?(&:booting?)
118
+ end
119
+
98
120
  def total_threads
99
121
  workers.reduce(0) { |total, wstats| total + wstats.max_threads }
100
122
  end
@@ -103,10 +125,20 @@ class Stats
103
125
  workers.reduce(0) { |total, wstats| total + wstats.running_threads }
104
126
  end
105
127
 
128
+ def spawned_threads
129
+ workers.reduce(0) { |total, wstats| total + wstats.spawned_threads }
130
+ end
131
+
106
132
  def max_threads
107
133
  workers.reduce(0) { |total, wstats| total + wstats.max_threads }
108
134
  end
109
135
 
136
+ def requests_count
137
+ workers_with_requests_count = workers.select(&:requests_count)
138
+ return if workers_with_requests_count.none?
139
+ workers_with_requests_count.reduce(0) { |total, wstats| total + wstats.requests_count }
140
+ end
141
+
110
142
  def running
111
143
  @stats['running'] || 0
112
144
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puma-status
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: '1.3'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yoann Lecuyer
@@ -126,7 +126,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
126
126
  - !ruby/object:Gem::Version
127
127
  version: '0'
128
128
  requirements: []
129
- rubygems_version: 3.1.2
129
+ rubygems_version: 3.3.0.dev
130
130
  signing_key:
131
131
  specification_version: 4
132
132
  summary: Command-line tool for puma to display information about running request/process