Add disk activity

This commit is contained in:
Mikko Ahlroth 2023-09-02 23:34:12 +03:00
parent 7646c5ec3c
commit c2ae057124
10 changed files with 168 additions and 35 deletions

View file

@ -10,18 +10,18 @@ h1 {
h2 { h2 {
font-size: 1.5rem; font-size: 1.5rem;
margin-top: 2rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
h2:first-child {
margin-top: 0;
}
#cpus, #mems, #disks { #cpus, #mems, #disks {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 10px; gap: 10px;
margin-top: 2rem;
}
#cpus {
margin-top: 0;
} }
#cpus h2, #mems h2, #disks h2 { #cpus h2, #mems h2, #disks h2 {

View file

@ -17,7 +17,7 @@ config :tietopaketti,
name: System.fetch_env!("INSTANCE_NAME") name: System.fetch_env!("INSTANCE_NAME")
}, },
update_interval: String.to_integer(System.get_env("UPDATE_INTERVAL") || "2000"), update_interval: String.to_integer(System.get_env("UPDATE_INTERVAL") || "2000"),
disks_to_show: String.split(System.get_env("DISKS_TO_SHOW", ""), ",") disks: Jason.decode!(System.get_env("DISKS", "{}"))
# ## Using releases # ## Using releases
# #

View file

@ -15,7 +15,7 @@ defmodule Tietopaketti.Application do
name: Tietopaketti.Sysmon, name: Tietopaketti.Sysmon,
pubsub_name: Tietopaketti.PubSub, pubsub_name: Tietopaketti.PubSub,
update_interval: Application.fetch_env!(:tietopaketti, :update_interval), update_interval: Application.fetch_env!(:tietopaketti, :update_interval),
disks_to_show: MapSet.new(Application.fetch_env!(:tietopaketti, :disks_to_show)) disks: Application.fetch_env!(:tietopaketti, :disks)
}}, }},
# Start the Endpoint (http/https) # Start the Endpoint (http/https)
TietopakettiWeb.Endpoint TietopakettiWeb.Endpoint

View file

@ -0,0 +1,72 @@
defmodule Tietopaketti.ProcDiskstats do
import Tietopaketti.TypedStruct
@fields 17
deftypedstruct(%{
block: String.t(),
reads_completed: {non_neg_integer(), 0},
reads_merged: {non_neg_integer(), 0},
sectors_read: {non_neg_integer(), 0},
milliseconds_reading: {non_neg_integer(), 0},
writes_completed: {non_neg_integer(), 0},
writes_merged: {non_neg_integer(), 0},
sectors_written: {non_neg_integer(), 0},
milliseconds_writing: {non_neg_integer(), 0},
ios_in_progress: {non_neg_integer(), 0},
milliseconds_doing_io: {non_neg_integer(), 0},
weighted_milliseconds_doing_io: {non_neg_integer(), 0},
discards_completed: {non_neg_integer(), 0},
discards_merged: {non_neg_integer(), 0},
sectors_discarded: {non_neg_integer(), 0},
milliseconds_discarding: {non_neg_integer(), 0},
flush_requests_completed: {non_neg_integer(), 0},
milliseconds_flushing: {non_neg_integer(), 0}
})
def sector_bytes(), do: 512
def path(), do: "/proc/diskstats"
def parse_line(line) do
parts = line |> String.split(" ") |> Enum.reject(&(&1 == "")) |> Enum.with_index()
[_, _, {block, _} | parts] = parts
for {part, i} <- parts, reduce: %__MODULE__{block: block} do
acc ->
val =
case Integer.parse(part) do
{int, _bin} -> int
:error -> 0
end
if val <= @fields - 1 + 3 do
key =
case i do
3 -> :reads_completed
4 -> :reads_merged
5 -> :sectors_read
6 -> :milliseconds_readin
7 -> :writes_completed
8 -> :writes_merged
9 -> :sectors_written
10 -> :milliseconds_writin
11 -> :ios_in_progres
12 -> :milliseconds_doing_io
13 -> :weighted_milliseconds_doing_io
14 -> :discards_completed
15 -> :discards_merged
16 -> :sectors_discarded
17 -> :milliseconds_discarding
18 -> :flush_requests_completed
19 -> :milliseconds_flushing
end
struct!(acc, [{key, val}])
else
val
end
end
end
end

View file

