puma-status 0.2 → 1.3

Sign up to get free protection for your applications and to get access to all the features.
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