diff --git a/app/models/bandwidth_usage.rb b/app/models/bandwidth_usage.rb new file mode 100644 index 0000000..32bb0a9 --- /dev/null +++ b/app/models/bandwidth_usage.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class BandwidthUsage < ActiveRecord::Base +end diff --git a/app/models/disk_io.rb b/app/models/disk_io.rb new file mode 100644 index 0000000..a55aef4 --- /dev/null +++ b/app/models/disk_io.rb @@ -0,0 +1,4 @@ +# # frozen_string_literal: true + +class DiskIO < ActiveRecord::Base +end diff --git a/app/puny_monitor.rb b/app/puny_monitor.rb index 5e82d78..2f5356f 100644 --- a/app/puny_monitor.rb +++ b/app/puny_monitor.rb @@ -10,6 +10,8 @@ require_relative "models/cpu_load" require_relative "models/memory_usage" require_relative "models/filesystem_usage" +require_relative "models/disk_io" +require_relative "models/bandwidth_usage" require_relative "../lib/system_utils" require_relative "../config/application" @@ -63,10 +65,44 @@ class App < Sinatra::Base filesystem_usage.to_json end + get "/data/disk_io" do + content_type :json + end_time = Time.now + start_time = end_time - 1.hour + [ + { name: "Read MB/s", data: DiskIO.where(created_at: start_time..end_time) + .group_by_minute(:created_at, n: 1, series: true) + .average(:read_mb_per_sec) }, + { name: "Write MB/s", data: DiskIO.where(created_at: start_time..end_time) + .group_by_minute(:created_at, n: 1, series: true) + .average(:write_mb_per_sec) } + ].to_json + end + + get "/data/bandwidth" do + content_type :json + end_time = Time.now + start_time = end_time - 1.hour + [ + { name: "Incoming Mbps", data: BandwidthUsage.where(created_at: start_time..end_time) + .group_by_minute(:created_at, n: 1, series: true) + .average(:incoming_mbps) }, + { name: "Outgoing Mbps", data: BandwidthUsage.where(created_at: start_time..end_time) + .group_by_minute(:created_at, n: 1, series: true) + .average(:outgoing_mbps) } + ].to_json + end + @scheduler.every "5s" do - CpuLoad.create(load_average: SystemUtils.cpu_load_average) + CpuLoad.create(load_average: SystemUtils.cpu_usage_percent) MemoryUsage.create(used_percent: SystemUtils.memory_usage_percent) FilesystemUsage.create(used_percent: SystemUtils.filesystem_usage_percent) + + disk_io = SystemUtils.disk_io_stats + DiskIO.create(read_mb_per_sec: disk_io[:read_mb_per_sec], write_mb_per_sec: disk_io[:write_mb_per_sec]) + + bandwidth = SystemUtils.bandwidth_usage + BandwidthUsage.create(incoming_mbps: bandwidth[:incoming_mbps], outgoing_mbps: bandwidth[:outgoing_mbps]) end end end diff --git a/app/views/index.erb b/app/views/index.erb index a7d8f75..2e896ad 100644 --- a/app/views/index.erb +++ b/app/views/index.erb @@ -1,18 +1,30 @@

System Information

-

Recent CPU Loads

+

CPU Usage

<%= area_chart "/data/cpu", -ytitle: "Load Average", +ytitle: "CPU Usage (%)", +min: 0, +max: 100, +library: { + title: { + text: "CPU Usage", + }, +}, +refresh: 5 %> + +

Bandwidth Usage

+<%= line_chart "/data/bandwidth", +ytitle: "Bandwidth (Mbps)", library: { title: { - text: "CPU Load Over Time", + text: "Bandwidth Usage", }, }, refresh: 5 %>

Memory Usage

<%= line_chart "/data/memory", -ytitle: "Used Memory (%)", +ytitle: "Memory Usage (%)", min: 0, max: 100, library: { @@ -33,3 +45,13 @@ library: { }, }, refresh: 5 %> + +

Disk I/O