@ -4,9 +4,11 @@ defmodule Tietopaketti.Sysdata do
defmodule Disk do defmodule Disk do
deftypedstruct(%{ deftypedstruct(%{
id: non_neg_integer(), id: non_neg_integer(),
name: String.t(), path: String.t(),
total: non_neg_integer(), total: non_neg_integer(),
used_percent: non_neg_integer() used_percent: 0..100,
stats: Tietopaketti.ProcDiskstats.t(),
prev_stats: Tietopaketti.ProcDiskstats.t()
}) })
end end

View file

@ -2,13 +2,14 @@ defmodule Tietopaketti.Sysmon do
use GenServer use GenServer
import Tietopaketti.TypedStruct import Tietopaketti.TypedStruct
alias Tietopaketti.Sysdata alias Tietopaketti.Sysdata
alias Tietopaketti.ProcDiskstats
defmodule Options do defmodule Options do
deftypedstruct(%{ deftypedstruct(%{
name: GenServer.name(), name: GenServer.name(),
pubsub_name: GenServer.name(), pubsub_name: GenServer.name(),
update_interval: non_neg_integer(), update_interval: non_neg_integer(),
disks_to_show: MapSet.t(String.t()) disks: %{optional(String.t()) => String.t()}
}) })
end end
@ -16,7 +17,9 @@ defmodule Tietopaketti.Sysmon do
deftypedstruct(%{ deftypedstruct(%{
pubsub_name: GenServer.name(), pubsub_name: GenServer.name(),
update_interval: non_neg_integer(), update_interval: non_neg_integer(),
disks_to_show: MapSet.t(String.t()) paths_by_block: %{optional(String.t()) => String.t()},
blocks_by_path: %{optional(String.t()) => String.t()},
prev_disk_stats: %{optional(String.t()) => ProcDiskstats.t()}
}) })
end end
@ -36,7 +39,9 @@ defmodule Tietopaketti.Sysmon do
%State{ %State{
pubsub_name: opts.pubsub_name, pubsub_name: opts.pubsub_name,
update_interval: opts.update_interval, update_interval: opts.update_interval,
disks_to_show: opts.disks_to_show paths_by_block: opts.disks,
blocks_by_path: Map.new(opts.disks, fn {k, v} -> {v, k} end),
prev_disk_stats: Map.new(opts.disks, fn {k, _v} -> {k, %ProcDiskstats{block: k}} end)
}} }}
end end
@ -54,13 +59,23 @@ defmodule Tietopaketti.Sysmon do
mem_data = :memsup.get_system_memory_data() mem_data = :memsup.get_system_memory_data()
disk_data = :disksup.get_disk_data()
disk_activity = load_disk_activity(state)
disk_data = disk_data =
:disksup.get_disk_data() for {{path, total, used}, i} <- Enum.with_index(disk_data),
|> Enum.filter(fn {name, _, _} -> MapSet.member?(state.disks_to_show, name) end) Map.has_key?(state.blocks_by_path, path) do
|> Enum.with_index() block = Map.fetch!(state.blocks_by_path, path)
|> Enum.map(fn {{name, total, used}, i} ->
%Sysdata.Disk{id: i, name: name, total: total, used_percent: used} %Sysdata.Disk{
end) id: i,
path: path,
total: total,
used_percent: used,
stats: Map.get(disk_activity, block, %ProcDiskstats{block: block}),
prev_stats: Map.get(state.prev_disk_stats, block, %ProcDiskstats{block: block})
}
end
free_memory = free_memory =
case Keyword.get(mem_data, :available_memory, :unavailable) do case Keyword.get(mem_data, :available_memory, :unavailable) do
@ -86,6 +101,20 @@ defmodule Tietopaketti.Sysmon do
} }
Phoenix.PubSub.broadcast(state.pubsub_name, pubsub_topic(), {Tietopaketti.Sysdata, data}) Phoenix.PubSub.broadcast(state.pubsub_name, pubsub_topic(), {Tietopaketti.Sysdata, data})
state %State{state | prev_disk_stats: disk_activity}
end
defp load_disk_activity(%State{} = state) do
with {:ok, content} <- File.read(ProcDiskstats.path()),
lines <- String.split(content, "\n") do
for line <- lines,
parsed <- ProcDiskstats.parse_line(line),
Map.has_key?(state.paths_by_block, parsed.block),
reduce: %{} do
acc -> Map.put(acc, parsed.block, parsed)
end
else
_ -> %{}
end
end end
end end

View file

