feat: add dynamic capacity tracking for backhaul devices (#113)
For devices with device_role="backhaul", traffic graphs now show capacity reference lines that update over time based on which interfaces are active. Features: - Calculates total capacity per timestamp from active monitored interfaces - Uses configured_capacity_bps if set, falls back to if_speed - Capacity adjusts as interfaces go up/down throughout the time period - Shows as dashed lines at +capacity (inbound) and -capacity (outbound) - Matches the existing 0-axis graph style Implementation: - Small graph (device detail page): Updated build_traffic_json_datasets/2 and add_capacity_datasets/3 in device_live/show.ex - Full-page graph: Updated build_traffic_datasets/2 and add_capacity_to_full_graph/4 in graph_live/show.ex The capacity lines provide visual reference for backhaul link saturation, showing how close actual throughput is to maximum capacity at each point in time. Reviewed-on: graham/towerops-web#113
This commit is contained in:
parent
35b1c9e753
commit
2bb040154a
3 changed files with 204 additions and 5 deletions
|
|
@ -302,7 +302,7 @@ defmodule Towerops.Snmp do
|
|||
def get_device_with_associations(device_id) do
|
||||
Device
|
||||
|> where([d], d.device_id == ^device_id)
|
||||
|> preload([:interfaces, :sensors, :state_sensors, :processors, :storage])
|
||||
|> preload([:device, :interfaces, :sensors, :state_sensors, :processors, :storage])
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1167,7 +1167,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
if Enum.empty?(all_stats) do
|
||||
nil
|
||||
else
|
||||
build_traffic_json_datasets(all_stats)
|
||||
build_traffic_json_datasets(all_stats, device)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1220,7 +1220,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
end)
|
||||
end
|
||||
|
||||
defp build_traffic_json_datasets(all_stats) do
|
||||
defp build_traffic_json_datasets(all_stats, device) do
|
||||
in_data = Enum.map(all_stats, fn s -> %{x: s.timestamp_ms, y: Float.round(s.in_bps, 2)} end)
|
||||
out_data = Enum.map(all_stats, fn s -> %{x: s.timestamp_ms, y: -Float.round(s.out_bps, 2)} end)
|
||||
|
||||
|
|
@ -1229,9 +1229,102 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
%{label: "Inbound", data: in_data}
|
||||
]
|
||||
|
||||
# For backhaul devices, add capacity reference lines
|
||||
datasets =
|
||||
if device.device.device_role == "backhaul" do
|
||||
add_capacity_datasets(datasets, all_stats, device.interfaces)
|
||||
else
|
||||
datasets
|
||||
end
|
||||
|
||||
Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
|
||||
# Add capacity reference lines for backhaul devices
|
||||
# Calculates capacity per timestamp based on which interfaces have data at that point
|
||||
defp add_capacity_datasets(datasets, all_stats, interfaces) do
|
||||
# Build a map of interface_id -> capacity for quick lookup
|
||||
interface_capacity_map =
|
||||
interfaces
|
||||
|> Enum.filter(& &1.monitored)
|
||||
|> Map.new(fn interface ->
|
||||
capacity = interface.configured_capacity_bps || interface.if_speed || 0
|
||||
{interface.id, capacity}
|
||||
end)
|
||||
|
||||
# Get the raw interface stats to determine which interfaces were active at each timestamp
|
||||
twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||
|
||||
# Track which interfaces have data at each timestamp
|
||||
interface_activity_by_timestamp =
|
||||
interfaces
|
||||
|> Enum.flat_map(fn interface ->
|
||||
interface.id
|
||||
|> Snmp.get_interface_stats(since: twenty_four_hours_ago, limit: 1000)
|
||||
|> Enum.map(fn stat ->
|
||||
ts_seconds = DateTime.to_unix(stat.checked_at)
|
||||
rounded_ts_ms = div(ts_seconds, 60) * 60 * 1000
|
||||
{rounded_ts_ms, interface.id}
|
||||
end)
|
||||
end)
|
||||
|> Enum.group_by(fn {ts, _} -> ts end, fn {_, iface_id} -> iface_id end)
|
||||
|
||||
# Calculate capacity at each timestamp based on active interfaces
|
||||
capacity_data =
|
||||
Enum.map(all_stats, fn stat ->
|
||||
# Get interfaces that had data at this timestamp
|
||||
active_interfaces = Map.get(interface_activity_by_timestamp, stat.timestamp_ms, [])
|
||||
# Sum capacity of active interfaces
|
||||
total_capacity =
|
||||
active_interfaces
|
||||
|> Enum.uniq()
|
||||
|> Enum.reduce(0, fn iface_id, acc ->
|
||||
capacity = Map.get(interface_capacity_map, iface_id, 0)
|
||||
acc + capacity
|
||||
end)
|
||||
|
||||
{stat.timestamp_ms, total_capacity}
|
||||
end)
|
||||
|
||||
# Only add capacity datasets if we have at least some capacity data
|
||||
max_capacity = capacity_data |> Enum.map(fn {_, cap} -> cap end) |> Enum.max(fn -> 0 end)
|
||||
|
||||
if max_capacity > 0 do
|
||||
capacity_in_data =
|
||||
Enum.map(capacity_data, fn {ts, cap} ->
|
||||
%{x: ts, y: Float.round(cap * 1.0, 2)}
|
||||
end)
|
||||
|
||||
capacity_out_data =
|
||||
Enum.map(capacity_data, fn {ts, cap} ->
|
||||
%{x: ts, y: -Float.round(cap * 1.0, 2)}
|
||||
end)
|
||||
|
||||
# Add capacity datasets with special styling
|
||||
datasets ++
|
||||
[
|
||||
%{
|
||||
label: "Capacity (In)",
|
||||
data: capacity_in_data,
|
||||
borderDash: [5, 5],
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
},
|
||||
%{
|
||||
label: "Capacity (Out)",
|
||||
data: capacity_out_data,
|
||||
borderDash: [5, 5],
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
}
|
||||
]
|
||||
else
|
||||
datasets
|
||||
end
|
||||
end
|
||||
|
||||
# Group interfaces by type for organized display
|
||||
defp group_interfaces_by_type(interfaces) do
|
||||
interfaces
|
||||
|
|
|
|||
|
|
@ -631,11 +631,21 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
limit = get_limit_for_range(range)
|
||||
active_interfaces = Enum.filter(device.interfaces, &(&1.if_oper_status == "up"))
|
||||
|
||||
if Enum.empty?(active_interfaces), do: nil, else: encode_interface_datasets(active_interfaces, since, limit)
|
||||
if Enum.empty?(active_interfaces), do: nil, else: encode_interface_datasets(active_interfaces, since, limit, device)
|
||||
end
|
||||
|
||||
defp encode_interface_datasets(interfaces, since, limit) do
|
||||
# Get device for adding capacity lines to full-page traffic view
|
||||
defp encode_interface_datasets(interfaces, since, limit, device) do
|
||||
datasets = build_per_interface_datasets(interfaces, since, limit)
|
||||
|
||||
# Add capacity datasets for backhaul devices
|
||||
datasets =
|
||||
if device.device.device_role == "backhaul" && !Enum.empty?(datasets) do
|
||||
add_capacity_to_full_graph(datasets, interfaces, since, limit)
|
||||
else
|
||||
datasets
|
||||
end
|
||||
|
||||
if Enum.empty?(datasets), do: nil, else: Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
|
||||
|
|
@ -661,6 +671,102 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
end)
|
||||
end
|
||||
|
||||
# Add capacity reference lines for backhaul devices on full-page graph
|
||||
# Calculates capacity per timestamp based on which interfaces have data at that point
|
||||
defp add_capacity_to_full_graph(datasets, interfaces, since, limit) do
|
||||
# Build a map of interface_id -> capacity for quick lookup
|
||||
interface_capacity_map =
|
||||
interfaces
|
||||
|> Enum.filter(& &1.monitored)
|
||||
|> Map.new(fn interface ->
|
||||
capacity = interface.configured_capacity_bps || interface.if_speed || 0
|
||||
{interface.id, capacity}
|
||||
end)
|
||||
|
||||
# Track which interfaces have data at each timestamp
|
||||
interface_activity_by_timestamp =
|
||||
interfaces
|
||||
|> Enum.flat_map(fn interface ->
|
||||
interface.id
|
||||
|> Snmp.get_interface_stats(since: since, limit: limit)
|
||||
|> Enum.map(fn stat ->
|
||||
ts_ms = DateTime.to_unix(stat.checked_at, :millisecond)
|
||||
{ts_ms, interface.id}
|
||||
end)
|
||||
end)
|
||||
|> Enum.group_by(fn {ts, _} -> ts end, fn {_, iface_id} -> iface_id end)
|
||||
|
||||
# Get all unique timestamps from the datasets
|
||||
all_timestamps =
|
||||
datasets
|
||||
|> Enum.flat_map(fn dataset ->
|
||||
case dataset.data do
|
||||
nil -> []
|
||||
data -> Enum.map(data, & &1.x)
|
||||
end
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.sort()
|
||||
|
||||
# Calculate capacity at each timestamp based on active interfaces
|
||||
capacity_data =
|
||||
Enum.map(all_timestamps, fn ts ->
|
||||
# Get interfaces that had data at this timestamp (within 1 minute tolerance)
|
||||
active_interfaces =
|
||||
interface_activity_by_timestamp
|
||||
|> Enum.filter(fn {stat_ts, _} -> abs(stat_ts - ts) < 60_000 end)
|
||||
|> Enum.flat_map(fn {_, iface_ids} -> iface_ids end)
|
||||
|> Enum.uniq()
|
||||
|
||||
# Sum capacity of active interfaces
|
||||
total_capacity =
|
||||
Enum.reduce(active_interfaces, 0, fn iface_id, acc ->
|
||||
capacity = Map.get(interface_capacity_map, iface_id, 0)
|
||||
acc + capacity
|
||||
end)
|
||||
|
||||
{ts, total_capacity}
|
||||
end)
|
||||
|
||||
# Only add capacity datasets if we have at least some capacity data
|
||||
max_capacity = capacity_data |> Enum.map(fn {_, cap} -> cap end) |> Enum.max(fn -> 0 end)
|
||||
|
||||
if max_capacity > 0 do
|
||||
capacity_in_data =
|
||||
Enum.map(capacity_data, fn {ts, cap} ->
|
||||
%{x: ts, y: Float.round(cap * 1.0, 2)}
|
||||
end)
|
||||
|
||||
capacity_out_data =
|
||||
Enum.map(capacity_data, fn {ts, cap} ->
|
||||
%{x: ts, y: -Float.round(cap * 1.0, 2)}
|
||||
end)
|
||||
|
||||
# Add capacity datasets with dashed styling
|
||||
datasets ++
|
||||
[
|
||||
%{
|
||||
label: "Capacity (In)",
|
||||
data: capacity_in_data,
|
||||
borderDash: [5, 5],
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
},
|
||||
%{
|
||||
label: "Capacity (Out)",
|
||||
data: capacity_out_data,
|
||||
borderDash: [5, 5],
|
||||
borderWidth: 2,
|
||||
pointRadius: 0,
|
||||
fill: false
|
||||
}
|
||||
]
|
||||
else
|
||||
datasets
|
||||
end
|
||||
end
|
||||
|
||||
defp build_interface_traffic_chart_json(interface, range) do
|
||||
since = get_datetime_from_range(range)
|
||||
limit = get_limit_for_range(range)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue