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