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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ServerStats
4
+ VERSION = "0.1.0"
5
+ end
@@ -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: []