+<%= area_chart "/data/disk_io", +ytitle: "MB/s", +library: { + title: { + text: "Disk I/O", + }, +}, +refresh: 5 %> diff --git a/config/initializers/chartkick.rb b/config/initializers/chartkick.rb index 7818831..04dbafe 100644 --- a/config/initializers/chartkick.rb +++ b/config/initializers/chartkick.rb @@ -4,11 +4,5 @@ height: "400px", xtitle: "Time", points: false, - curve: false, - library: { - yAxis: { - min: 0, - max: 100 - } - } + curve: false } diff --git a/db/migrate/20230516000000_create_disk_ios.rb b/db/migrate/20230516000000_create_disk_ios.rb new file mode 100644 index 0000000..9c3056d --- /dev/null +++ b/db/migrate/20230516000000_create_disk_ios.rb @@ -0,0 +1,9 @@ +class CreateDiskIos < ActiveRecord::Migration[7.0] + def change + create_table :disk_ios do |t| + t.float :read_mb_per_sec + t.float :write_mb_per_sec + t.timestamps + end + end +end diff --git a/db/migrate/20230517000000_create_bandwidth_usages.rb b/db/migrate/20230517000000_create_bandwidth_usages.rb new file mode 100644 index 0000000..1f506c8 --- /dev/null +++ b/db/migrate/20230517000000_create_bandwidth_usages.rb @@ -0,0 +1,9 @@ +class CreateBandwidthUsages < ActiveRecord::Migration[7.0] + def change + create_table :bandwidth_usages do |t| + t.float :incoming_mbps + t.float :outgoing_mbps + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 59076e2..6c9f30f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,13 +10,27 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2023_05_15_123458) do +ActiveRecord::Schema[7.2].define(version: 2024_09_30_155845) do + create_table "bandwidth_usages", force: :cascade do |t| + t.float "incoming_mbps" + t.float "outgoing_mbps" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "cpu_loads", force: :cascade do |t| t.float "load_average" t.datetime "created_at", null: false t.datetime "updated_at", null: false end + create_table "disk_ios", force: :cascade do |t| + t.float "read_mb_per_sec" + t.float "write_mb_per_sec" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "filesystem_usages", force: :cascade do |t| t.float "used_percent" t.datetime "created_at", null: false diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..e69de29 diff --git a/lib/system_utils.rb b/lib/system_utils.rb index a3f3f1d..0f74812 100644 --- a/lib/system_utils.rb +++ b/lib/system_utils.rb @@ -2,9 +2,25 @@ module SystemUtils class << self - def cpu_load_average - load_avg_file = "#{proc_path}/loadavg" - File.readlines(load_avg_file).first.to_f + def cpu_usage_percent + prev_cpu = read_cpu_stat + sleep(1) + current_cpu = read_cpu_stat + + prev_idle = prev_cpu[:idle] + prev_cpu[:iowait] + idle = current_cpu[:idle] + current_cpu[:iowait] + + prev_non_idle = prev_cpu[:user] + prev_cpu[:nice] + prev_cpu[:system] + prev_cpu[:irq] + prev_cpu[:softirq] + prev_cpu[:steal] + non_idle = current_cpu[:user] + current_cpu[:nice] + current_cpu[:system] + current_cpu[:irq] + current_cpu[:softirq] + current_cpu[:steal] + + prev_total = prev_idle + prev_non_idle + total = idle + non_idle + + total_diff = total - prev_total + idle_diff = idle - prev_idle + + cpu_percentage = ((total_diff - idle_diff).to_f / total_diff * 100).round(2) + [cpu_percentage, 100.0].min end def memory_usage_percent @@ -15,9 +31,6 @@ def memory_usage_percent cached = mem_info.match(/Cached:\s+(\d+)/)[1].to_f used = total - free - buffers - cached (used / total * 100).round(2) - rescue StandardError => e - puts "Error reading memory usage: #{e.message}" - 0.0 end def filesystem_usage_percent(mount_point = "/") @@ -27,9 +40,39 @@ def filesystem_usage_percent(mount_point = "/") used_blocks = total_blocks - available_blocks used_percent = (used_blocks.to_f / total_blocks * 100).round(2) [used_percent, 100.0].min - rescue StandardError => e - puts "Error reading filesystem usage: #{e.message}" - 0.0 + end + + def disk_io_stats + prev_stats = read_disk_stats + sleep(1) + curr_stats = read_disk_stats + + read_sectors = curr_stats[:read_sectors] - prev_stats[:read_sectors] + write_sectors = curr_stats[:write_sectors] - prev_stats[:write_sectors] + + sector_size = 512 + read_mb_per_sec = (read_sectors * sector_size / 1_048_576.0).round(2) + write_mb_per_sec = (write_sectors * sector_size / 1_048_576.0).round(2) + + { + read_mb_per_sec:, + write_mb_per_sec: + } + end + + def bandwidth_usage + prev_stats = read_network_stats + sleep(1) + curr_stats = read_network_stats + + incoming_bytes = curr_stats[:rx_bytes] - prev_stats[:rx_bytes] + outgoing_bytes = curr_stats[:tx_bytes] - prev_stats[:tx_bytes] + + bytes_to_mbits = 8.0 / 1_000_000 # Convert bytes to megabits + { + incoming_mbps: (incoming_bytes * bytes_to_mbits).round(2), + outgoing_mbps: (outgoing_bytes * bytes_to_mbits).round(2) + } end private @@ -37,5 +80,55 @@ def filesystem_usage_percent(mount_point = "/") def proc_path ENV.fetch("PROC_PATH", "/proc") end + + def read_cpu_stat + cpu_stats = File.read("#{proc_path}/stat").lines.first.split(/\s+/) + { + user: cpu_stats[1].to_i, + nice: cpu_stats[2].to_i, + system: cpu_stats[3].to_i, + idle: cpu_stats[4].to_i, + iowait: cpu_stats[5].to_i, + irq: cpu_stats[6].to_i, + softirq: cpu_stats[7].to_i, + steal: cpu_stats[8].to_i + } + end + + def read_disk_stats + primary_disk = File.read("#{proc_path}/partitions") + .lines + .drop(2) + .first + .split + .last + + stats = File.read("#{proc_path}/diskstats") + .lines + .map(&:split) + .find { |line| line[2] == primary_disk } + + { + read_sectors: stats[5].to_i, + write_sectors: stats[9].to_i + } + end + + def read_network_stats + primary_interface = File.read("#{proc_path}/net/route") + .lines + .drop(1) + .find { |line| line.split[1] == "00000000" } + &.split&.first + stats = File.read("#{proc_path}/net/dev") + .lines + .map(&:split) + .find { |line| line[0].chomp(":") == primary_interface } + + { + rx_bytes: stats[1].to_i, + tx_bytes: stats[9].to_i + } + end end end