puny-monitor 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/.ruby-version +1 -0
- data/LICENSE +21 -0
- data/README.md +96 -0
- data/Rakefile +50 -0
- data/app/models/bandwidth.rb +21 -0
- data/app/models/cpu_load.rb +18 -0
- data/app/models/cpu_usage.rb +10 -0
- data/app/models/disk_io.rb +21 -0
- data/app/models/filesystem_usage.rb +10 -0
- data/app/models/memory_usage.rb +10 -0
- data/app/puny_monitor.rb +92 -0
- data/app/scheduler.rb +33 -0
- data/app/views/index.erb +96 -0
- data/app/views/layout.erb +46 -0
- data/config/database.yml +16 -0
- data/config/environment.rb +17 -0
- data/config/initializers/chartkick.rb +7 -0
- data/config.ru +5 -0
- data/db/migrate/20231023000000_add_indices_to_created_at_columns.rb +12 -0
- data/db/schema.rb +61 -0
- data/db/seeds.rb +3 -0
- data/exe/puny-monitor +8 -0
- data/lib/puny_monitor/version.rb +5 -0
- data/lib/puny_monitor.rb +4 -0
- data/lib/system_utils.rb +148 -0
- data/public/favicon.ico +0 -0
- data/public/fonts/Rubik.woff2 +0 -0
- data/public/icon-512.png +0 -0
- data/public/icon.svg +23 -0
- data/public/javascript/index.js +4 -0
- data/public/style.css +161 -0
- metadata +206 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ef7da3ad71ee116621cfd11bc930e6ab5624aad481afaa024e6e09790916cde4
|
4
|
+
data.tar.gz: d590745c16a3ca947e1a1af8b91931cbf9daec21e9f1703a308b8be7e92e5bf5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 345ed87c689aa7c153009f8a3f21fa33778ed97cf1db0a3e60948717ee0906886c4d9eb037a6b4602567354a34c89378f9fa57fb7b1f4a3596024f1580f2b687
|
7
|
+
data.tar.gz: af40c578d0c669f4216e0eaaa456154835819934c79f6befb40ea9200e888eba3854bef9594278644718ab242bc0c9804c862f8bdbca02d1df323ebeae6892e3
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.3.4
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Hans Schnedlitz
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
<div align="center">
|
2
|
+
|
3
|
+
# Puny Monitor
|
4
|
+
|
5
|
+
<img alt="logo" src="/public/icon-512.png" width="256" height="auto">
|
6
|
+
|
7
|
+
### A batteries-included monitoring tool for single hosts.
|
8
|
+
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<p align="center">
|
12
|
+
<img alt="Screenshot of Puny Monitor" src="screenshot.png" width="90%">
|
13
|
+
</p>
|
14
|
+
|
15
|
+
## Features
|
16
|
+
|
17
|
+
- Just enough data to be useful 🔍
|
18
|
+
- Install in 30 seconds 🏎️
|
19
|
+
- Perfect for [Kamal](https://kamal-deploy.org/) and other containerized setups 🐋
|
20
|
+
|
21
|
+
|
22
|
+
## Getting Started
|
23
|
+
|
24
|
+
Puny Monitor works best with Docker. Run this command to check it out quickly:
|
25
|
+
|
26
|
+
```
|
27
|
+
docker run --rm \
|
28
|
+
-v=/:/host:ro,rslave -v=puny-data:/puny-monitor/db \
|
29
|
+
-e HOST_PATH=/host \
|
30
|
+
-p 4567:4567 \
|
31
|
+
hschne/puny-monitor:latest
|
32
|
+
```
|
33
|
+
|
34
|
+
Visit [localhost:4567](http://localhost:4567) to check your system data. To see how to deploy Puny Monitor in a production environment see [Deployment].
|
35
|
+
|
36
|
+
## Deployment
|
37
|
+
|
38
|
+
Puny Monitor was made with [Kamal](https://kamal-deploy.org/) and [Ruby on Rails](https://rubyonrails.org/) in mind. It is recommended that you deploy it as an accessory to your application. Add the following lines to `config/deploy.yml`:
|
39
|
+
|
40
|
+
```
|
41
|
+
accessories:
|
42
|
+
puny-monitor:
|
43
|
+
image: hschne/puny-monitor:latest
|
44
|
+
host: <host>
|
45
|
+
port: "127.0.0.1:4567:4567"
|
46
|
+
volumes:
|
47
|
+
- /:/host:ro,rslave
|
48
|
+
- puny-monitor-data:/puny-monitor/db
|
49
|
+
|
50
|
+
aliases:
|
51
|
+
add-puny-monitor-to-proxy: |
|
52
|
+
server exec docker exec kamal-proxy kamal-proxy deploy puny-monitor
|
53
|
+
--target "<your-service-name>-puny-monitor:4567"
|
54
|
+
--host "puny-monitor.<your-domain>"
|
55
|
+
--tls
|
56
|
+
```
|
57
|
+
|
58
|
+
Then run `kamal-proxy` to point to Puny Monitor:
|
59
|
+
|
60
|
+
```
|
61
|
+
kamal add-puny-monitor-to-proxy
|
62
|
+
```
|
63
|
+
|
64
|
+
### Other Deployment Options
|
65
|
+
|
66
|
+
You may install the Puny Monitor gem and run the application from the command line.
|
67
|
+
|
68
|
+
```bash
|
69
|
+
gem install puny-monitor
|
70
|
+
# Run puny monitor on port 4567
|
71
|
+
puny-monitor
|
72
|
+
```
|
73
|
+
|
74
|
+
## Why Puny Monitor?
|
75
|
+
|
76
|
+
Puny Monitor aims to be a dead-simple, no-frills monitoring solution for single hosts. It provides enough information to be useful (and not a bit more) and avoids the complications and overhead that come with existing solutions.
|
77
|
+
|
78
|
+
To put it simply, Puny Monitor replicates [Digital Ocean's Monitoring](https://www.digitalocean.com/products/monitoring) but runs on any ol' VPS or metal server you might have lying around. It is the perfect solution for IndieHackers who use Rails & Kamal, but works beautifully for anyone that wants some useful monitoring quickly.
|
79
|
+
|
80
|
+
## Development
|
81
|
+
|
82
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
83
|
+
|
84
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
85
|
+
|
86
|
+
## Contributing
|
87
|
+
|
88
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/puny-monitor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/puny-monitor/blob/main/CODE_OF_CONDUCT.md).
|
89
|
+
|
90
|
+
## License
|
91
|
+
|
92
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
93
|
+
|
94
|
+
## Code of Conduct
|
95
|
+
|
96
|
+
Everyone interacting in the Puny::Monitor project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/puny-monitor/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
|
5
|
+
require_relative "config/environment"
|
6
|
+
|
7
|
+
require "sinatra/activerecord/rake"
|
8
|
+
|
9
|
+
if PunyMonitor::App.development?
|
10
|
+
|
11
|
+
require "minitest/test_task"
|
12
|
+
Minitest::TestTask.create
|
13
|
+
|
14
|
+
require "rubocop/rake_task"
|
15
|
+
RuboCop::RakeTask.new
|
16
|
+
|
17
|
+
namespace :docker do
|
18
|
+
desc "Build Puny Monitor Docker image"
|
19
|
+
task :build do
|
20
|
+
sh "docker build -t hschne/puny-monitor:latest ."
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Push Puny Monitor Docker image"
|
24
|
+
task :push do
|
25
|
+
sh "docker push -a hschne/puny-monitor"
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Run Docker container"
|
29
|
+
task :run do
|
30
|
+
`docker run --rm \
|
31
|
+
-v=/:/host:ro,rslave -v=puny-data:/puny-monitor/db \
|
32
|
+
-e ROOT_PATH=/host \
|
33
|
+
-p 80:4567 \
|
34
|
+
hschne/puny-monitor:latest`
|
35
|
+
end
|
36
|
+
|
37
|
+
desc "Run Docker interactive shell"
|
38
|
+
task :shell do
|
39
|
+
`docker run --rm \
|
40
|
+
-v=/:/host:ro,rslave -v=puny-data:/puny-monitor/db \
|
41
|
+
-e ROOT_PATH=/host \
|
42
|
+
-p 80:4567 \
|
43
|
+
-it \
|
44
|
+
hschne/puny-monitor:latest \
|
45
|
+
/bin/bash`
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
task default: %i[test rubocop]
|
50
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Bandwidth < ActiveRecord::Base
|
4
|
+
class << self
|
5
|
+
def average_usage(start_time, group_by)
|
6
|
+
[
|
7
|
+
{ name: "Incoming Mbps", data: average_for_period(:incoming_mbps, start_time, group_by) },
|
8
|
+
{ name: "Outgoing Mbps", data: average_for_period(:outgoing_mbps, start_time, group_by) }
|
9
|
+
]
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def average_for_period(column, start_time, group_by)
|
15
|
+
where(created_at: start_time..)
|
16
|
+
.group_by_period(group_by, :created_at)
|
17
|
+
.average(column)
|
18
|
+
.transform_values { |value| value&.round(2) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CpuLoad < ActiveRecord::Base
|
4
|
+
def self.average_load(start_time, end_time, group_by)
|
5
|
+
[
|
6
|
+
{ name: "1 minute", data: average_for_period(:one_minute, start_time, end_time, group_by) },
|
7
|
+
{ name: "5 minutes", data: average_for_period(:five_minutes, start_time, end_time, group_by) },
|
8
|
+
{ name: "15 minutes", data: average_for_period(:fifteen_minutes, start_time, end_time, group_by) }
|
9
|
+
]
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.average_for_period(column, start_time, end_time, group_by)
|
13
|
+
where(created_at: start_time..end_time)
|
14
|
+
.group_by_period(group_by, :created_at)
|
15
|
+
.average(column)
|
16
|
+
.transform_values { |value| value&.round(2) }
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CpuUsage < ActiveRecord::Base
|
4
|
+
def self.average_usage(start_time, group_by)
|
5
|
+
where(created_at: start_time..)
|
6
|
+
.group_by_period(group_by, :created_at, expand_range: true)
|
7
|
+
.average(:used_percent)
|
8
|
+
.transform_values { |value| value&.round(2) }
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class DiskIO < ActiveRecord::Base
|
4
|
+
class << self
|
5
|
+
def average_io(start_time, group_by)
|
6
|
+
[
|
7
|
+
{ name: "Read MB/s", data: average_for_period(:read_mb_per_sec, start_time, group_by) },
|
8
|
+
{ name: "Write MB/s", data: average_for_period(:write_mb_per_sec, start_time, group_by) }
|
9
|
+
]
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def average_for_period(column, start_time, group_by)
|
15
|
+
where(created_at: start_time..)
|
16
|
+
.group_by_period(group_by, :created_at)
|
17
|
+
.average(column)
|
18
|
+
.transform_values { |value| value&.round(2) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class FilesystemUsage < ActiveRecord::Base
|
4
|
+
def self.average_usage(start_time, group_by)
|
5
|
+
where(created_at: start_time..)
|
6
|
+
.group_by_period(group_by, :created_at)
|
7
|
+
.average(:used_percent)
|
8
|
+
.transform_values { |value| value&.round(2) }
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class MemoryUsage < ActiveRecord::Base
|
4
|
+
def self.average_usage(start_time, group_by)
|
5
|
+
where(created_at: start_time..)
|
6
|
+
.group_by_period(group_by, :created_at)
|
7
|
+
.average(:used_percent)
|
8
|
+
.transform_values { |value| value&.round(2) }
|
9
|
+
end
|
10
|
+
end
|
data/app/puny_monitor.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rufus-scheduler"
|
4
|
+
require_relative "scheduler"
|
5
|
+
require_relative "../lib/system_utils"
|
6
|
+
|
7
|
+
module PunyMonitor
|
8
|
+
class App < Sinatra::Base
|
9
|
+
configure do
|
10
|
+
register Sinatra::ActiveRecordExtension
|
11
|
+
@scheduler = Rufus::Scheduler.new
|
12
|
+
@scheduler.every("5s") { Scheduler.collect_data }
|
13
|
+
@scheduler.every("1h") { Scheduler.cleanup_old_data }
|
14
|
+
end
|
15
|
+
|
16
|
+
configure :development do
|
17
|
+
register Sinatra::Reloader
|
18
|
+
end
|
19
|
+
|
20
|
+
set :erb, layout: :layout
|
21
|
+
set :public_folder, File.join(__dir__, "..", "public")
|
22
|
+
set :database_file, "../config/database.yml"
|
23
|
+
|
24
|
+
get "/" do
|
25
|
+
erb :index, locals: { params:, logo: }
|
26
|
+
end
|
27
|
+
|
28
|
+
get "/up" do
|
29
|
+
200
|
30
|
+
end
|
31
|
+
|
32
|
+
get "/data/cpu_usage" do
|
33
|
+
content_type :json
|
34
|
+
CpuUsage.average_usage(start_time, group_by).to_json
|
35
|
+
end
|
36
|
+
|
37
|
+
get "/data/cpu_load" do
|
38
|
+
content_type :json
|
39
|
+
CpuLoad.average_load(start_time, Time.now, group_by).to_json
|
40
|
+
end
|
41
|
+
|
42
|
+
get "/data/memory_usage" do
|
43
|
+
content_type :json
|
44
|
+
MemoryUsage.average_usage(start_time, group_by).to_json
|
45
|
+
end
|
46
|
+
|
47
|
+
get "/data/filesystem_usage" do
|
48
|
+
content_type :json
|
49
|
+
FilesystemUsage.average_usage(start_time, group_by).to_json
|
50
|
+
end
|
51
|
+
|
52
|
+
get "/data/disk_io" do
|
53
|
+
content_type :json
|
54
|
+
DiskIO.average_io(start_time, group_by).to_json
|
55
|
+
end
|
56
|
+
|
57
|
+
get "/data/bandwidth" do
|
58
|
+
content_type :json
|
59
|
+
Bandwidth.average_usage(start_time, group_by).to_json
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def logo
|
65
|
+
@logo ||= begin
|
66
|
+
file = File.open("public/icon.svg")
|
67
|
+
file.read
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def duration
|
72
|
+
params[:duration] || "1d"
|
73
|
+
end
|
74
|
+
|
75
|
+
def start_time
|
76
|
+
case duration
|
77
|
+
when "1h" then 1.hour.ago
|
78
|
+
when "3d" then 3.days.ago
|
79
|
+
when "1w" then 1.week.ago
|
80
|
+
when "1m" then 1.month.ago
|
81
|
+
else 1.day.ago
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def group_by
|
86
|
+
case duration
|
87
|
+
when "1h", "1d" then :minute
|
88
|
+
else :hour
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/app/scheduler.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../lib/system_utils"
|
4
|
+
require "debug"
|
5
|
+
|
6
|
+
module PunyMonitor
|
7
|
+
class Scheduler
|
8
|
+
class << self
|
9
|
+
def collect_data
|
10
|
+
CpuUsage.create(used_percent: SystemUtils.cpu_usage_percent)
|
11
|
+
cpu_load_averages = SystemUtils.cpu_load_average
|
12
|
+
CpuLoad.create(one_minute: cpu_load_averages[0],
|
13
|
+
five_minutes: cpu_load_averages[1],
|
14
|
+
fifteen_minutes: cpu_load_averages[2])
|
15
|
+
MemoryUsage.create(used_percent: SystemUtils.memory_usage_percent)
|
16
|
+
FilesystemUsage.create(used_percent: SystemUtils.filesystem_usage_percent)
|
17
|
+
|
18
|
+
disk_io = SystemUtils.disk_io_stats
|
19
|
+
DiskIO.create(read_mb_per_sec: disk_io[:read_mb_per_sec], write_mb_per_sec: disk_io[:write_mb_per_sec])
|
20
|
+
|
21
|
+
bandwidth = SystemUtils.bandwidth_usage
|
22
|
+
Bandwidth.create(incoming_mbps: bandwidth[:incoming_mbps], outgoing_mbps: bandwidth[:outgoing_mbps])
|
23
|
+
end
|
24
|
+
|
25
|
+
def cleanup_old_data
|
26
|
+
one_month_ago = 1.month.ago
|
27
|
+
[CpuUsage, CpuLoad, MemoryUsage, FilesystemUsage, DiskIO, Bandwidth].each do |model|
|
28
|
+
model.where("created_at < ?", one_month_ago).delete_all
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/app/views/index.erb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
<section class="controls">
|
2
|
+
<form action="/" method="get">
|
3
|
+
<select name="duration" onchange="this.form.submit()">
|
4
|
+
<option value="1h" <%= params[:duration] == '1h' ? 'selected' : '' %>>1 Hour</option>
|
5
|
+
<option
|
6
|
+
value="1d"
|
7
|
+
<%= params[:duration] == '1d' || params[:duration].nil? ? 'selected' : '' %>
|
8
|
+
>1 Day</option>
|
9
|
+
<option value="3d" <%= params[:duration] == '3d' ? 'selected' : '' %>>3 Days</option>
|
10
|
+
<option value="1w" <%= params[:duration] == '1w' ? 'selected' : '' %>>1 Week</option>
|
11
|
+
<option value="1m" <%= params[:duration] == '1m' ? 'selected' : '' %>>1 Month</option>
|
12
|
+
</select>
|
13
|
+
</form>
|
14
|
+
</section>
|
15
|
+
|
16
|
+
<section class="charts">
|
17
|
+
|
18
|
+
<div class="tile">
|
19
|
+
<h2>CPU Usage</h2>
|
20
|
+
<%= area_chart "/data/cpu_usage?duration=#{params[:duration] || "1d"}",
|
21
|
+
ytitle: "CPU Usage (%)",
|
22
|
+
min: 0,
|
23
|
+
max: 100,
|
24
|
+
library: {
|
25
|
+
title: {
|
26
|
+
text: "CPU Usage",
|
27
|
+
},
|
28
|
+
},
|
29
|
+
refresh: 5 %>
|
30
|
+
</div>
|
31
|
+
|
32
|
+
<div class="tile">
|
33
|
+
<h2>CPU Load</h2>
|
34
|
+
<%= line_chart "/data/cpu_load?duration=#{params[:duration] || "1d"}",
|
35
|
+
ytitle: "Load Average",
|
36
|
+
library: {
|
37
|
+
title: {
|
38
|
+
text: "Load Average",
|
39
|
+
},
|
40
|
+
},
|
41
|
+
refresh: 5 %>
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<div class="tile">
|
45
|
+
<h2>Memory Usage</h2>
|
46
|
+
<%= area_chart "/data/memory_usage?duration=#{params[:duration] || "1d"}",
|
47
|
+
ytitle: "Memory Usage (%)",
|
48
|
+
min: 0,
|
49
|
+
max: 100,
|
50
|
+
library: {
|
51
|
+
title: {
|
52
|
+
text: "Memory Usage",
|
53
|
+
},
|
54
|
+
},
|
55
|
+
refresh: 5 %>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<div class="tile">
|
59
|
+
<h2>Filesystem Usage</h2>
|
60
|
+
<%= area_chart "/data/filesystem_usage?duration=#{params[:duration] || "1d"}",
|
61
|
+
ytitle: "Used Space (%)",
|
62
|
+
min: 0,
|
63
|
+
max: 100,
|
64
|
+
library: {
|
65
|
+
title: {
|
66
|
+
text: "Filesystem Usage",
|
67
|
+
},
|
68
|
+
},
|
69
|
+
refresh: 5 %>
|
70
|
+
</div>
|
71
|
+
|
72
|
+
<div class="tile">
|
73
|
+
<h2>Disk I/O</h2>
|
74
|
+
<%= area_chart "/data/disk_io?duration=#{params[:duration] || "1d"}",
|
75
|
+
ytitle: "MB/s",
|
76
|
+
library: {
|
77
|
+
title: {
|
78
|
+
text: "Disk I/O",
|
79
|
+
},
|
80
|
+
},
|
81
|
+
refresh: 5 %>
|
82
|
+
</div>
|
83
|
+
|
84
|
+
<div class="tile">
|
85
|
+
<h2>Bandwidth</h2>
|
86
|
+
<%= area_chart "/data/bandwidth?duration=#{params[:duration] || "1d"}",
|
87
|
+
ytitle: "Bandwidth (Mbps)",
|
88
|
+
library: {
|
89
|
+
title: {
|
90
|
+
text: "Bandwidth",
|
91
|
+
},
|
92
|
+
},
|
93
|
+
refresh: 5 %>
|
94
|
+
</div>
|
95
|
+
|
96
|
+
</section>
|
@@ -0,0 +1,46 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
7
|
+
<title>Puny Monitor - Lightweight System Monitoring</title>
|
8
|
+
<meta
|
9
|
+
name="description"
|
10
|
+
content="Puny Monitor is a lightweight system monitoring tool that tracks CPU usage, memory usage, disk I/O, and network bandwidth."
|
11
|
+
>
|
12
|
+
<meta name="robots" content="noindex">
|
13
|
+
<link rel="icon" href="favicon.ico" sizes="32x32">
|
14
|
+
<link rel="icon" href="icon.svg" type="image/svg+xml">
|
15
|
+
|
16
|
+
<script
|
17
|
+
src="https://cdn.jsdelivr.net/npm/chartkick@4.2.0/dist/chartkick.min.js"
|
18
|
+
></script>
|
19
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js"></script>
|
20
|
+
<script
|
21
|
+
src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@2.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"
|
22
|
+
></script>
|
23
|
+
<link
|
24
|
+
rel="preload"
|
25
|
+
href="fonts/Rubik.woff2"
|
26
|
+
as="font"
|
27
|
+
type="font/woff2"
|
28
|
+
crossorigin
|
29
|
+
>
|
30
|
+
<link href="style.css" rel="stylesheet">
|
31
|
+
|
32
|
+
</head>
|
33
|
+
<body>
|
34
|
+
<header>
|
35
|
+
<%= logo %>
|
36
|
+
<h1>Puny Monitor</h1>
|
37
|
+
</header>
|
38
|
+
<main>
|
39
|
+
<%= yield %>
|
40
|
+
</main>
|
41
|
+
<footer>
|
42
|
+
Found an issue or need a feature? File an issue on
|
43
|
+
<a href="https://github.com/hschne/puny-monitor">GitHub!</a>
|
44
|
+
</footer>
|
45
|
+
</body>
|
46
|
+
</html>
|
data/config/database.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
default: &default
|
2
|
+
adapter: sqlite3
|
3
|
+
pool: 5
|
4
|
+
timeout: 5000
|
5
|
+
|
6
|
+
development:
|
7
|
+
<<: *default
|
8
|
+
database: db/development.sqlite3
|
9
|
+
|
10
|
+
test:
|
11
|
+
<<: *default
|
12
|
+
database: db/test.sqlite3
|
13
|
+
|
14
|
+
production:
|
15
|
+
<<: *default
|
16
|
+
database: db/production.sqlite3
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV["RACK_ENV"] ||= "development"
|
4
|
+
|
5
|
+
require "bundler/setup"
|
6
|
+
Bundler.require(:default, ENV.fetch("RACK_ENV", nil))
|
7
|
+
|
8
|
+
require "sinatra/contrib"
|
9
|
+
require "sinatra/activerecord"
|
10
|
+
require "rufus-scheduler"
|
11
|
+
require "groupdate"
|
12
|
+
require "chartkick"
|
13
|
+
require "sqlite3"
|
14
|
+
require "sys-filesystem"
|
15
|
+
|
16
|
+
Dir["#{__dir__}/initializers/**/*.rb"].each { |file| require file }
|
17
|
+
Dir["#{__dir__}/../app/**/*.rb"].each { |file| require file }
|
data/config.ru
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AddIndicesToCreatedAtColumns < ActiveRecord::Migration[6.1]
|
4
|
+
def change
|
5
|
+
add_index :bandwidths, :created_at
|
6
|
+
add_index :cpu_loads, :created_at
|
7
|
+
add_index :cpu_usages, :created_at
|
8
|
+
add_index :disk_ios, :created_at
|
9
|
+
add_index :filesystem_usages, :created_at
|
10
|
+
add_index :memory_usages, :created_at
|
11
|
+
end
|
12
|
+
end
|
data/db/schema.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file is auto-generated from the current state of the database. Instead
|
4
|
+
# of editing this file, please use the migrations feature of Active Record to
|
5
|
+
# incrementally modify your database, and then regenerate this schema definition.
|
6
|
+
#
|
7
|
+
# This file is the source Rails uses to define your schema when running `bin/rails
|
8
|
+
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
|
9
|
+
# be faster and is potentially less error prone than running all of your
|
10
|
+
# migrations from scratch. Old migrations may fail to apply correctly if those
|
11
|
+
# migrations use external dependencies or application code.
|
12
|
+
#
|
13
|
+
# It's strongly recommended that you check this file into your version control system.
|
14
|
+
|
15
|
+
ActiveRecord::Schema[7.2].define(version: 20_240_930_155_845) do
|
16
|
+
create_table "bandwidths", force: :cascade do |t|
|
17
|
+
t.float "incoming_mbps"
|
18
|
+
t.float "outgoing_mbps"
|
19
|
+
t.datetime "created_at", null: false
|
20
|
+
t.datetime "updated_at", null: false
|
21
|
+
t.index ["created_at"], name: "index_bandwidths_on_created_at"
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table "cpu_loads", force: :cascade do |t|
|
25
|
+
t.float "one_minute"
|
26
|
+
t.float "five_minutes"
|
27
|
+
t.float "fifteen_minutes"
|
28
|
+
t.datetime "created_at", null: false
|
29
|
+
t.datetime "updated_at", null: false
|
30
|
+
t.index ["created_at"], name: "index_cpu_loads_on_created_at"
|
31
|
+
end
|
32
|
+
|
33
|
+
create_table "cpu_usages", force: :cascade do |t|
|
34
|
+
t.float "used_percent"
|
35
|
+
t.datetime "created_at", null: false
|
36
|
+
t.datetime "updated_at", null: false
|
37
|
+
t.index ["created_at"], name: "index_cpu_usages_on_created_at"
|
38
|
+
end
|
39
|
+
|
40
|
+
create_table "disk_ios", force: :cascade do |t|
|
41
|
+
t.float "read_mb_per_sec"
|
42
|
+
t.float "write_mb_per_sec"
|
43
|
+
t.datetime "created_at", null: false
|
44
|
+
t.datetime "updated_at", null: false
|
45
|
+
t.index ["created_at"], name: "index_disk_ios_on_created_at"
|
46
|
+
end
|
47
|
+
|
48
|
+
create_table "filesystem_usages", force: :cascade do |t|
|
49
|
+
t.float "used_percent"
|
50
|
+
t.datetime "created_at", null: false
|
51
|
+
t.datetime "updated_at", null: false
|
52
|
+
t.index ["created_at"], name: "index_filesystem_usages_on_created_at"
|
53
|
+
end
|
54
|
+
|
55
|
+
create_table "memory_usages", force: :cascade do |t|
|
56
|
+
t.float "used_percent"
|
57
|
+
t.datetime "created_at", null: false
|
58
|
+
t.datetime "updated_at", null: false
|
59
|
+
t.index ["created_at"], name: "index_memory_usages_on_created_at"
|
60
|
+
end
|
61
|
+
end
|
data/db/seeds.rb
ADDED
data/exe/puny-monitor
ADDED
data/lib/puny_monitor.rb
ADDED
data/lib/system_utils.rb
ADDED
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sys/filesystem"
|
4
|
+
|
5
|
+
class SystemUtils
|
6
|
+
class << self
|
7
|
+
def cpu_usage_percent
|
8
|
+
prev_cpu = read_cpu_stat
|
9
|
+
sleep(1)
|
10
|
+
current_cpu = read_cpu_stat
|
11
|
+
|
12
|
+
prev_idle = prev_cpu[:idle] + prev_cpu[:iowait]
|
13
|
+
idle = current_cpu[:idle] + current_cpu[:iowait]
|
14
|
+
|
15
|
+
prev_non_idle = prev_cpu[:user] + prev_cpu[:nice] + prev_cpu[:system] +
|
16
|
+
prev_cpu[:irq] + prev_cpu[:softirq] + prev_cpu[:steal]
|
17
|
+
non_idle = current_cpu[:user] + current_cpu[:nice] + current_cpu[:system] +
|
18
|
+
current_cpu[:irq] + current_cpu[:softirq] + current_cpu[:steal]
|
19
|
+
|
20
|
+
prev_total = prev_idle + prev_non_idle
|
21
|
+
total = idle + non_idle
|
22
|
+
|
23
|
+
total_diff = total - prev_total
|
24
|
+
idle_diff = idle - prev_idle
|
25
|
+
|
26
|
+
cpu_percentage = ((total_diff - idle_diff).to_f / total_diff * 100).round(2)
|
27
|
+
[cpu_percentage, 100.0].min.round(2)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cpu_load_average
|
31
|
+
File.read("#{proc_path}/loadavg").split.take(3)
|
32
|
+
.map(&:to_f)
|
33
|
+
.map { |value| value.round(2) }
|
34
|
+
end
|
35
|
+
|
36
|
+
def memory_usage_percent
|
37
|
+
mem_info = File.read("#{proc_path}/meminfo")
|
38
|
+
total = mem_info.match(/MemTotal:\s+(\d+)/)[1].to_f
|
39
|
+
free = mem_info.match(/MemFree:\s+(\d+)/)[1].to_f
|
40
|
+
buffers = mem_info.match(/Buffers:\s+(\d+)/)[1].to_f
|
41
|
+
cached = mem_info.match(/Cached:\s+(\d+)/)[1].to_f
|
42
|
+
used = total - free - buffers - cached
|
43
|
+
(used / total * 100).round(2)
|
44
|
+
end
|
45
|
+
|
46
|
+
def filesystem_usage_percent
|
47
|
+
stat = Sys::Filesystem.stat(root_path)
|
48
|
+
total_blocks = stat.blocks
|
49
|
+
available_blocks = stat.blocks_available
|
50
|
+
used_blocks = total_blocks - available_blocks
|
51
|
+
used_percent = (used_blocks.to_f / total_blocks * 100).round(2)
|
52
|
+
[used_percent, 100.0].min.round(2)
|
53
|
+
end
|
54
|
+
|
55
|
+
def disk_io_stats
|
56
|
+
prev_stats = read_disk_stats
|
57
|
+
sleep(1)
|
58
|
+
curr_stats = read_disk_stats
|
59
|
+
|
60
|
+
read_sectors = curr_stats[:read_sectors] - prev_stats[:read_sectors]
|
61
|
+
write_sectors = curr_stats[:write_sectors] - prev_stats[:write_sectors]
|
62
|
+
|
63
|
+
sector_size = 512
|
64
|
+
read_mb_per_sec = (read_sectors * sector_size / 1_048_576.0).round(2)
|
65
|
+
write_mb_per_sec = (write_sectors * sector_size / 1_048_576.0).round(2)
|
66
|
+
|
67
|
+
{
|
68
|
+
read_mb_per_sec:,
|
69
|
+
write_mb_per_sec:
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def bandwidth_usage
|
74
|
+
prev_stats = read_network_stats
|
75
|
+
sleep(1)
|
76
|
+
curr_stats = read_network_stats
|
77
|
+
|
78
|
+
incoming_bytes = curr_stats[:rx_bytes] - prev_stats[:rx_bytes]
|
79
|
+
outgoing_bytes = curr_stats[:tx_bytes] - prev_stats[:tx_bytes]
|
80
|
+
|
81
|
+
bytes_to_mbits = 8.0 / 1_000_000 # Convert bytes to megabits
|
82
|
+
{
|
83
|
+
incoming_mbps: (incoming_bytes * bytes_to_mbits).round(2),
|
84
|
+
outgoing_mbps: (outgoing_bytes * bytes_to_mbits).round(2)
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def proc_path
|
91
|
+
File.join(root_path, "proc")
|
92
|
+
end
|
93
|
+
|
94
|
+
def root_path
|
95
|
+
ENV.fetch("ROOT_PATH", "/")
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_cpu_stat
|
99
|
+
cpu_stats = File.read("#{proc_path}/stat").lines.first.split(/\s+/)
|
100
|
+
{
|
101
|
+
user: cpu_stats[1].to_i,
|
102
|
+
nice: cpu_stats[2].to_i,
|
103
|
+
system: cpu_stats[3].to_i,
|
104
|
+
idle: cpu_stats[4].to_i,
|
105
|
+
iowait: cpu_stats[5].to_i,
|
106
|
+
irq: cpu_stats[6].to_i,
|
107
|
+
softirq: cpu_stats[7].to_i,
|
108
|
+
steal: cpu_stats[8].to_i
|
109
|
+
}
|
110
|
+
end
|
111
|
+
|
112
|
+
def read_disk_stats
|
113
|
+
primary_disk = File.read("#{proc_path}/partitions")
|
114
|
+
.lines
|
115
|
+
.drop(2)
|
116
|
+
.first
|
117
|
+
.split
|
118
|
+
.last
|
119
|
+
|
120
|
+
stats = File.read("#{proc_path}/diskstats")
|
121
|
+
.lines
|
122
|
+
.map(&:split)
|
123
|
+
.find { |line| line[2] == primary_disk }
|
124
|
+
|
125
|
+
{
|
126
|
+
read_sectors: stats[5].to_i,
|
127
|
+
write_sectors: stats[9].to_i
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
def read_network_stats
|
132
|
+
primary_interface = File.read("#{proc_path}/net/route")
|
133
|
+
.lines
|
134
|
+
.drop(1)
|
135
|
+
.find { |line| line.split[1] == "00000000" }
|
136
|
+
&.split&.first
|
137
|
+
stats = File.read("#{proc_path}/net/dev")
|
138
|
+
.lines
|
139
|
+
.map(&:split)
|
140
|
+
.find { |line| line[0].chomp(":") == primary_interface }
|
141
|
+
|
142
|
+
{
|
143
|
+
rx_bytes: stats[1].to_i,
|
144
|
+
tx_bytes: stats[9].to_i
|
145
|
+
}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/public/favicon.ico
ADDED
Binary file
|
Binary file
|
data/public/icon-512.png
ADDED
Binary file
|
data/public/icon.svg
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<svg class="logo" version="1.1" viewBox="0 0 215 160" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
3
|
+
<style>
|
4
|
+
g {
|
5
|
+
stroke: oklch(10.91% 0.003 286.03);
|
6
|
+
}
|
7
|
+
@media (prefers-color-scheme: dark) {
|
8
|
+
g {
|
9
|
+
stroke:oklch(98.14% 0.001 286.38);
|
10
|
+
}
|
11
|
+
}
|
12
|
+
</style>
|
13
|
+
<g transform="translate(2.5 -37.876)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit=".8">
|
14
|
+
<circle cx="30.662" cy="150.82" r="8" stroke-width="4.5" />
|
15
|
+
<circle cx="64.516" cy="101.87" r="8" stroke-width="4.5" />
|
16
|
+
<circle cx="133.07" cy="132.66" r="8" stroke-width="4.5" />
|
17
|
+
<circle cx="179.32" cy="78.569" r="8" stroke-width="4.5" />
|
18
|
+
<path d="m41.112 135.88 13.321-19.699" stroke-width="6" />
|
19
|
+
<path d="m81.341 109.81 34.616 16.25" stroke-width="6" />
|
20
|
+
<path d="m145.39 119.63 22.239-27.032" stroke-width="6" />
|
21
|
+
<rect x="7.5345" y="46.187" width="194.93" height="143.38" ry="7.6132" stroke-width="8" />
|
22
|
+
</g>
|
23
|
+
</svg>
|
data/public/style.css
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
@font-face {
|
2
|
+
font-family: "Rubik";
|
3
|
+
src: url("fonts/Rubik.woff2") format("woff2");
|
4
|
+
font-weight: 1 999;
|
5
|
+
font-display: optional;
|
6
|
+
}
|
7
|
+
|
8
|
+
:root {
|
9
|
+
--font-base-size: 1rem;
|
10
|
+
--font-scale-ratio: 1.3;
|
11
|
+
|
12
|
+
--color-grey-50: oklch(98.14% 0.001 286.38);
|
13
|
+
--color-grey-100: oklch(91.34% 0.003 286.35);
|
14
|
+
--color-grey-200: oklch(83.09% 0.006 286.28);
|
15
|
+
--color-grey-300: oklch(73.99% 0.009 286.19);
|
16
|
+
--color-grey-400: oklch(65.59% 0.012 286.07);
|
17
|
+
--color-grey-500: oklch(55.86% 0.014 285.95);
|
18
|
+
--color-grey-600: oklch(47.36% 0.011 285.97);
|
19
|
+
--color-grey-700: oklch(37.33% 0.008 285.98);
|
20
|
+
--color-grey-800: oklch(27.39% 0.005 286.03);
|
21
|
+
--color-grey-900: oklch(15.95% 0.002 286.16);
|
22
|
+
--color-grey-950: oklch(10.91% 0.003 286.03);
|
23
|
+
|
24
|
+
--color-text: var(--color-grey-900);
|
25
|
+
--color-background: var(--color-grey-50);
|
26
|
+
--color-link-active: var(--color-grey-700);
|
27
|
+
--color-link-hover: var(--color-grey-600);
|
28
|
+
--color-border: var(--color-grey-400);
|
29
|
+
|
30
|
+
--font-size-sm: calc(var(--font-base-size) / var(--font-scale-ratio));
|
31
|
+
--font-size-md: var(--font-base-size);
|
32
|
+
--font-size-lg: calc(var(--font-size-md) * var(--font-scale-ratio));
|
33
|
+
--font-size-xl: calc(var(--font-size-lg) * var(--font-scale-ratio));
|
34
|
+
--font-size-2xl: calc(var(--font-size-xl) * var(--font-scale-ratio));
|
35
|
+
--font-size-3xl: calc(var(--font-size-2xl) * var(--font-scale-ratio));
|
36
|
+
--font-size-4xl: calc(var(--font-size-2xl) * var(--font-scale-ratio));
|
37
|
+
|
38
|
+
--space-unit: 1em;
|
39
|
+
--space-xxs: calc(0.25 * var(--space-unit));
|
40
|
+
--space-xs: calc(0.5 * var(--space-unit));
|
41
|
+
--space-sm: calc(0.75 * var(--space-unit));
|
42
|
+
--space-md: calc(1.25 * var(--space-unit));
|
43
|
+
--space-lg: calc(1.5 * var(--space-unit));
|
44
|
+
--space-xl: calc(2 * var(--space-unit));
|
45
|
+
--space-2xl: calc(3.25 * var(--space-unit));
|
46
|
+
--space-3xl: calc(5.25 * var(--space-unit));
|
47
|
+
|
48
|
+
--screen--size-md: 55rem;
|
49
|
+
--screen--size-lg: 100rem;
|
50
|
+
}
|
51
|
+
|
52
|
+
* {
|
53
|
+
margin: 0;
|
54
|
+
padding: 0;
|
55
|
+
box-sizing: border-box;
|
56
|
+
}
|
57
|
+
|
58
|
+
body {
|
59
|
+
margin: 0 auto;
|
60
|
+
font-family: "Rubik", sans-serif;
|
61
|
+
font-optical-sizing: auto;
|
62
|
+
font-style: normal;
|
63
|
+
color: var(--color-text);
|
64
|
+
background-color: var(--color-background);
|
65
|
+
max-width: var(--screen--size-lg);
|
66
|
+
padding: 0 var(--space-md);
|
67
|
+
}
|
68
|
+
|
69
|
+
header {
|
70
|
+
display: flex;
|
71
|
+
flex-direction: row;
|
72
|
+
gap: var(--space-md);
|
73
|
+
padding: var(--space-md) 0;
|
74
|
+
|
75
|
+
.logo {
|
76
|
+
width: 3rem;
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
main {
|
81
|
+
padding: var(--space-md) 0;
|
82
|
+
}
|
83
|
+
|
84
|
+
h1 {
|
85
|
+
font-size: var(--font-size-2xl);
|
86
|
+
}
|
87
|
+
|
88
|
+
a:visited {
|
89
|
+
color: var(--color-link-hover);
|
90
|
+
}
|
91
|
+
|
92
|
+
footer {
|
93
|
+
padding: var(--space-sm) 0;
|
94
|
+
}
|
95
|
+
|
96
|
+
.charts {
|
97
|
+
display: grid;
|
98
|
+
grid-template-columns: 1fr;
|
99
|
+
gap: var(--space-md);
|
100
|
+
}
|
101
|
+
|
102
|
+
.tile {
|
103
|
+
display: flex;
|
104
|
+
flex-direction: column;
|
105
|
+
gap: var(--space-sm);
|
106
|
+
padding: var(--space-md);
|
107
|
+
border: solid 1px var(--color-border);
|
108
|
+
border-radius: 10px;
|
109
|
+
height: 20rem;
|
110
|
+
}
|
111
|
+
|
112
|
+
.controls {
|
113
|
+
display: flex;
|
114
|
+
justify-content: flex-end;
|
115
|
+
align-items: center;
|
116
|
+
gap: var(--space-md);
|
117
|
+
margin-bottom: var(--space-sm);
|
118
|
+
|
119
|
+
select {
|
120
|
+
font-size: var(--font-size-md);
|
121
|
+
padding: var(--space-xxs) var(--space-xs);
|
122
|
+
border: 1px solid var(--color-border);
|
123
|
+
border-radius: 5px;
|
124
|
+
background-color: var(--color-background);
|
125
|
+
color: var(--color-text);
|
126
|
+
cursor: pointer;
|
127
|
+
transition:
|
128
|
+
border-color 0.3s,
|
129
|
+
box-shadow 0.3s;
|
130
|
+
}
|
131
|
+
|
132
|
+
select:hover {
|
133
|
+
border-color: var(--color-link-hover);
|
134
|
+
}
|
135
|
+
|
136
|
+
select:focus {
|
137
|
+
outline: none;
|
138
|
+
border-color: var(--color-link-active);
|
139
|
+
box-shadow: 0 0 0 2px var(--color-border);
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
@media (min-width: 55rem) {
|
144
|
+
.charts {
|
145
|
+
grid-template-columns: repeat(2, 1fr);
|
146
|
+
}
|
147
|
+
|
148
|
+
.tile {
|
149
|
+
height: 25rem;
|
150
|
+
}
|
151
|
+
}
|
152
|
+
|
153
|
+
@media (prefers-color-scheme: dark) {
|
154
|
+
:root {
|
155
|
+
--color-text: var(--color-grey-50);
|
156
|
+
--color-background: var(--color-grey-900);
|
157
|
+
--color-link-active: var(--color-grey-300);
|
158
|
+
--color-link-hover: var(--color-grey-400);
|
159
|
+
--color-border: var(--color-grey-600);
|
160
|
+
}
|
161
|
+
}
|
metadata
ADDED
@@ -0,0 +1,206 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: puny-monitor
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Hans Schnedlitz
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-10-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: chartkick
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: groupdate
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '6.4'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '6.4'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rackup
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '2.1'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rufus-scheduler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.9'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sinatra
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '4.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '4.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: sinatra-activerecord
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sinatra-contrib
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '4.0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2.0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '2.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: sys-filesystem
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.4'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.4'
|
139
|
+
description:
|
140
|
+
email:
|
141
|
+
- hello@hansschnedlitz.com
|
142
|
+
executables:
|
143
|
+
- puny-monitor
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- ".ruby-version"
|
148
|
+
- LICENSE
|
149
|
+
- README.md
|
150
|
+
- Rakefile
|
151
|
+
- app/models/bandwidth.rb
|
152
|
+
- app/models/cpu_load.rb
|
153
|
+
- app/models/cpu_usage.rb
|
154
|
+
- app/models/disk_io.rb
|
155
|
+
- app/models/filesystem_usage.rb
|
156
|
+
- app/models/memory_usage.rb
|
157
|
+
- app/puny_monitor.rb
|
158
|
+
- app/scheduler.rb
|
159
|
+
- app/views/index.erb
|
160
|
+
- app/views/layout.erb
|
161
|
+
- config.ru
|
162
|
+
- config/database.yml
|
163
|
+
- config/environment.rb
|
164
|
+
- config/initializers/chartkick.rb
|
165
|
+
- db/migrate/20231023000000_add_indices_to_created_at_columns.rb
|
166
|
+
- db/schema.rb
|
167
|
+
- db/seeds.rb
|
168
|
+
- exe/puny-monitor
|
169
|
+
- lib/puny_monitor.rb
|
170
|
+
- lib/puny_monitor/version.rb
|
171
|
+
- lib/system_utils.rb
|
172
|
+
- public/favicon.ico
|
173
|
+
- public/fonts/Rubik.woff2
|
174
|
+
- public/icon-512.png
|
175
|
+
- public/icon.svg
|
176
|
+
- public/javascript/index.js
|
177
|
+
- public/style.css
|
178
|
+
homepage: https://github.com/hschne/puny-monitor
|
179
|
+
licenses:
|
180
|
+
- MIT
|
181
|
+
metadata:
|
182
|
+
allowed_push_host: https://rubygems.org
|
183
|
+
homepage_uri: https://github.com/hschne/puny-monitor
|
184
|
+
source_code_uri: https://github.com/hschne/puny-monitor
|
185
|
+
changelog_uri: https://github.com/hschne/puny-monitor/CHANGELOG
|
186
|
+
rubygems_mfa_required: 'true'
|
187
|
+
post_install_message:
|
188
|
+
rdoc_options: []
|
189
|
+
require_paths:
|
190
|
+
- lib
|
191
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: 3.1.0
|
196
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
requirements: []
|
202
|
+
rubygems_version: 3.5.21
|
203
|
+
signing_key:
|
204
|
+
specification_version: 4
|
205
|
+
summary: A batteries-included monitoring tool for single hosts. Works great with Kamal.
|
206
|
+
test_files: []
|