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)) stat = File.statvfs(path) total = stat.blocks * stat.frsize free = stat.bavail * stat.frsize used = total - free { path: path, total_bytes: total, used_bytes: used, free_bytes: free, used_percent: total.positive? ? ((used.to_f / total) * 100).round(1) : 0.0, dir_size_bytes: recordings_dir_size(path) } rescue StandardError unavailable_disk end def recordings_dir_size(path) return nil unless path == RECORDINGS_PATH && Dir.exist?(path) `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 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