@ -1,5 +1,6 @@
defmodule TietopakettiWeb.DashLive.Index do defmodule TietopakettiWeb.DashLive.Index do
use TietopakettiWeb, :live_view use TietopakettiWeb, :live_view
alias Tietopaketti.ProcDiskstats
def mount(_params, _session, socket) do def mount(_params, _session, socket) do
socket = socket =
@ -22,6 +23,16 @@ defmodule TietopakettiWeb.DashLive.Index do
{:noreply, socket} {:noreply, socket}
end end
def reads_getter(%ProcDiskstats{} = stats), do: stats.sectors_read
def writes_getter(%ProcDiskstats{} = stats), do: stats.sectors_written
def sector_diff_bytes(prev_stats, stats, getter) do
prev = getter.(prev_stats)
now = getter.(stats)
(now - prev) * ProcDiskstats.sector_bytes()
end
def humanize_size(bytes, add_unit) do def humanize_size(bytes, add_unit) do
bytes = bytes + 0.0 bytes = bytes + 0.0

View file

@ -46,7 +46,7 @@
/> />
</div> </div>
</div> </div>
<%= if @sysdata.disk != [] do %> <%= if @sysdata.disk != [] do %>
<div id="disks"> <div id="disks">
<h2>Disks</h2> <h2>Disks</h2>
@ -55,15 +55,23 @@
<.live_component <.live_component
module={TietopakettiWeb.Progress} module={TietopakettiWeb.Progress}
id={"disk-progress-#{disk.id}"} id={"disk-progress-#{disk.id}"}
label={disk.name} label={"R: #{disk.path}"}
value={disk.used_percent} value={sector_diff_bytes(disk.prev_stats, disk.stats, &reads_getter/1)}
max={100}
value-display={"#{disk.used_percent} % of #{humanize_size_si( value-display={"#{disk.used_percent} % of #{humanize_size_si(
disk.total * 1024, disk.total * 1024,
true true
)}"} )}"}
/> />
</div> </div>
<div class="disk">
<.live_component
module={TietopakettiWeb.Progress}
id={"disk-progress-#{disk.id}"}
label={"W: #{disk.path}"}
value={sector_diff_bytes(disk.prev_stats, disk.stats, &writes_getter/1)}
value-display={"R #{humanize_size_si(sector_diff_bytes(disk.prev_stats, disk.stats, &reads_getter/1), true)} | W #{humanize_size_si(sector_diff_bytes(disk.prev_stats, disk.stats, &writes_getter/1), true)}"}
/>
</div>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>

View file

@ -4,24 +4,31 @@ defmodule TietopakettiWeb.Progress do
@max_history 100 @max_history 100
def mount(socket) do def mount(socket) do
{:ok, assign(socket, max: 0, label: "", id: "", value_display: "", val_id: 0)} {:ok,
assign(socket,
label: "",
id: "",
value_display: "",
vals: 1..@max_history |> Enum.map(fn _ -> 0 end) |> :queue.from_list(),
max: 0
)}
end end
def update(assigns, socket) do def update(assigns, socket) do
val_id = socket.assigns.val_id vals = :queue.drop(:queue.in(assigns.value, socket.assigns.vals))
as_list = :queue.to_list(vals)
max = Map.get(assigns, :max, Enum.max(as_list))
{:ok, {:ok,
socket socket
|> assign( |> assign(
max: assigns.max,
label: assigns.label, label: assigns.label,
id: assigns.id, id: assigns.id,
value_display: assigns[:"value-display"], value_display: assigns[:"value-display"],
val_id: val_id + 1 vals: vals,
) vals_as_list: as_list,
|> stream(:history, [%{id: "#{assigns.id}-vals-#{val_id}", val: assigns.value}], max: max
at: -1,
limit: -@max_history
)} )}
end end
@ -32,9 +39,13 @@ defmodule TietopakettiWeb.Progress do
<div class="progress-current"> <div class="progress-current">
<%= @value_display %> <%= @value_display %>
</div> </div>
<div id={"#{@id}-vals"} class="progress-vals" aria-hidden="true" phx-update="stream"> <div id={"#{@id}-vals"} class="progress-vals" aria-hidden="true">
<%= for {id, val} <- @streams.history, @max > 0 do %> <%= for {val, i} <- Enum.with_index(@vals_as_list), @max > 0 do %>
<div id={id} class="progress-val" style={"height: #{Float.round(val.val / @max * 100)}%;"}> <div
id={"#{@id}-vals-#{i}"}
class="progress-val"
style={"height:#{Float.round(val / @max * 100)}%;"}
>
</div> </div>
<% end %> <% end %>
</div> </div>

View file

@ -4,7 +4,7 @@ defmodule Tietopaketti.MixProject do
def project do def project do
[ [
app: :tietopaketti, app: :tietopaketti,
version: "1.1.1", version: "1.2.0",
elixir: "~> 1.15", elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,