290 lines
9 KiB
Elixir
290 lines
9 KiB
Elixir
defmodule ToweropsWeb.GraphLive.Show do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Devices
|
|
alias Towerops.Snmp
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
{:ok, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(%{"id" => device_id, "sensor_type" => sensor_type} = params, _, socket) do
|
|
_ =
|
|
if connected?(socket) do
|
|
Phoenix.PubSub.subscribe(Towerops.PubSub, "device:#{device_id}")
|
|
end
|
|
|
|
range = Map.get(params, "range", "24h")
|
|
interface_id = Map.get(params, "interface_id")
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:device_id, device_id)
|
|
|> assign(:sensor_type, sensor_type)
|
|
|> assign(:range, range)
|
|
|> assign(:interface_id, interface_id)
|
|
|> load_graph_data()
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("change_range", %{"range" => range}, socket) do
|
|
params = build_graph_params(socket.assigns, range)
|
|
|
|
{:noreply,
|
|
push_patch(socket,
|
|
to:
|
|
~p"/orgs/#{socket.assigns.current_organization.slug}/devices/#{socket.assigns.device_id}/graph/#{socket.assigns.sensor_type}?#{params}"
|
|
)}
|
|
end
|
|
|
|
defp build_graph_params(assigns, range) do
|
|
base_params = %{"range" => range}
|
|
|
|
if assigns.interface_id do
|
|
Map.put(base_params, "interface_id", assigns.interface_id)
|
|
else
|
|
base_params
|
|
end
|
|
end
|
|
|
|
# Private functions
|
|
|
|
defp load_graph_data(socket) do
|
|
device_id = socket.assigns.device_id
|
|
sensor_type = socket.assigns.sensor_type
|
|
range = socket.assigns.range
|
|
interface_id = socket.assigns.interface_id
|
|
|
|
device = Devices.get_device!(device_id)
|
|
|
|
{chart_data, title_suffix} =
|
|
cond do
|
|
sensor_type == "traffic" && interface_id ->
|
|
{load_interface_traffic_chart_data(interface_id, range), get_interface_name(interface_id)}
|
|
|
|
sensor_type == "traffic" ->
|
|
{load_traffic_chart_data(device_id, range), nil}
|
|
|
|
true ->
|
|
sensor_types = get_sensor_types_for_chart(sensor_type)
|
|
{load_sensor_chart_data(device_id, sensor_types, range), nil}
|
|
end
|
|
|
|
{title, unit, auto_scale} = get_chart_config(sensor_type)
|
|
chart_title = if title_suffix, do: "#{title} - #{title_suffix}", else: title
|
|
show_zero_line = sensor_type == "traffic"
|
|
|
|
socket
|
|
|> assign(:device, device)
|
|
|> assign(:page_title, "#{device.name} - #{chart_title}")
|
|
|> assign(:chart_title, chart_title)
|
|
|> assign(:chart_data, chart_data)
|
|
|> assign(:unit, unit)
|
|
|> assign(:auto_scale, auto_scale)
|
|
|> assign(:show_zero_line, show_zero_line)
|
|
end
|
|
|
|
defp get_sensor_types_for_chart("processors"), do: ["cpu_load"]
|
|
defp get_sensor_types_for_chart("memory"), do: ["memory_usage"]
|
|
defp get_sensor_types_for_chart("storage"), do: ["disk_usage"]
|
|
defp get_sensor_types_for_chart("temperature"), do: ["temperature", "cpu_temperature", "celsius"]
|
|
defp get_sensor_types_for_chart("voltage"), do: ["voltage", "volts"]
|
|
defp get_sensor_types_for_chart("traffic"), do: nil
|
|
defp get_sensor_types_for_chart(_), do: []
|
|
|
|
defp get_chart_config("processors"), do: {"Processor Usage", "%", false}
|
|
defp get_chart_config("memory"), do: {"Memory Usage", "%", false}
|
|
defp get_chart_config("storage"), do: {"Storage Usage", "%", false}
|
|
defp get_chart_config("temperature"), do: {"Temperature", "°C", true}
|
|
defp get_chart_config("voltage"), do: {"Voltage", "V", true}
|
|
defp get_chart_config("traffic"), do: {"Overall Traffic", "bps", true}
|
|
defp get_chart_config(_), do: {"Sensor Data", "", false}
|
|
|
|
defp load_sensor_chart_data(device_id, sensor_types, range) do
|
|
case Snmp.get_device_with_associations(device_id) do
|
|
nil ->
|
|
nil
|
|
|
|
device ->
|
|
build_sensor_chart_json(device, sensor_types, range)
|
|
end
|
|
end
|
|
|
|
defp build_sensor_chart_json(device, sensor_types, range) do
|
|
sensors =
|
|
device.sensors
|
|
|> Enum.filter(&(&1.sensor_type in sensor_types))
|
|
|> Enum.sort_by(& &1.sensor_index)
|
|
|
|
if Enum.empty?(sensors) do
|
|
nil
|
|
else
|
|
since = get_datetime_from_range(range)
|
|
limit = get_limit_for_range(range)
|
|
datasets = Enum.map(sensors, &sensor_to_dataset(&1, since, limit))
|
|
Jason.encode!(%{datasets: datasets})
|
|
end
|
|
end
|
|
|
|
defp sensor_to_dataset(sensor, since, limit) do
|
|
readings =
|
|
sensor.id
|
|
|> Snmp.get_sensor_readings(since: since, limit: limit)
|
|
|> Enum.reverse()
|
|
|
|
%{
|
|
label: sensor.sensor_descr,
|
|
data: Enum.map(readings, &reading_to_chart_point/1)
|
|
}
|
|
end
|
|
|
|
defp reading_to_chart_point(reading) do
|
|
%{
|
|
x: DateTime.to_unix(reading.checked_at, :millisecond),
|
|
y: if(reading.value, do: Float.round(reading.value, 1))
|
|
}
|
|
end
|
|
|
|
defp get_datetime_from_range("1h"), do: DateTime.add(DateTime.utc_now(), -1, :hour)
|
|
defp get_datetime_from_range("6h"), do: DateTime.add(DateTime.utc_now(), -6, :hour)
|
|
defp get_datetime_from_range("12h"), do: DateTime.add(DateTime.utc_now(), -12, :hour)
|
|
defp get_datetime_from_range("24h"), do: DateTime.add(DateTime.utc_now(), -24, :hour)
|
|
defp get_datetime_from_range("7d"), do: DateTime.add(DateTime.utc_now(), -7, :day)
|
|
defp get_datetime_from_range("30d"), do: DateTime.add(DateTime.utc_now(), -30, :day)
|
|
defp get_datetime_from_range(_), do: DateTime.add(DateTime.utc_now(), -24, :hour)
|
|
|
|
defp get_limit_for_range("1h"), do: 360
|
|
defp get_limit_for_range("6h"), do: 720
|
|
defp get_limit_for_range("12h"), do: 1440
|
|
defp get_limit_for_range("24h"), do: 2880
|
|
defp get_limit_for_range("7d"), do: 10_080
|
|
defp get_limit_for_range("30d"), do: 43_200
|
|
defp get_limit_for_range(_), do: 2880
|
|
|
|
defp load_traffic_chart_data(device_id, range) do
|
|
case Snmp.get_device_with_associations(device_id) do
|
|
nil -> nil
|
|
device -> build_traffic_chart_json(device, range)
|
|
end
|
|
end
|
|
|
|
defp load_interface_traffic_chart_data(interface_id, range) do
|
|
case Snmp.get_interface(interface_id) do
|
|
nil -> nil
|
|
interface -> build_interface_traffic_chart_json(interface, range)
|
|
end
|
|
end
|
|
|
|
defp get_interface_name(interface_id) do
|
|
case Snmp.get_interface(interface_id) do
|
|
nil -> "Unknown Interface"
|
|
interface -> interface.if_name || interface.if_descr || "Interface #{interface.if_index}"
|
|
end
|
|
end
|
|
|
|
defp build_traffic_chart_json(device, range) do
|
|
if Enum.empty?(device.interfaces), do: nil, else: build_traffic_datasets(device, range)
|
|
end
|
|
|
|
defp build_traffic_datasets(device, range) do
|
|
since = get_datetime_from_range(range)
|
|
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)
|
|
end
|
|
|
|
defp encode_interface_datasets(interfaces, since, limit) do
|
|
datasets = build_per_interface_datasets(interfaces, since, limit)
|
|
if Enum.empty?(datasets), do: nil, else: Jason.encode!(%{datasets: datasets})
|
|
end
|
|
|
|
defp build_per_interface_datasets(interfaces, since, limit) do
|
|
Enum.flat_map(interfaces, fn interface ->
|
|
stats =
|
|
interface.id
|
|
|> Snmp.get_interface_stats(since: since, limit: limit)
|
|
|> Enum.reverse()
|
|
|
|
if Enum.empty?(stats) do
|
|
[]
|
|
else
|
|
interface_name = interface.if_name || interface.if_descr
|
|
in_data = calculate_interface_traffic_data(stats, :inbound)
|
|
out_data = calculate_interface_traffic_data(stats, :outbound)
|
|
|
|
[
|
|
%{label: "#{interface_name} Out", data: out_data},
|
|
%{label: "#{interface_name} In", data: in_data}
|
|
]
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp build_interface_traffic_chart_json(interface, range) do
|
|
since = get_datetime_from_range(range)
|
|
limit = get_limit_for_range(range)
|
|
|
|
stats =
|
|
interface.id
|
|
|> Snmp.get_interface_stats(since: since, limit: limit)
|
|
|> Enum.reverse()
|
|
|
|
if Enum.empty?(stats) do
|
|
nil
|
|
else
|
|
in_data = calculate_interface_traffic_data(stats, :inbound)
|
|
out_data = calculate_interface_traffic_data(stats, :outbound)
|
|
|
|
datasets = [
|
|
%{label: "Outbound", data: out_data},
|
|
%{label: "Inbound", data: in_data}
|
|
]
|
|
|
|
Jason.encode!(%{datasets: datasets})
|
|
end
|
|
end
|
|
|
|
defp calculate_interface_traffic_data(stats, :inbound) do
|
|
stats
|
|
|> Enum.chunk_every(2, 1, :discard)
|
|
|> Enum.map(fn [stat1, stat2] ->
|
|
time_diff = stat2.checked_at |> DateTime.diff(stat1.checked_at, :second) |> max(1)
|
|
|
|
bps =
|
|
((stat2.if_in_octets - stat1.if_in_octets) * 8 / time_diff)
|
|
|> max(0)
|
|
|> :erlang.float()
|
|
|> Float.round(2)
|
|
|
|
%{
|
|
x: DateTime.to_unix(stat2.checked_at, :millisecond),
|
|
y: -bps
|
|
}
|
|
end)
|
|
end
|
|
|
|
defp calculate_interface_traffic_data(stats, :outbound) do
|
|
stats
|
|
|> Enum.chunk_every(2, 1, :discard)
|
|
|> Enum.map(fn [stat1, stat2] ->
|
|
time_diff = stat2.checked_at |> DateTime.diff(stat1.checked_at, :second) |> max(1)
|
|
|
|
bps =
|
|
((stat2.if_out_octets - stat1.if_out_octets) * 8 / time_diff)
|
|
|> max(0)
|
|
|> :erlang.float()
|
|
|> Float.round(2)
|
|
|
|
%{
|
|
x: DateTime.to_unix(stat2.checked_at, :millisecond),
|
|
y: bps
|
|
}
|
|
end)
|
|
end
|
|
end
|