Introduce incidenti con dedup, job Sidekiq ogni 3 min, scan log cron, /up/deep, dashboard /admin/ops e push ntfy; include Sentry opzionale e fix test suite. Co-authored-by: Cursor <cursoragent@cursor.com>
189 lines
5.0 KiB
Ruby
189 lines
5.0 KiB
Ruby
module Admin
|
|
class HostMetrics
|
|
RECORDINGS_PATH = ENV.fetch("RECORDINGS_PATH", "/recordings")
|
|
ROOT_PATH = ENV.fetch("ADMIN_DISK_ROOT_PATH", "/")
|
|
|
|
def sample!
|
|
cpu = read_cpu_percent
|
|
memory = read_memory
|
|
disk = read_disk
|
|
network = read_network
|
|
|
|
sample = {
|
|
at: Time.current.iso8601,
|
|
cpu: cpu,
|
|
mem: memory[:used_percent]
|
|
}
|
|
MetricsStore.push_sample(sample)
|
|
|
|
{
|
|
cpu: cpu,
|
|
memory: memory,
|
|
disk: disk,
|
|
network: network,
|
|
history: MetricsStore.history,
|
|
load_avg: read_load_avg
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def read_cpu_percent
|
|
a = cpu_jiffies
|
|
sleep 0.08
|
|
b = cpu_jiffies
|
|
return 0.0 unless a && b
|
|
|
|
idle_delta = b[:idle] - a[:idle]
|
|
total_delta = b[:total] - a[:total]
|
|
return 0.0 if total_delta <= 0
|
|
|
|
((1.0 - (idle_delta.to_f / total_delta)) * 100).round(1)
|
|
rescue StandardError
|
|
0.0
|
|
end
|
|
|
|
def cpu_jiffies
|
|
line = File.read("/proc/stat").each_line.find { |l| l.start_with?("cpu ") }
|
|
return nil unless line
|
|
|
|
parts = line.split[1..-1].map(&:to_i)
|
|
idle = parts[3] + (parts[4] || 0)
|
|
total = parts.sum
|
|
{ idle: idle, total: total }
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
def read_memory
|
|
info = {}
|
|
File.read("/proc/meminfo").each_line do |line|
|
|
key, value = line.split(":", 2)
|
|
info[key.strip] = value.to_i
|
|
end
|
|
total = info["MemTotal"] * 1024
|
|
available = (info["MemAvailable"] || info["MemFree"]) * 1024
|
|
used = [total - available, 0].max
|
|
{
|
|
total_bytes: total,
|
|
used_bytes: used,
|
|
available_bytes: available,
|
|
used_percent: total.positive? ? ((used.to_f / total) * 100).round(1) : 0.0
|
|
}
|
|
rescue StandardError
|
|
{ total_bytes: 0, used_bytes: 0, available_bytes: 0, used_percent: 0.0 }
|
|
end
|
|
|
|
def read_disk
|
|
{
|
|
root: disk_usage_for(ROOT_PATH),
|
|
recordings: disk_usage_for(RECORDINGS_PATH)
|
|
}
|
|
end
|
|
|
|
def disk_usage_for(path)
|
|
return unavailable_disk unless path.present? && (Dir.exist?(path) || File.exist?(path))
|
|
|
|
usage = disk_usage_via_statvfs(path) || disk_usage_via_df(path)
|
|
return unavailable_disk unless usage
|
|
|
|
usage.merge(dir_size_bytes: recordings_dir_size(path))
|
|
rescue StandardError
|
|
unavailable_disk
|
|
end
|
|
|
|
def disk_usage_via_statvfs(path)
|
|
return nil unless File.respond_to?(:statvfs)
|
|
|
|
stat = File.statvfs(path)
|
|
total = stat.blocks * stat.frsize
|
|
free = stat.bavail * stat.frsize
|
|
used = total - free
|
|
build_disk_usage(path, total: total, used: used, free: free)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
def disk_usage_via_df(path)
|
|
output = shell_out("df -B1 --output=size,used,avail,pcent,target #{Shellwords.escape(path)} 2>/dev/null").strip
|
|
line = output.lines.map(&:strip).reject(&:blank?).last
|
|
return nil if line.blank? || line.start_with?("Filesystem")
|
|
|
|
parts = line.split
|
|
return nil if parts.size < 5
|
|
|
|
total = parts[0].to_i
|
|
used = parts[1].to_i
|
|
free = parts[2].to_i
|
|
pct = parts[3].delete("%").to_f
|
|
build_disk_usage(path, total: total, used: used, free: free, used_percent: pct)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
def build_disk_usage(path, total:, used:, free:, used_percent: nil)
|
|
{
|
|
path: path,
|
|
total_bytes: total,
|
|
used_bytes: used,
|
|
free_bytes: free,
|
|
used_percent: used_percent || (total.positive? ? ((used.to_f / total) * 100).round(1) : 0.0)
|
|
}
|
|
end
|
|
|
|
def recordings_dir_size(path)
|
|
return nil unless path == RECORDINGS_PATH && Dir.exist?(path)
|
|
|
|
shell_out("du -sb #{Shellwords.escape(path)} 2>/dev/null").split.first.to_i
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
|
|
def unavailable_disk
|
|
{ path: nil, total_bytes: nil, used_bytes: nil, free_bytes: nil, used_percent: nil, dir_size_bytes: nil }
|
|
end
|
|
|
|
def shell_out(cmd)
|
|
`#{cmd}`
|
|
end
|
|
|
|
def read_network
|
|
rx, tx = parse_net_bytes
|
|
prev = MetricsStore.net_totals
|
|
MetricsStore.save_net_totals(rx: rx, tx: tx)
|
|
|
|
delta_rx = prev[:rx] ? [rx - prev[:rx], 0].max : 0
|
|
delta_tx = prev[:tx] ? [tx - prev[:tx], 0].max : 0
|
|
|
|
{
|
|
rx_bytes_total: rx,
|
|
tx_bytes_total: tx,
|
|
total_bytes: rx + tx
|
|
}
|
|
rescue StandardError
|
|
{ rx_bytes_total: 0, tx_bytes_total: 0, total_bytes: 0 }
|
|
end
|
|
|
|
def parse_net_bytes
|
|
rx = 0
|
|
tx = 0
|
|
File.read("/proc/net/dev").each_line.drop(2).each do |line|
|
|
iface = line.split(":").first.strip
|
|
next if iface == "lo"
|
|
|
|
cols = line.split(":").last.split.map(&:to_i)
|
|
rx += cols[0]
|
|
tx += cols[8]
|
|
end
|
|
[rx, tx]
|
|
end
|
|
|
|
def read_load_avg
|
|
loads = File.read("/proc/loadavg").split[0..2].map(&:to_f)
|
|
{ "1m" => loads[0], "5m" => loads[1], "15m" => loads[2] }
|
|
rescue StandardError
|
|
{ "1m" => 0.0, "5m" => 0.0, "15m" => 0.0 }
|
|
end
|
|
end
|
|
end
|