feat(agent): host collector for /proc loadavg, meminfo, uptime

This commit is contained in:
Carsten 2026-04-21 22:08:04 +02:00
parent e4db0beac6
commit ce828084c8
5 changed files with 152 additions and 0 deletions

View file

@ -0,0 +1,106 @@
defmodule ProxmoxAgent.Collectors.Host do
@moduledoc """
Reads host metrics from /proc. Accepts `proc_dir:` option for testability.
Never raises on read failure, populates `:errors` and leaves the field nil.
"""
@type sample :: %{
hostname: String.t(),
load1: float() | nil,
load5: float() | nil,
load15: float() | nil,
mem_total_bytes: non_neg_integer() | nil,
mem_available_bytes: non_neg_integer() | nil,
mem_used_bytes: non_neg_integer() | nil,
uptime_seconds: non_neg_integer() | nil,
errors: [term()]
}
@spec collect(keyword()) :: sample()
def collect(opts \\ []) do
proc_dir = Keyword.get(opts, :proc_dir, "/proc")
{load, e1} = safe(fn -> read_loadavg(proc_dir) end, {nil, nil, nil}, :loadavg)
{mem, e2} = safe(fn -> read_meminfo(proc_dir) end, %{total: nil, available: nil}, :meminfo)
{uptime, e3} = safe(fn -> read_uptime(proc_dir) end, nil, :uptime)
total = mem.total
avail = mem.available
used = if total && avail, do: total - avail, else: nil
{load1, load5, load15} = load
%{
hostname: hostname(),
load1: load1,
load5: load5,
load15: load15,
mem_total_bytes: total,
mem_available_bytes: avail,
mem_used_bytes: used,
uptime_seconds: uptime,
errors: Enum.filter([e1, e2, e3], & &1)
}
end
defp safe(fun, fallback, tag) do
try do
{fun.(), nil}
rescue
e -> {fallback, {tag, Exception.message(e)}}
catch
:error, reason -> {fallback, {tag, reason}}
end
end
defp read_loadavg(proc_dir) do
body = File.read!(Path.join(proc_dir, "loadavg"))
[l1, l5, l15 | _] = String.split(body, ~r/\s+/, trim: true)
{to_float(l1), to_float(l5), to_float(l15)}
end
defp read_meminfo(proc_dir) do
body = File.read!(Path.join(proc_dir, "meminfo"))
parsed =
body
|> String.split("\n", trim: true)
|> Enum.reduce(%{}, fn line, acc ->
case String.split(line, ~r/:\s+/, parts: 2) do
[key, val] -> Map.put(acc, key, val)
_ -> acc
end
end)
%{
total: kb_to_bytes(parsed["MemTotal"]),
available: kb_to_bytes(parsed["MemAvailable"])
}
end
defp read_uptime(proc_dir) do
body = File.read!(Path.join(proc_dir, "uptime"))
[secs | _] = String.split(body, " ", trim: true)
secs |> to_float() |> trunc()
end
defp kb_to_bytes(nil), do: nil
defp kb_to_bytes(str) do
case Regex.run(~r/(\d+)\s*kB/, str) do
[_, kb] -> String.to_integer(kb) * 1024
_ -> nil
end
end
defp to_float(s) do
{f, _} = Float.parse(s)
f
end
defp hostname do
case :inet.gethostname() do
{:ok, name} -> List.to_string(name)
_ -> "unknown-host"
end
end
end

1
agent/test/fixtures/proc/loadavg vendored Normal file
View file

@ -0,0 +1 @@
0.42 0.55 0.31 3/512 12345

7
agent/test/fixtures/proc/meminfo vendored Normal file
View file

@ -0,0 +1,7 @@
MemTotal: 16384000 kB
MemFree: 2048000 kB
MemAvailable: 8192000 kB
Buffers: 256000 kB
Cached: 4096000 kB
SwapTotal: 4194304 kB
SwapFree: 4194304 kB

1
agent/test/fixtures/proc/uptime vendored Normal file
View file

@ -0,0 +1 @@
123456.78 987654.32

View file

@ -0,0 +1,37 @@
defmodule ProxmoxAgent.Collectors.HostTest do
use ExUnit.Case, async: true
alias ProxmoxAgent.Collectors.Host
@proc Path.expand("../../fixtures/proc", __DIR__)
test "collects load average" do
sample = Host.collect(proc_dir: @proc)
assert sample.load1 == 0.42
assert sample.load5 == 0.55
assert sample.load15 == 0.31
end
test "collects memory in bytes" do
sample = Host.collect(proc_dir: @proc)
assert sample.mem_total_bytes == 16_384_000 * 1024
assert sample.mem_available_bytes == 8_192_000 * 1024
assert sample.mem_used_bytes == sample.mem_total_bytes - sample.mem_available_bytes
end
test "collects uptime seconds" do
sample = Host.collect(proc_dir: @proc)
assert sample.uptime_seconds == 123_456
end
test "includes hostname string" do
sample = Host.collect(proc_dir: @proc)
assert is_binary(sample.hostname)
assert sample.hostname != ""
end
test "missing proc files yield :errors field, not a crash" do
sample = Host.collect(proc_dir: "/nonexistent/path/xyz")
assert sample.errors != []
end
end