server_stats 0.1.0
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 +7 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +397 -0
- data/README_EN.md +405 -0
- data/Rakefile +10 -0
- data/bin/server-stats +4 -0
- data/bin/server_stats +9 -0
- data/lib/server_stats/cli.rb +193 -0
- data/lib/server_stats/collector.rb +61 -0
- data/lib/server_stats/formatter.rb +54 -0
- data/lib/server_stats/models.rb +205 -0
- data/lib/server_stats/os.rb +31 -0
- data/lib/server_stats/providers/base.rb +94 -0
- data/lib/server_stats/providers/linux.rb +220 -0
- data/lib/server_stats/providers/macos.rb +225 -0
- data/lib/server_stats/version.rb +5 -0
- data/lib/server_stats.rb +100 -0
- data/server_stats.gemspec +25 -0
- metadata +64 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module ServerStats
|
|
6
|
+
module Providers
|
|
7
|
+
class MacOs < Base
|
|
8
|
+
attr_accessor :sysctl_executor, :vm_stat_output, :iostat_output
|
|
9
|
+
|
|
10
|
+
def initialize(options = {})
|
|
11
|
+
super()
|
|
12
|
+
@options = options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Collect CPU statistics on macOS
|
|
16
|
+
def cpu(interval = 0.5)
|
|
17
|
+
cores = logical_cpu_cores
|
|
18
|
+
phys_cores = physical_cpu_cores
|
|
19
|
+
model = cpu_model
|
|
20
|
+
load_avg = parse_load_average
|
|
21
|
+
usage_pct = calculate_cpu_usage(interval.to_f)
|
|
22
|
+
|
|
23
|
+
CpuStats.new(
|
|
24
|
+
cores: cores,
|
|
25
|
+
physical_cores: phys_cores || cores,
|
|
26
|
+
usage_percent: usage_pct,
|
|
27
|
+
load_average: load_avg,
|
|
28
|
+
model: model
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Collect Memory statistics on macOS
|
|
33
|
+
def memory
|
|
34
|
+
total_bytes = sysctl_int("hw.memsize")
|
|
35
|
+
vm_output = @vm_stat_output || execute("vm_stat")
|
|
36
|
+
stats, page_size = parse_vm_stat(vm_output)
|
|
37
|
+
|
|
38
|
+
free_pages = stats["Pages free"] || 0
|
|
39
|
+
speculative_pages = stats["Pages speculative"] || 0
|
|
40
|
+
inactive_pages = stats["Pages inactive"] || 0
|
|
41
|
+
purgeable_pages = stats["Pages purgeable"] || 0
|
|
42
|
+
wired_pages = stats["Pages wired down"] || 0
|
|
43
|
+
active_pages = stats["Pages active"] || 0
|
|
44
|
+
compressor_pages = stats["Pages occupied by compressor"] || 0
|
|
45
|
+
|
|
46
|
+
free_bytes = (free_pages + speculative_pages) * page_size
|
|
47
|
+
available_bytes = (free_pages + inactive_pages + speculative_pages + purgeable_pages) * page_size
|
|
48
|
+
available_bytes = [available_bytes, total_bytes].min
|
|
49
|
+
used_bytes = [total_bytes - available_bytes, 0].max
|
|
50
|
+
|
|
51
|
+
MemoryStats.new(
|
|
52
|
+
total_bytes: total_bytes,
|
|
53
|
+
free_bytes: free_bytes,
|
|
54
|
+
available_bytes: available_bytes,
|
|
55
|
+
used_bytes: used_bytes,
|
|
56
|
+
details: {
|
|
57
|
+
page_size_bytes: page_size,
|
|
58
|
+
active_bytes: active_pages * page_size,
|
|
59
|
+
inactive_bytes: inactive_pages * page_size,
|
|
60
|
+
wired_bytes: wired_pages * page_size,
|
|
61
|
+
purgeable_bytes: purgeable_pages * page_size,
|
|
62
|
+
compressed_bytes: compressor_pages * page_size
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Collect System statistics on macOS
|
|
68
|
+
def system
|
|
69
|
+
product_name = execute("sw_vers -productName")
|
|
70
|
+
product_name = "macOS" if product_name.empty?
|
|
71
|
+
|
|
72
|
+
version = execute("sw_vers -productVersion")
|
|
73
|
+
version = "unknown" if version.empty?
|
|
74
|
+
|
|
75
|
+
arch = execute("uname -m")
|
|
76
|
+
arch = "unknown" if arch.empty?
|
|
77
|
+
|
|
78
|
+
kernel = execute("uname -r")
|
|
79
|
+
kernel = "unknown" if kernel.empty?
|
|
80
|
+
|
|
81
|
+
uptime_secs = parse_uptime
|
|
82
|
+
|
|
83
|
+
SystemStats.new(
|
|
84
|
+
os: product_name,
|
|
85
|
+
os_family: :macos,
|
|
86
|
+
os_version: version,
|
|
87
|
+
hostname: hostname,
|
|
88
|
+
architecture: arch,
|
|
89
|
+
kernel: kernel,
|
|
90
|
+
uptime_seconds: uptime_secs
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Internal helper: logical CPU cores
|
|
95
|
+
def logical_cpu_cores
|
|
96
|
+
val = sysctl_int("hw.ncpu")
|
|
97
|
+
val.positive? ? val : cpu_cores
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Internal helper: physical CPU cores
|
|
101
|
+
def physical_cpu_cores
|
|
102
|
+
val = sysctl_int("hw.physicalcpu")
|
|
103
|
+
val.positive? ? val : logical_cpu_cores
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Internal helper: CPU model brand string
|
|
107
|
+
def cpu_model
|
|
108
|
+
brand = execute("sysctl -n machdep.cpu.brand_string")
|
|
109
|
+
return brand unless brand.empty?
|
|
110
|
+
|
|
111
|
+
model = execute("sysctl -n hw.model")
|
|
112
|
+
return model unless model.empty?
|
|
113
|
+
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Internal helper: sysctl integer helper
|
|
118
|
+
def sysctl_int(key)
|
|
119
|
+
execute("sysctl -n #{key}").to_i
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Internal helper: parse load average on macOS
|
|
123
|
+
def parse_load_average
|
|
124
|
+
raw = execute("sysctl -n vm.loadavg")
|
|
125
|
+
if (m = raw.match(/\{\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)\s*\}/))
|
|
126
|
+
return {
|
|
127
|
+
one_minute: m[1].to_f,
|
|
128
|
+
five_minutes: m[2].to_f,
|
|
129
|
+
fifteen_minutes: m[3].to_f
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Fallback to uptime command
|
|
134
|
+
uptime_str = execute("uptime")
|
|
135
|
+
if (m = uptime_str.match(/load averages?:\s*([\d.]+)\s+([\d.]+)\s+([\d.]+)/))
|
|
136
|
+
return {
|
|
137
|
+
one_minute: m[1].to_f,
|
|
138
|
+
five_minutes: m[2].to_f,
|
|
139
|
+
fifteen_minutes: m[3].to_f
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
{ one_minute: 0.0, five_minutes: 0.0, fifteen_minutes: 0.0 }
|
|
144
|
+
rescue StandardError
|
|
145
|
+
{ one_minute: 0.0, five_minutes: 0.0, fifteen_minutes: 0.0 }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Internal helper: parse vm_stat output
|
|
149
|
+
def parse_vm_stat(output)
|
|
150
|
+
return [{}, 4096] if output.nil? || output.empty?
|
|
151
|
+
|
|
152
|
+
page_size = output[/page size of (\d+) bytes/, 1]&.to_i || 4096
|
|
153
|
+
stats = {}
|
|
154
|
+
|
|
155
|
+
output.each_line do |line|
|
|
156
|
+
if (m = line.match(/^"?([^":]+)"?:\s+(\d+)\./))
|
|
157
|
+
stats[m[1].strip] = m[2].to_i
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
[stats, page_size]
|
|
162
|
+
rescue StandardError
|
|
163
|
+
[{}, 4096]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Internal helper: parse uptime on macOS
|
|
167
|
+
def parse_uptime
|
|
168
|
+
raw = execute("sysctl -n kern.boottime")
|
|
169
|
+
if (m = raw.match(/sec\s*=\s*(\d+)/))
|
|
170
|
+
boot_time = m[1].to_i
|
|
171
|
+
return [Time.now.to_i - boot_time, 0].max
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
0
|
|
175
|
+
rescue StandardError
|
|
176
|
+
0
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Internal helper: calculate CPU usage % on macOS
|
|
180
|
+
def calculate_cpu_usage(interval)
|
|
181
|
+
# Attempt 1: iostat with wait interval
|
|
182
|
+
sample_sec = [interval.round, 1].max
|
|
183
|
+
out = @iostat_output || execute("iostat -c 2 -w #{sample_sec}")
|
|
184
|
+
if (pct = parse_iostat_cpu(out))
|
|
185
|
+
return pct
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Attempt 2: top snapshot
|
|
189
|
+
top_out = execute("top -l 1 -n 0")
|
|
190
|
+
if (m = top_out.match(/CPU usage:\s*([\d.]+)%\s*user,\s*([\d.]+)%\s*sys/))
|
|
191
|
+
return (m[1].to_f + m[2].to_f).round(2)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
0.0
|
|
195
|
+
rescue StandardError
|
|
196
|
+
0.0
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Parse iostat output for CPU utilization
|
|
200
|
+
def parse_iostat_cpu(output)
|
|
201
|
+
return nil if output.nil? || output.empty?
|
|
202
|
+
|
|
203
|
+
lines = output.lines.map(&:strip).reject(&:empty?)
|
|
204
|
+
return nil if lines.size < 2
|
|
205
|
+
|
|
206
|
+
# The last line has the second sample interval stats
|
|
207
|
+
last_parts = lines.last.split(/\s+/)
|
|
208
|
+
header_line = lines.find { |l| l.include?("us") && l.include?("sy") && l.include?("id") }
|
|
209
|
+
return nil unless header_line
|
|
210
|
+
|
|
211
|
+
# In iostat output: ... us sy id 1m 5m 15m
|
|
212
|
+
# Counting from end: -6 is us, -5 is sy, -4 is id
|
|
213
|
+
if last_parts.size >= 6
|
|
214
|
+
id = last_parts[-4].to_f
|
|
215
|
+
usage = 100.0 - id
|
|
216
|
+
return [[usage, 0.0].max, 100.0].min.round(2)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
nil
|
|
220
|
+
rescue StandardError
|
|
221
|
+
nil
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
data/lib/server_stats.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "server_stats/version"
|
|
5
|
+
require_relative "server_stats/formatter"
|
|
6
|
+
require_relative "server_stats/models"
|
|
7
|
+
require_relative "server_stats/collector"
|
|
8
|
+
|
|
9
|
+
module ServerStats
|
|
10
|
+
class << self
|
|
11
|
+
def collector
|
|
12
|
+
@collector ||= Collector.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def collector=(custom_collector)
|
|
16
|
+
@collector = custom_collector
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cpu(interval: 0.5)
|
|
20
|
+
collector.cpu(interval: interval)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def memory
|
|
24
|
+
collector.memory
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
alias ram memory
|
|
28
|
+
|
|
29
|
+
def disk(path = "/")
|
|
30
|
+
path = path[:path] if path.is_a?(Hash) && path.key?(:path)
|
|
31
|
+
collector.disk(path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def system
|
|
35
|
+
collector.system
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def all(path: "/", interval: 0.5)
|
|
39
|
+
collector.all(path: path, interval: interval)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
alias snapshot all
|
|
43
|
+
|
|
44
|
+
def to_h(path: "/", interval: 0.5)
|
|
45
|
+
all(path: path, interval: interval).to_h
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to_json(path: "/", interval: 0.5, pretty: false)
|
|
49
|
+
hash = to_h(path: path, interval: interval)
|
|
50
|
+
pretty ? JSON.pretty_generate(hash) : hash.to_json
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def summary(path: "/", interval: 0.5)
|
|
54
|
+
snap = all(path: path, interval: interval)
|
|
55
|
+
sys = snap.system
|
|
56
|
+
cpu = snap.cpu
|
|
57
|
+
mem = snap.memory
|
|
58
|
+
dsk = snap.disk
|
|
59
|
+
|
|
60
|
+
load_str = if cpu.load_average.any?
|
|
61
|
+
"#{cpu.load_average[:one_minute]} (1m), #{cpu.load_average[:five_minutes]} (5m), #{cpu.load_average[:fifteen_minutes]} (15m)"
|
|
62
|
+
else
|
|
63
|
+
"N/A"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
lines = []
|
|
67
|
+
lines << "=" * 60
|
|
68
|
+
lines << " SERVER STATS REPORT"
|
|
69
|
+
lines << "=" * 60
|
|
70
|
+
if sys
|
|
71
|
+
lines << format(" %-14s %s %s (%s)", "OS:", sys.os, sys.os_version, sys.architecture)
|
|
72
|
+
lines << format(" %-14s %s", "Hostname:", sys.hostname)
|
|
73
|
+
lines << format(" %-14s %s (Kernel: %s)", "Uptime:", sys.uptime_human, sys.kernel)
|
|
74
|
+
lines << "-" * 60
|
|
75
|
+
end
|
|
76
|
+
if cpu
|
|
77
|
+
lines << format(" %-14s %d cores (Physical: %d)", "CPU Cores:", cpu.cores, cpu.physical_cores)
|
|
78
|
+
lines << format(" %-14s %s", "CPU Model:", cpu.model || "Unknown") if cpu.model
|
|
79
|
+
lines << format(" %-14s %s", "CPU Usage:", Formatter.format_percent(cpu.usage_percent))
|
|
80
|
+
lines << format(" %-14s %s", "Load Avg:", load_str)
|
|
81
|
+
lines << "-" * 60
|
|
82
|
+
end
|
|
83
|
+
if mem
|
|
84
|
+
lines << format(" %-14s %s", "RAM Total:", mem.total_human)
|
|
85
|
+
lines << format(" %-14s %s (%s)", "RAM Used:", mem.used_human, Formatter.format_percent(mem.usage_percent))
|
|
86
|
+
lines << format(" %-14s %s", "RAM Available:", mem.available_human)
|
|
87
|
+
lines << format(" %-14s %s", "RAM Free:", mem.free_human)
|
|
88
|
+
lines << "-" * 60
|
|
89
|
+
end
|
|
90
|
+
if dsk
|
|
91
|
+
lines << format(" %-14s %s (Mount: %s)", "Disk Total:", dsk.total_human, dsk.path)
|
|
92
|
+
lines << format(" %-14s %s (%s)", "Disk Used:", dsk.used_human, Formatter.format_percent(dsk.usage_percent))
|
|
93
|
+
lines << format(" %-14s %s", "Disk Available:", dsk.available_human)
|
|
94
|
+
lines << format(" %-14s %s", "Filesystem:", dsk.filesystem)
|
|
95
|
+
end
|
|
96
|
+
lines << "=" * 60
|
|
97
|
+
lines.join("\n")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/server_stats/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "server_stats"
|
|
7
|
+
spec.version = ServerStats::VERSION
|
|
8
|
+
spec.authors = ["Tam Nguyen"]
|
|
9
|
+
spec.email = ["ntamvl@gmail.com"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Thu vien Ruby gem lay thong ke trang thai server cho Ubuntu va macOS"
|
|
12
|
+
spec.description = "Thư viện Ruby gem thu thập thông số tài nguyên hệ thống (CPU cores, % CPU sử dụng, RAM tổng/trống, Ổ cứng tổng/trống, OS) trên Ubuntu/Linux và macOS mà không cần gem phụ thuộc bên ngoài."
|
|
13
|
+
spec.homepage = "https://github.com/ntamvl/server_stats"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 2.5.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
19
|
+
|
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
|
21
|
+
spec.files = Dir.glob("{bin,lib}/**/*") + %w[README.md README_EN.md LICENSE server_stats.gemspec Rakefile Gemfile]
|
|
22
|
+
spec.bindir = "bin"
|
|
23
|
+
spec.executables = ["server_stats", "server-stats"]
|
|
24
|
+
spec.require_paths = ["lib"]
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: server_stats
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tam Nguyen
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Thư viện Ruby gem thu thập thông số tài nguyên hệ thống (CPU cores, %
|
|
13
|
+
CPU sử dụng, RAM tổng/trống, Ổ cứng tổng/trống, OS) trên Ubuntu/Linux và macOS mà
|
|
14
|
+
không cần gem phụ thuộc bên ngoài.
|
|
15
|
+
email:
|
|
16
|
+
- ntamvl@gmail.com
|
|
17
|
+
executables:
|
|
18
|
+
- server-stats
|
|
19
|
+
- server_stats
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- Gemfile
|
|
24
|
+
- LICENSE
|
|
25
|
+
- README.md
|
|
26
|
+
- README_EN.md
|
|
27
|
+
- Rakefile
|
|
28
|
+
- bin/server-stats
|
|
29
|
+
- bin/server_stats
|
|
30
|
+
- lib/server_stats.rb
|
|
31
|
+
- lib/server_stats/cli.rb
|
|
32
|
+
- lib/server_stats/collector.rb
|
|
33
|
+
- lib/server_stats/formatter.rb
|
|
34
|
+
- lib/server_stats/models.rb
|
|
35
|
+
- lib/server_stats/os.rb
|
|
36
|
+
- lib/server_stats/providers/base.rb
|
|
37
|
+
- lib/server_stats/providers/linux.rb
|
|
38
|
+
- lib/server_stats/providers/macos.rb
|
|
39
|
+
- lib/server_stats/version.rb
|
|
40
|
+
- server_stats.gemspec
|
|
41
|
+
homepage: https://github.com/ntamvl/server_stats
|
|
42
|
+
licenses:
|
|
43
|
+
- MIT
|
|
44
|
+
metadata:
|
|
45
|
+
homepage_uri: https://github.com/ntamvl/server_stats
|
|
46
|
+
source_code_uri: https://github.com/ntamvl/server_stats
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: 2.5.0
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubygems_version: 4.0.19
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Thu vien Ruby gem lay thong ke trang thai server cho Ubuntu va macOS
|
|
64
|
+
test_files: []
|