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,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ServerStats
|
|
4
|
+
module Formatter
|
|
5
|
+
UNITS = %w[B KB MB GB TB PB].freeze
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Format bytes to human readable string (e.g. 1073741824 -> "1.00 GB")
|
|
10
|
+
def bytes_to_human(bytes, precision = 2)
|
|
11
|
+
return "0 B" if bytes.nil? || bytes <= 0
|
|
12
|
+
|
|
13
|
+
value = bytes.to_f
|
|
14
|
+
unit_index = 0
|
|
15
|
+
|
|
16
|
+
while value >= 1024.0 && unit_index < UNITS.size - 1
|
|
17
|
+
value /= 1024.0
|
|
18
|
+
unit_index += 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if unit_index.zero?
|
|
22
|
+
"#{bytes} B"
|
|
23
|
+
else
|
|
24
|
+
"#{format("%.#{precision}f", value)} #{UNITS[unit_index]}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Format seconds to human readable string (e.g. 90061 -> "1d 1h 1m 1s")
|
|
29
|
+
def seconds_to_human(total_seconds)
|
|
30
|
+
return "0s" if total_seconds.nil? || total_seconds <= 0
|
|
31
|
+
|
|
32
|
+
secs = total_seconds.to_i
|
|
33
|
+
days = secs / 86_400
|
|
34
|
+
hours = (secs % 86_400) / 3600
|
|
35
|
+
minutes = (secs % 3600) / 60
|
|
36
|
+
seconds = secs % 60
|
|
37
|
+
|
|
38
|
+
parts = []
|
|
39
|
+
parts << "#{days}d" if days.positive?
|
|
40
|
+
parts << "#{hours}h" if hours.positive?
|
|
41
|
+
parts << "#{minutes}m" if minutes.positive?
|
|
42
|
+
parts << "#{seconds}s" if seconds.positive? || parts.empty?
|
|
43
|
+
|
|
44
|
+
parts.join(" ")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Format percentage with standard precision
|
|
48
|
+
def format_percent(percent, precision = 2)
|
|
49
|
+
return "0.0%" if percent.nil?
|
|
50
|
+
|
|
51
|
+
"#{format("%.#{precision}f", percent)}%"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "formatter"
|
|
6
|
+
|
|
7
|
+
module ServerStats
|
|
8
|
+
class CpuStats
|
|
9
|
+
attr_reader :cores, :physical_cores, :usage_percent, :load_average, :model
|
|
10
|
+
|
|
11
|
+
def initialize(cores:, usage_percent:, load_average: {}, physical_cores: nil, model: nil)
|
|
12
|
+
@cores = cores
|
|
13
|
+
@usage_percent = usage_percent ? usage_percent.round(2) : 0.0
|
|
14
|
+
@load_average = load_average || {}
|
|
15
|
+
@physical_cores = physical_cores || cores
|
|
16
|
+
@model = model
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
cores: @cores,
|
|
22
|
+
physical_cores: @physical_cores,
|
|
23
|
+
usage_percent: @usage_percent,
|
|
24
|
+
load_average: @load_average,
|
|
25
|
+
model: @model
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_json(*args)
|
|
30
|
+
to_h.to_json(*args)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class MemoryStats
|
|
35
|
+
attr_reader :total_bytes, :free_bytes, :available_bytes, :used_bytes,
|
|
36
|
+
:usage_percent, :details
|
|
37
|
+
|
|
38
|
+
def initialize(total_bytes:, free_bytes:, available_bytes: nil, used_bytes: nil, details: {})
|
|
39
|
+
@total_bytes = total_bytes.to_i
|
|
40
|
+
@free_bytes = free_bytes.to_i
|
|
41
|
+
@available_bytes = (available_bytes || free_bytes).to_i
|
|
42
|
+
@used_bytes = (used_bytes || [@total_bytes - @available_bytes, 0].max).to_i
|
|
43
|
+
@usage_percent = if @total_bytes.positive?
|
|
44
|
+
((@used_bytes.to_f / @total_bytes) * 100.0).round(2)
|
|
45
|
+
else
|
|
46
|
+
0.0
|
|
47
|
+
end
|
|
48
|
+
@details = details || {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def total_human
|
|
52
|
+
Formatter.bytes_to_human(@total_bytes)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def free_human
|
|
56
|
+
Formatter.bytes_to_human(@free_bytes)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def available_human
|
|
60
|
+
Formatter.bytes_to_human(@available_bytes)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def used_human
|
|
64
|
+
Formatter.bytes_to_human(@used_bytes)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def to_h
|
|
68
|
+
{
|
|
69
|
+
total_bytes: @total_bytes,
|
|
70
|
+
free_bytes: @free_bytes,
|
|
71
|
+
available_bytes: @available_bytes,
|
|
72
|
+
used_bytes: @used_bytes,
|
|
73
|
+
usage_percent: @usage_percent,
|
|
74
|
+
total_human: total_human,
|
|
75
|
+
free_human: free_human,
|
|
76
|
+
available_human: available_human,
|
|
77
|
+
used_human: used_human,
|
|
78
|
+
details: @details
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def to_json(*args)
|
|
83
|
+
to_h.to_json(*args)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class DiskStats
|
|
88
|
+
attr_reader :path, :filesystem, :total_bytes, :used_bytes, :free_bytes,
|
|
89
|
+
:available_bytes, :usage_percent
|
|
90
|
+
|
|
91
|
+
def initialize(path:, filesystem:, total_bytes:, used_bytes:, available_bytes:, free_bytes: nil)
|
|
92
|
+
@path = path
|
|
93
|
+
@filesystem = filesystem
|
|
94
|
+
@total_bytes = total_bytes.to_i
|
|
95
|
+
@used_bytes = used_bytes.to_i
|
|
96
|
+
@available_bytes = available_bytes.to_i
|
|
97
|
+
@free_bytes = (free_bytes || available_bytes).to_i
|
|
98
|
+
@usage_percent = if @total_bytes.positive?
|
|
99
|
+
((@used_bytes.to_f / @total_bytes) * 100.0).round(2)
|
|
100
|
+
else
|
|
101
|
+
0.0
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def total_human
|
|
106
|
+
Formatter.bytes_to_human(@total_bytes)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def free_human
|
|
110
|
+
Formatter.bytes_to_human(@free_bytes)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def available_human
|
|
114
|
+
Formatter.bytes_to_human(@available_bytes)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def used_human
|
|
118
|
+
Formatter.bytes_to_human(@used_bytes)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def to_h
|
|
122
|
+
{
|
|
123
|
+
path: @path,
|
|
124
|
+
filesystem: @filesystem,
|
|
125
|
+
total_bytes: @total_bytes,
|
|
126
|
+
used_bytes: @used_bytes,
|
|
127
|
+
free_bytes: @free_bytes,
|
|
128
|
+
available_bytes: @available_bytes,
|
|
129
|
+
usage_percent: @usage_percent,
|
|
130
|
+
total_human: total_human,
|
|
131
|
+
used_human: used_human,
|
|
132
|
+
free_human: free_human,
|
|
133
|
+
available_human: available_human
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def to_json(*args)
|
|
138
|
+
to_h.to_json(*args)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class SystemStats
|
|
143
|
+
attr_reader :os, :os_family, :os_version, :hostname, :architecture,
|
|
144
|
+
:kernel, :uptime_seconds
|
|
145
|
+
|
|
146
|
+
def initialize(os:, os_family:, os_version:, hostname:, architecture:, kernel:, uptime_seconds:)
|
|
147
|
+
@os = os
|
|
148
|
+
@os_family = os_family
|
|
149
|
+
@os_version = os_version
|
|
150
|
+
@hostname = hostname
|
|
151
|
+
@architecture = architecture
|
|
152
|
+
@kernel = kernel
|
|
153
|
+
@uptime_seconds = uptime_seconds.to_i
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def uptime_human
|
|
157
|
+
Formatter.seconds_to_human(@uptime_seconds)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def to_h
|
|
161
|
+
{
|
|
162
|
+
os: @os,
|
|
163
|
+
os_family: @os_family,
|
|
164
|
+
os_version: @os_version,
|
|
165
|
+
hostname: @hostname,
|
|
166
|
+
architecture: @architecture,
|
|
167
|
+
kernel: @kernel,
|
|
168
|
+
uptime_seconds: @uptime_seconds,
|
|
169
|
+
uptime_human: uptime_human
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def to_json(*args)
|
|
174
|
+
to_h.to_json(*args)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
class Snapshot
|
|
179
|
+
attr_reader :cpu, :memory, :disk, :system, :timestamp
|
|
180
|
+
|
|
181
|
+
def initialize(cpu:, memory:, disk:, system:, timestamp: nil)
|
|
182
|
+
@cpu = cpu
|
|
183
|
+
@memory = memory
|
|
184
|
+
@disk = disk
|
|
185
|
+
@system = system
|
|
186
|
+
@timestamp = timestamp || Time.now.utc.iso8601
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
alias ram memory
|
|
190
|
+
|
|
191
|
+
def to_h
|
|
192
|
+
{
|
|
193
|
+
timestamp: @timestamp,
|
|
194
|
+
system: @system&.to_h,
|
|
195
|
+
cpu: @cpu&.to_h,
|
|
196
|
+
memory: @memory&.to_h,
|
|
197
|
+
disk: @disk&.to_h
|
|
198
|
+
}
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def to_json(*args)
|
|
202
|
+
to_h.to_json(*args)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
|
|
5
|
+
module ServerStats
|
|
6
|
+
module OS
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def host_os
|
|
10
|
+
RbConfig::CONFIG["host_os"]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def macos?
|
|
14
|
+
/darwin|mac os/i.match?(host_os)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def linux?
|
|
18
|
+
/linux/i.match?(host_os)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def type
|
|
22
|
+
if macos?
|
|
23
|
+
:macos
|
|
24
|
+
elsif linux?
|
|
25
|
+
:linux
|
|
26
|
+
else
|
|
27
|
+
:unknown
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "etc"
|
|
6
|
+
require "shellwords"
|
|
7
|
+
require_relative "../models"
|
|
8
|
+
|
|
9
|
+
module ServerStats
|
|
10
|
+
module Providers
|
|
11
|
+
class Base
|
|
12
|
+
# Execute command safely without raising exception
|
|
13
|
+
def execute(command)
|
|
14
|
+
stdout, _stderr, status = Open3.capture3(command)
|
|
15
|
+
return stdout.strip if status.success?
|
|
16
|
+
|
|
17
|
+
""
|
|
18
|
+
rescue StandardError
|
|
19
|
+
""
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Cross-platform disk stats using POSIX `df -P -k <path>`
|
|
23
|
+
def disk_stats(path = "/")
|
|
24
|
+
clean_path = path.to_s.strip
|
|
25
|
+
clean_path = "/" if clean_path.empty?
|
|
26
|
+
|
|
27
|
+
output = execute("df -P -k #{clean_path.shellescape}")
|
|
28
|
+
lines = output.lines.map(&:strip).reject(&:empty?)
|
|
29
|
+
|
|
30
|
+
if lines.size >= 2
|
|
31
|
+
# Skip header line, parse first data line
|
|
32
|
+
parts = lines[1].split(/\s+/)
|
|
33
|
+
filesystem = parts[0]
|
|
34
|
+
total_kb = parts[1].to_i
|
|
35
|
+
used_kb = parts[2].to_i
|
|
36
|
+
available_kb = parts[3].to_i
|
|
37
|
+
|
|
38
|
+
total_bytes = total_kb * 1024
|
|
39
|
+
used_bytes = used_kb * 1024
|
|
40
|
+
available_bytes = available_kb * 1024
|
|
41
|
+
|
|
42
|
+
DiskStats.new(
|
|
43
|
+
path: clean_path,
|
|
44
|
+
filesystem: filesystem,
|
|
45
|
+
total_bytes: total_bytes,
|
|
46
|
+
used_bytes: used_bytes,
|
|
47
|
+
available_bytes: available_bytes,
|
|
48
|
+
free_bytes: available_bytes
|
|
49
|
+
)
|
|
50
|
+
else
|
|
51
|
+
DiskStats.new(
|
|
52
|
+
path: clean_path,
|
|
53
|
+
filesystem: "unknown",
|
|
54
|
+
total_bytes: 0,
|
|
55
|
+
used_bytes: 0,
|
|
56
|
+
available_bytes: 0,
|
|
57
|
+
free_bytes: 0
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Logical CPU core count via Ruby's standard Etc
|
|
63
|
+
def cpu_cores
|
|
64
|
+
Etc.nprocessors
|
|
65
|
+
rescue StandardError
|
|
66
|
+
1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Hostname helper
|
|
70
|
+
def hostname
|
|
71
|
+
Socket.gethostname
|
|
72
|
+
rescue StandardError
|
|
73
|
+
"localhost"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Abstract methods to be implemented by OS providers
|
|
77
|
+
def cpu(_interval = 0.5)
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def memory
|
|
82
|
+
raise NotImplementedError
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def disk(path = "/")
|
|
86
|
+
disk_stats(path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def system
|
|
90
|
+
raise NotImplementedError
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module ServerStats
|
|
6
|
+
module Providers
|
|
7
|
+
class Linux < Base
|
|
8
|
+
attr_accessor :proc_meminfo_path, :proc_stat_path, :proc_loadavg_path,
|
|
9
|
+
:proc_uptime_path, :proc_cpuinfo_path, :os_release_path
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
super()
|
|
13
|
+
@proc_meminfo_path = options[:proc_meminfo_path] || "/proc/meminfo"
|
|
14
|
+
@proc_stat_path = options[:proc_stat_path] || "/proc/stat"
|
|
15
|
+
@proc_loadavg_path = options[:proc_loadavg_path] || "/proc/loadavg"
|
|
16
|
+
@proc_uptime_path = options[:proc_uptime_path] || "/proc/uptime"
|
|
17
|
+
@proc_cpuinfo_path = options[:proc_cpuinfo_path] || "/proc/cpuinfo"
|
|
18
|
+
@os_release_path = options[:os_release_path] || "/etc/os-release"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Collect CPU statistics on Linux
|
|
22
|
+
def cpu(interval = 0.5)
|
|
23
|
+
cores = cpu_cores
|
|
24
|
+
model, physical_cores = parse_cpuinfo
|
|
25
|
+
load_avg = parse_load_average
|
|
26
|
+
usage_pct = calculate_cpu_usage(interval.to_f)
|
|
27
|
+
|
|
28
|
+
CpuStats.new(
|
|
29
|
+
cores: cores,
|
|
30
|
+
physical_cores: physical_cores || cores,
|
|
31
|
+
usage_percent: usage_pct,
|
|
32
|
+
load_average: load_avg,
|
|
33
|
+
model: model
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Collect Memory statistics on Linux
|
|
38
|
+
def memory
|
|
39
|
+
meminfo = parse_meminfo
|
|
40
|
+
|
|
41
|
+
total_bytes = (meminfo["MemTotal"] || 0) * 1024
|
|
42
|
+
free_bytes = (meminfo["MemFree"] || 0) * 1024
|
|
43
|
+
buffers_bytes = (meminfo["Buffers"] || 0) * 1024
|
|
44
|
+
cached_bytes = (meminfo["Cached"] || 0) * 1024
|
|
45
|
+
available_kb = meminfo["MemAvailable"]
|
|
46
|
+
|
|
47
|
+
available_bytes = if available_kb
|
|
48
|
+
available_kb * 1024
|
|
49
|
+
else
|
|
50
|
+
free_bytes + buffers_bytes + cached_bytes
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
used_bytes = [total_bytes - available_bytes, 0].max
|
|
54
|
+
|
|
55
|
+
MemoryStats.new(
|
|
56
|
+
total_bytes: total_bytes,
|
|
57
|
+
free_bytes: free_bytes,
|
|
58
|
+
available_bytes: available_bytes,
|
|
59
|
+
used_bytes: used_bytes,
|
|
60
|
+
details: {
|
|
61
|
+
buffers_bytes: buffers_bytes,
|
|
62
|
+
cached_bytes: cached_bytes,
|
|
63
|
+
swap_total_bytes: (meminfo["SwapTotal"] || 0) * 1024,
|
|
64
|
+
swap_free_bytes: (meminfo["SwapFree"] || 0) * 1024
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Collect System statistics on Linux
|
|
70
|
+
def system
|
|
71
|
+
os_name, os_ver = parse_os_release
|
|
72
|
+
arch = execute("uname -m")
|
|
73
|
+
arch = "unknown" if arch.empty?
|
|
74
|
+
|
|
75
|
+
kernel = execute("uname -r")
|
|
76
|
+
kernel = "unknown" if kernel.empty?
|
|
77
|
+
|
|
78
|
+
uptime_secs = parse_uptime
|
|
79
|
+
|
|
80
|
+
SystemStats.new(
|
|
81
|
+
os: os_name,
|
|
82
|
+
os_family: :linux,
|
|
83
|
+
os_version: os_ver,
|
|
84
|
+
hostname: hostname,
|
|
85
|
+
architecture: arch,
|
|
86
|
+
kernel: kernel,
|
|
87
|
+
uptime_seconds: uptime_secs
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Internal helper: parse /proc/meminfo
|
|
92
|
+
def parse_meminfo
|
|
93
|
+
return {} unless File.exist?(@proc_meminfo_path)
|
|
94
|
+
|
|
95
|
+
info = {}
|
|
96
|
+
File.foreach(@proc_meminfo_path) do |line|
|
|
97
|
+
if (m = line.match(/^(\w+):\s+(\d+)\s+kB/))
|
|
98
|
+
info[m[1]] = m[2].to_i
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
info
|
|
102
|
+
rescue StandardError
|
|
103
|
+
{}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Internal helper: parse /proc/loadavg
|
|
107
|
+
def parse_load_average
|
|
108
|
+
return { one_minute: 0.0, five_minutes: 0.0, fifteen_minutes: 0.0 } unless File.exist?(@proc_loadavg_path)
|
|
109
|
+
|
|
110
|
+
parts = File.read(@proc_loadavg_path).strip.split(/\s+/)
|
|
111
|
+
{
|
|
112
|
+
one_minute: parts[0].to_f,
|
|
113
|
+
five_minutes: parts[1].to_f,
|
|
114
|
+
fifteen_minutes: parts[2].to_f
|
|
115
|
+
}
|
|
116
|
+
rescue StandardError
|
|
117
|
+
{ one_minute: 0.0, five_minutes: 0.0, fifteen_minutes: 0.0 }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Internal helper: parse /proc/uptime
|
|
121
|
+
def parse_uptime
|
|
122
|
+
return 0 unless File.exist?(@proc_uptime_path)
|
|
123
|
+
|
|
124
|
+
parts = File.read(@proc_uptime_path).strip.split(/\s+/)
|
|
125
|
+
parts[0].to_f.to_i
|
|
126
|
+
rescue StandardError
|
|
127
|
+
0
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Internal helper: parse /proc/cpuinfo
|
|
131
|
+
def parse_cpuinfo
|
|
132
|
+
return [nil, nil] unless File.exist?(@proc_cpuinfo_path)
|
|
133
|
+
|
|
134
|
+
model = nil
|
|
135
|
+
physical_core_pairs = {}
|
|
136
|
+
current_phys_id = nil
|
|
137
|
+
|
|
138
|
+
File.foreach(@proc_cpuinfo_path) do |line|
|
|
139
|
+
line = line.strip
|
|
140
|
+
if line.start_with?("model name")
|
|
141
|
+
model ||= line.split(":", 2)[1]&.strip
|
|
142
|
+
elsif line.start_with?("physical id")
|
|
143
|
+
current_phys_id = line.split(":", 2)[1]&.strip
|
|
144
|
+
elsif line.start_with?("core id") && current_phys_id
|
|
145
|
+
core_id = line.split(":", 2)[1]&.strip
|
|
146
|
+
physical_core_pairs["#{current_phys_id}-#{core_id}"] = true
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
phys_count = physical_core_pairs.empty? ? nil : physical_core_pairs.size
|
|
151
|
+
[model, phys_count]
|
|
152
|
+
rescue StandardError
|
|
153
|
+
[nil, nil]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Internal helper: read first CPU line from /proc/stat
|
|
157
|
+
def read_cpu_ticks
|
|
158
|
+
return nil unless File.exist?(@proc_stat_path)
|
|
159
|
+
|
|
160
|
+
File.foreach(@proc_stat_path) do |line|
|
|
161
|
+
if line.start_with?("cpu ")
|
|
162
|
+
# cpu user nice system idle iowait irq softirq steal guest guest_nice
|
|
163
|
+
fields = line.strip.split(/\s+/)[1..-1].map(&:to_i)
|
|
164
|
+
idle = (fields[3] || 0) + (fields[4] || 0) # idle + iowait
|
|
165
|
+
total = fields.inject(0, :+)
|
|
166
|
+
return { idle: idle, total: total }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
nil
|
|
170
|
+
rescue StandardError
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Internal helper: calculate CPU % using sample interval
|
|
175
|
+
def calculate_cpu_usage(interval)
|
|
176
|
+
sample1 = read_cpu_ticks
|
|
177
|
+
return 0.0 unless sample1
|
|
178
|
+
|
|
179
|
+
if interval > 0.0
|
|
180
|
+
sleep(interval)
|
|
181
|
+
sample2 = read_cpu_ticks
|
|
182
|
+
return 0.0 unless sample2
|
|
183
|
+
|
|
184
|
+
delta_total = sample2[:total] - sample1[:total]
|
|
185
|
+
delta_idle = sample2[:idle] - sample1[:idle]
|
|
186
|
+
|
|
187
|
+
if delta_total.positive?
|
|
188
|
+
usage = ((delta_total - delta_idle).to_f / delta_total) * 100.0
|
|
189
|
+
return [[usage, 0.0].max, 100.0].min
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
0.0
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Internal helper: parse /etc/os-release
|
|
197
|
+
def parse_os_release
|
|
198
|
+
return ["Linux", "unknown"] unless File.exist?(@os_release_path)
|
|
199
|
+
|
|
200
|
+
env = {}
|
|
201
|
+
File.foreach(@os_release_path) do |line|
|
|
202
|
+
line = line.strip
|
|
203
|
+
next if line.empty? || line.start_with?("#")
|
|
204
|
+
|
|
205
|
+
if (m = line.match(/^([A-Za-z_]+)=(?:"([^"]*)"|'([^']*)'|(.*))$/))
|
|
206
|
+
key = m[1]
|
|
207
|
+
val = m[2] || m[3] || m[4]
|
|
208
|
+
env[key] = val
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
name = env["PRETTY_NAME"] || env["NAME"] || "Linux"
|
|
213
|
+
version = env["VERSION_ID"] || env["VERSION"] || "unknown"
|
|
214
|
+
[name, version]
|
|
215
|
+
rescue StandardError
|
|
216
|
+
["Linux", "unknown"]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|