Refactor credo issues: reduce complexity and nesting depth
- Refactor check_threshold_violation: extract threshold checks into separate functions (complexity 10→~3) - Refactor load_sensor_chart_data: extract helper functions to reduce nesting - Refactor save_equipment: extract SNMP discovery logic into helpers - Refactor poll_sensors: extract error handling and result processing - Refactor handle_status_change: extract equipment down/up handlers - Refactor mount_current_scope: extract session scope building - Refactor load_traffic_chart_data: extract aggregation and calculation helpers - Refactor load_overall_traffic_chart_data: similar pattern to traffic chart - Refactor load_sensor_chart_data in equipment_live/show: extract dataset builders - Refactor discover_storage_sensors: extract storage sensor building - Refactor perform_poll: extract device polling logic into separate functions All 254 tests passing. Credo reports no issues.
This commit is contained in:
parent
02e0744c3c
commit
139a42531f
7 changed files with 652 additions and 544 deletions
|
|
@ -135,82 +135,79 @@ defmodule Towerops.Monitoring.EquipmentMonitor do
|
|||
|
||||
case {old_status, new_status} do
|
||||
{_, :down} ->
|
||||
# Equipment went down - create alert if one doesn't exist
|
||||
if !Alerts.has_active_alert?(equipment.id, :equipment_down) do
|
||||
alert_message =
|
||||
if equipment.snmp_enabled do
|
||||
"Equipment is not responding to SNMP"
|
||||
else
|
||||
"Equipment is not responding to ping"
|
||||
end
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: now,
|
||||
message: alert_message
|
||||
})
|
||||
|
||||
# Send email notification in background (not in test environment)
|
||||
# _ =
|
||||
# if !test_env?() do
|
||||
# Task.start(fn ->
|
||||
# Alerts.send_alert_notification(alert)
|
||||
# end)
|
||||
# end
|
||||
|
||||
# Broadcast alert
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:new",
|
||||
{:new_alert, equipment.id, :equipment_down}
|
||||
)
|
||||
end
|
||||
handle_equipment_down(equipment, now)
|
||||
|
||||
{_, :up} ->
|
||||
# Equipment came back up - create recovery alert and resolve down alert
|
||||
recovery_message =
|
||||
if equipment.snmp_enabled do
|
||||
"Equipment is now responding to SNMP"
|
||||
else
|
||||
"Equipment is now responding to ping"
|
||||
end
|
||||
handle_equipment_up(equipment, now)
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
triggered_at: now,
|
||||
message: recovery_message
|
||||
})
|
||||
defp handle_equipment_down(equipment, now) do
|
||||
if Alerts.has_active_alert?(equipment.id, :equipment_down) do
|
||||
:ok
|
||||
else
|
||||
create_equipment_down_alert(equipment, now)
|
||||
end
|
||||
end
|
||||
|
||||
# Send email notification in background (not in test environment)
|
||||
# _ =
|
||||
# if !test_env?() do
|
||||
# Task.start(fn ->
|
||||
# Alerts.send_alert_notification(alert)
|
||||
# end)
|
||||
# end
|
||||
defp create_equipment_down_alert(equipment, now) do
|
||||
alert_message = get_down_alert_message(equipment)
|
||||
|
||||
# Resolve any active equipment_down alerts
|
||||
_ =
|
||||
case Alerts.get_active_alert(equipment.id, :equipment_down) do
|
||||
nil ->
|
||||
:ok
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_down,
|
||||
triggered_at: now,
|
||||
message: alert_message
|
||||
})
|
||||
|
||||
alert ->
|
||||
Alerts.resolve_alert(alert)
|
||||
end
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:new",
|
||||
{:new_alert, equipment.id, :equipment_down}
|
||||
)
|
||||
end
|
||||
|
||||
# Broadcast recovery
|
||||
_ =
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:resolved",
|
||||
{:alert_resolved, equipment.id, :equipment_down}
|
||||
)
|
||||
defp get_down_alert_message(equipment) do
|
||||
if equipment.snmp_enabled do
|
||||
"Equipment is not responding to SNMP"
|
||||
else
|
||||
"Equipment is not responding to ping"
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_up(equipment, now) do
|
||||
recovery_message = get_recovery_message(equipment)
|
||||
|
||||
{:ok, _alert} =
|
||||
Alerts.create_alert(%{
|
||||
equipment_id: equipment.id,
|
||||
alert_type: :equipment_up,
|
||||
triggered_at: now,
|
||||
message: recovery_message
|
||||
})
|
||||
|
||||
resolve_down_alert(equipment)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"alerts:resolved",
|
||||
{:alert_resolved, equipment.id, :equipment_down}
|
||||
)
|
||||
end
|
||||
|
||||
defp get_recovery_message(equipment) do
|
||||
if equipment.snmp_enabled do
|
||||
"Equipment is now responding to SNMP"
|
||||
else
|
||||
"Equipment is now responding to ping"
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_down_alert(equipment) do
|
||||
case Alerts.get_active_alert(equipment.id, :equipment_down) do
|
||||
nil -> :ok
|
||||
alert -> Alerts.resolve_alert(alert)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
alias Towerops.Snmp
|
||||
alias Towerops.Snmp.Client
|
||||
alias Towerops.Snmp.PollerRegistry
|
||||
alias Towerops.Snmp.Sensor
|
||||
|
||||
require Logger
|
||||
|
||||
|
|
@ -96,126 +97,133 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
equipment = Equipment.get_equipment!(equipment_id)
|
||||
|
||||
if equipment.snmp_enabled do
|
||||
device = Snmp.get_device_with_associations(equipment_id)
|
||||
|
||||
if device do
|
||||
# Check if recently polled by another pod (distributed coordination)
|
||||
poll_interval = get_poll_interval(equipment)
|
||||
grace_period = 5
|
||||
|
||||
if should_skip_poll?(equipment, poll_interval, grace_period) do
|
||||
Logger.debug("Skipping poll for #{equipment.name} - recently polled by another process")
|
||||
# Update poll timestamp before polling (optimistic locking)
|
||||
|
||||
# Poll all sensors
|
||||
|
||||
# Poll all interfaces
|
||||
else
|
||||
Equipment.update_snmp_poll_time(equipment)
|
||||
|
||||
client_opts = build_client_opts(equipment)
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
try do
|
||||
poll_sensors(device.sensors, client_opts, now)
|
||||
Logger.debug("Polled #{length(device.sensors)} sensors for #{equipment.name}")
|
||||
rescue
|
||||
error ->
|
||||
Logger.error(
|
||||
"Error polling sensors for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}"
|
||||
)
|
||||
end
|
||||
|
||||
try do
|
||||
poll_interfaces(device.interfaces, client_opts, now)
|
||||
Logger.debug("Polled #{length(device.interfaces)} interfaces for #{equipment.name}")
|
||||
rescue
|
||||
error ->
|
||||
Logger.error(
|
||||
"Error polling interfaces for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}"
|
||||
)
|
||||
end
|
||||
|
||||
try do
|
||||
check_interface_changes(device.interfaces, equipment, client_opts, now)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error(
|
||||
"Error checking interface changes for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Schedule next poll regardless of whether we polled or skipped
|
||||
schedule_next_poll(poll_interval)
|
||||
end
|
||||
poll_equipment_device(equipment_id, equipment)
|
||||
end
|
||||
end
|
||||
|
||||
defp poll_equipment_device(equipment_id, equipment) do
|
||||
device = Snmp.get_device_with_associations(equipment_id)
|
||||
|
||||
if device do
|
||||
poll_interval = get_poll_interval(equipment)
|
||||
execute_device_poll(device, equipment, poll_interval)
|
||||
schedule_next_poll(poll_interval)
|
||||
end
|
||||
end
|
||||
|
||||
defp execute_device_poll(device, equipment, poll_interval) do
|
||||
if should_skip_poll?(equipment, poll_interval, 5) do
|
||||
Logger.debug("Skipping poll for #{equipment.name} - recently polled by another process")
|
||||
else
|
||||
perform_device_data_collection(device, equipment)
|
||||
end
|
||||
end
|
||||
|
||||
defp perform_device_data_collection(device, equipment) do
|
||||
Equipment.update_snmp_poll_time(equipment)
|
||||
|
||||
client_opts = build_client_opts(equipment)
|
||||
now = DateTime.truncate(DateTime.utc_now(), :second)
|
||||
|
||||
poll_device_sensors(device, equipment, client_opts, now)
|
||||
poll_device_interfaces(device, equipment, client_opts, now)
|
||||
check_device_interface_changes(device, equipment, client_opts, now)
|
||||
end
|
||||
|
||||
defp poll_device_sensors(device, equipment, client_opts, now) do
|
||||
poll_sensors(device.sensors, client_opts, now)
|
||||
Logger.debug("Polled #{length(device.sensors)} sensors for #{equipment.name}")
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Error polling sensors for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
|
||||
end
|
||||
|
||||
defp poll_device_interfaces(device, equipment, client_opts, now) do
|
||||
poll_interfaces(device.interfaces, client_opts, now)
|
||||
Logger.debug("Polled #{length(device.interfaces)} interfaces for #{equipment.name}")
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("Error polling interfaces for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
|
||||
end
|
||||
|
||||
defp check_device_interface_changes(device, equipment, client_opts, now) do
|
||||
check_interface_changes(device.interfaces, equipment, client_opts, now)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error(
|
||||
"Error checking interface changes for #{equipment.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}"
|
||||
)
|
||||
end
|
||||
|
||||
defp poll_sensors(sensors, client_opts, timestamp) do
|
||||
Enum.each(sensors, fn sensor ->
|
||||
# Check if sensor requires special calculation (e.g., storage percentage)
|
||||
result =
|
||||
if sensor.metadata["calculation"] == "percentage" do
|
||||
poll_percentage_sensor(sensor, client_opts)
|
||||
else
|
||||
poll_simple_sensor(sensor, client_opts)
|
||||
end
|
||||
|
||||
case result do
|
||||
{:ok, value} ->
|
||||
Snmp.create_sensor_reading(%{
|
||||
sensor_id: sensor.id,
|
||||
value: value,
|
||||
status: "ok",
|
||||
checked_at: timestamp
|
||||
})
|
||||
|
||||
# Check for sensor value changes and threshold violations
|
||||
detect_sensor_changes(sensor, value, timestamp)
|
||||
|
||||
# Update sensor's last value and timestamp
|
||||
Snmp.update_sensor(sensor, %{
|
||||
last_value: value,
|
||||
last_checked_at: timestamp
|
||||
})
|
||||
|
||||
{:error, :non_numeric} ->
|
||||
Logger.debug("Non-numeric SNMP value for sensor #{sensor.sensor_descr}")
|
||||
|
||||
Snmp.create_sensor_reading(%{
|
||||
sensor_id: sensor.id,
|
||||
value: nil,
|
||||
status: "unknown",
|
||||
checked_at: timestamp
|
||||
})
|
||||
|
||||
{:error, reason} ->
|
||||
# Only log warnings for actual communication errors, not missing OIDs
|
||||
case reason do
|
||||
:no_such_object ->
|
||||
Logger.debug("Sensor #{sensor.sensor_descr} OID not found on device")
|
||||
|
||||
:no_such_instance ->
|
||||
Logger.debug("Sensor #{sensor.sensor_descr} instance not found on device")
|
||||
|
||||
:end_of_mib_view ->
|
||||
Logger.debug("Sensor #{sensor.sensor_descr} end of MIB view")
|
||||
|
||||
_ ->
|
||||
Logger.warning("Failed to poll sensor #{sensor.sensor_descr}: #{inspect(reason)}")
|
||||
end
|
||||
|
||||
Snmp.create_sensor_reading(%{
|
||||
sensor_id: sensor.id,
|
||||
value: nil,
|
||||
status: "error",
|
||||
checked_at: timestamp
|
||||
})
|
||||
end
|
||||
result = poll_sensor_value(sensor, client_opts)
|
||||
handle_sensor_poll_result(sensor, result, timestamp)
|
||||
end)
|
||||
end
|
||||
|
||||
defp poll_sensor_value(sensor, client_opts) do
|
||||
if sensor.metadata["calculation"] == "percentage" do
|
||||
poll_percentage_sensor(sensor, client_opts)
|
||||
else
|
||||
poll_simple_sensor(sensor, client_opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_sensor_poll_result(sensor, {:ok, value}, timestamp) do
|
||||
Snmp.create_sensor_reading(%{
|
||||
sensor_id: sensor.id,
|
||||
value: value,
|
||||
status: "ok",
|
||||
checked_at: timestamp
|
||||
})
|
||||
|
||||
detect_sensor_changes(sensor, value, timestamp)
|
||||
|
||||
Snmp.update_sensor(sensor, %{
|
||||
last_value: value,
|
||||
last_checked_at: timestamp
|
||||
})
|
||||
end
|
||||
|
||||
defp handle_sensor_poll_result(sensor, {:error, :non_numeric}, timestamp) do
|
||||
Logger.debug("Non-numeric SNMP value for sensor #{sensor.sensor_descr}")
|
||||
|
||||
Snmp.create_sensor_reading(%{
|
||||
sensor_id: sensor.id,
|
||||
value: nil,
|
||||
status: "unknown",
|
||||
checked_at: timestamp
|
||||
})
|
||||
end
|
||||
|
||||
defp handle_sensor_poll_result(sensor, {:error, reason}, timestamp) do
|
||||
log_sensor_error(sensor, reason)
|
||||
|
||||
Snmp.create_sensor_reading(%{
|
||||
sensor_id: sensor.id,
|
||||
value: nil,
|
||||
status: "error",
|
||||
checked_at: timestamp
|
||||
})
|
||||
end
|
||||
|
||||
defp log_sensor_error(sensor, :no_such_object) do
|
||||
Logger.debug("Sensor #{sensor.sensor_descr} OID not found on device")
|
||||
end
|
||||
|
||||
defp log_sensor_error(sensor, :no_such_instance) do
|
||||
Logger.debug("Sensor #{sensor.sensor_descr} instance not found on device")
|
||||
end
|
||||
|
||||
defp log_sensor_error(sensor, :end_of_mib_view) do
|
||||
Logger.debug("Sensor #{sensor.sensor_descr} end of MIB view")
|
||||
end
|
||||
|
||||
defp log_sensor_error(sensor, reason) do
|
||||
Logger.warning("Failed to poll sensor #{sensor.sensor_descr}: #{inspect(reason)}")
|
||||
end
|
||||
|
||||
defp poll_simple_sensor(sensor, client_opts) do
|
||||
case Client.get(client_opts, sensor.sensor_oid) do
|
||||
{:ok, raw_value} ->
|
||||
|
|
@ -605,7 +613,7 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
end
|
||||
end
|
||||
|
||||
@spec detect_sensor_changes(Towerops.Snmp.Sensor.t(), float(), DateTime.t()) :: :ok
|
||||
@spec detect_sensor_changes(Sensor.t(), float(), DateTime.t()) :: :ok
|
||||
defp detect_sensor_changes(sensor, current_value, timestamp) do
|
||||
# Skip change detection if there's no previous value to compare against
|
||||
if sensor.last_value == nil do
|
||||
|
|
@ -644,58 +652,111 @@ defmodule Towerops.Snmp.PollerWorker do
|
|||
if threshold_event, do: [threshold_event | events], else: events
|
||||
end
|
||||
|
||||
@spec check_threshold_violation(
|
||||
Sensor.t(),
|
||||
float(),
|
||||
DateTime.t(),
|
||||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_threshold_violation(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
cond do
|
||||
thresholds.critical_high && current_value >= thresholds.critical_high ->
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
"critical_high",
|
||||
thresholds.critical_high,
|
||||
"critical",
|
||||
"critically high"
|
||||
)
|
||||
threshold_checks = [
|
||||
&check_critical_high/5,
|
||||
&check_critical_low/5,
|
||||
&check_warning_high/5,
|
||||
&check_warning_low/5
|
||||
]
|
||||
|
||||
thresholds.critical_low && current_value <= thresholds.critical_low ->
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
"critical_low",
|
||||
thresholds.critical_low,
|
||||
"critical",
|
||||
"critically low"
|
||||
)
|
||||
Enum.find_value(threshold_checks, fn check_fn ->
|
||||
check_fn.(sensor, current_value, timestamp, equipment_id, thresholds)
|
||||
end) || check_returned_to_normal(sensor, current_value, timestamp, equipment_id, thresholds)
|
||||
end
|
||||
|
||||
thresholds.warning_high && current_value >= thresholds.warning_high ->
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
"warning_high",
|
||||
thresholds.warning_high,
|
||||
"warning",
|
||||
"high"
|
||||
)
|
||||
@spec check_critical_high(
|
||||
Sensor.t(),
|
||||
float(),
|
||||
DateTime.t(),
|
||||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_critical_high(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
if thresholds.critical_high && current_value >= thresholds.critical_high do
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
"critical_high",
|
||||
thresholds.critical_high,
|
||||
"critical",
|
||||
"critically high"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
thresholds.warning_low && current_value <= thresholds.warning_low ->
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
"warning_low",
|
||||
thresholds.warning_low,
|
||||
"warning",
|
||||
"low"
|
||||
)
|
||||
@spec check_critical_low(
|
||||
Sensor.t(),
|
||||
float(),
|
||||
DateTime.t(),
|
||||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_critical_low(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
if thresholds.critical_low && current_value <= thresholds.critical_low do
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
"critical_low",
|
||||
thresholds.critical_low,
|
||||
"critical",
|
||||
"critically low"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
true ->
|
||||
check_returned_to_normal(sensor, current_value, timestamp, equipment_id, thresholds)
|
||||
@spec check_warning_high(
|
||||
Sensor.t(),
|
||||
float(),
|
||||
DateTime.t(),
|
||||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_warning_high(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
if thresholds.warning_high && current_value >= thresholds.warning_high do
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
"warning_high",
|
||||
thresholds.warning_high,
|
||||
"warning",
|
||||
"high"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_warning_low(
|
||||
Sensor.t(),
|
||||
float(),
|
||||
DateTime.t(),
|
||||
Ecto.UUID.t(),
|
||||
map()
|
||||
) :: map() | nil
|
||||
defp check_warning_low(sensor, current_value, timestamp, equipment_id, thresholds) do
|
||||
if thresholds.warning_low && current_value <= thresholds.warning_low do
|
||||
build_threshold_event(
|
||||
sensor,
|
||||
current_value,
|
||||
timestamp,
|
||||
equipment_id,
|
||||
"warning_low",
|
||||
thresholds.warning_low,
|
||||
"warning",
|
||||
"low"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -247,58 +247,66 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
|
|||
end
|
||||
|
||||
defp discover_storage_sensors(client_opts) do
|
||||
# Walk hrStorageTable to get memory and disk usage
|
||||
with {:ok, descr_results} <- Client.walk(client_opts, "1.3.6.1.2.1.25.2.3.1.3"),
|
||||
{:ok, size_results} <- Client.walk(client_opts, "1.3.6.1.2.1.25.2.3.1.5"),
|
||||
{:ok, used_results} <- Client.walk(client_opts, "1.3.6.1.2.1.25.2.3.1.6") do
|
||||
# Get indices
|
||||
indices =
|
||||
descr_results
|
||||
|> Map.keys()
|
||||
|> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end)
|
||||
|
||||
indices
|
||||
|> Enum.map(fn index ->
|
||||
descr_oid = "1.3.6.1.2.1.25.2.3.1.3.#{index}"
|
||||
size_oid = "1.3.6.1.2.1.25.2.3.1.5.#{index}"
|
||||
used_oid = "1.3.6.1.2.1.25.2.3.1.6.#{index}"
|
||||
|
||||
descr = Map.get(descr_results, descr_oid, "")
|
||||
size = Map.get(size_results, size_oid, 0)
|
||||
used = Map.get(used_results, used_oid, 0)
|
||||
|
||||
if size > 0 do
|
||||
percent = used / size * 100
|
||||
|
||||
type =
|
||||
cond do
|
||||
String.contains?(descr, "memory") -> "memory"
|
||||
String.contains?(descr, "disk") -> "disk"
|
||||
true -> "storage"
|
||||
end
|
||||
|
||||
%{
|
||||
sensor_type: "#{type}_usage",
|
||||
sensor_index: index,
|
||||
sensor_oid: used_oid,
|
||||
sensor_descr: descr,
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1,
|
||||
last_value: percent,
|
||||
status: storage_status(type, percent),
|
||||
metadata: %{
|
||||
size_oid: size_oid,
|
||||
calculation: "percentage"
|
||||
}
|
||||
}
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
indices = extract_storage_indices(descr_results)
|
||||
build_storage_sensors(indices, descr_results, size_results, used_results)
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_storage_indices(descr_results) do
|
||||
descr_results
|
||||
|> Map.keys()
|
||||
|> Enum.map(fn oid -> oid |> String.split(".") |> List.last() end)
|
||||
end
|
||||
|
||||
defp build_storage_sensors(indices, descr_results, size_results, used_results) do
|
||||
indices
|
||||
|> Enum.map(&build_storage_sensor(&1, descr_results, size_results, used_results))
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp build_storage_sensor(index, descr_results, size_results, used_results) do
|
||||
descr_oid = "1.3.6.1.2.1.25.2.3.1.3.#{index}"
|
||||
size_oid = "1.3.6.1.2.1.25.2.3.1.5.#{index}"
|
||||
used_oid = "1.3.6.1.2.1.25.2.3.1.6.#{index}"
|
||||
|
||||
descr = Map.get(descr_results, descr_oid, "")
|
||||
size = Map.get(size_results, size_oid, 0)
|
||||
used = Map.get(used_results, used_oid, 0)
|
||||
|
||||
if size > 0 do
|
||||
percent = used / size * 100
|
||||
type = determine_storage_type(descr)
|
||||
|
||||
%{
|
||||
sensor_type: "#{type}_usage",
|
||||
sensor_index: index,
|
||||
sensor_oid: used_oid,
|
||||
sensor_descr: descr,
|
||||
sensor_unit: "%",
|
||||
sensor_divisor: 1,
|
||||
last_value: percent,
|
||||
status: storage_status(type, percent),
|
||||
metadata: %{
|
||||
size_oid: size_oid,
|
||||
calculation: "percentage"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defp determine_storage_type(descr) do
|
||||
cond do
|
||||
String.contains?(descr, "memory") -> "memory"
|
||||
String.contains?(descr, "disk") -> "disk"
|
||||
true -> "storage"
|
||||
end
|
||||
end
|
||||
|
||||
defp discover_poe_sensors(client_opts) do
|
||||
# Walk POE table to get per-port POE statistics
|
||||
# Table columns: Name(3), Status(4), Voltage(5), Current(6), Power(7)
|
||||
|
|
|
|||
|
|
@ -132,14 +132,7 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
defp save_equipment(socket, :new, equipment_params) do
|
||||
case Equipment.create_equipment(equipment_params) do
|
||||
{:ok, equipment} ->
|
||||
# Start SNMP discovery in background if enabled
|
||||
flash_message =
|
||||
if equipment.snmp_enabled do
|
||||
Task.start(fn -> Snmp.discover_equipment(equipment) end)
|
||||
"Equipment created successfully. SNMP discovery started in background."
|
||||
else
|
||||
"Equipment created successfully"
|
||||
end
|
||||
flash_message = handle_equipment_creation(equipment)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|
|
@ -156,19 +149,7 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
|
||||
case Equipment.update_equipment(old_equipment, equipment_params) do
|
||||
{:ok, equipment} ->
|
||||
# Start SNMP discovery if SNMP was just enabled or configuration changed
|
||||
flash_message =
|
||||
if equipment.snmp_enabled and
|
||||
(!old_equipment.snmp_enabled or
|
||||
equipment.snmp_community != old_equipment.snmp_community or
|
||||
equipment.snmp_version != old_equipment.snmp_version or
|
||||
equipment.snmp_port != old_equipment.snmp_port or
|
||||
equipment.ip_address != old_equipment.ip_address) do
|
||||
Task.start(fn -> Snmp.discover_equipment(equipment) end)
|
||||
"Equipment updated successfully. SNMP discovery started in background."
|
||||
else
|
||||
"Equipment updated successfully"
|
||||
end
|
||||
flash_message = handle_equipment_update(old_equipment, equipment)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|
|
@ -180,6 +161,37 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|
|||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_creation(equipment) do
|
||||
if equipment.snmp_enabled do
|
||||
Task.start(fn -> Snmp.discover_equipment(equipment) end)
|
||||
"Equipment created successfully. SNMP discovery started in background."
|
||||
else
|
||||
"Equipment created successfully"
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_equipment_update(old_equipment, equipment) do
|
||||
if should_trigger_snmp_discovery?(old_equipment, equipment) do
|
||||
Task.start(fn -> Snmp.discover_equipment(equipment) end)
|
||||
"Equipment updated successfully. SNMP discovery started in background."
|
||||
else
|
||||
"Equipment updated successfully"
|
||||
end
|
||||
end
|
||||
|
||||
defp should_trigger_snmp_discovery?(old_equipment, equipment) do
|
||||
equipment.snmp_enabled and
|
||||
(!old_equipment.snmp_enabled or
|
||||
snmp_config_changed?(old_equipment, equipment))
|
||||
end
|
||||
|
||||
defp snmp_config_changed?(old_equipment, equipment) do
|
||||
equipment.snmp_community != old_equipment.snmp_community or
|
||||
equipment.snmp_version != old_equipment.snmp_version or
|
||||
equipment.snmp_port != old_equipment.snmp_port or
|
||||
equipment.ip_address != old_equipment.ip_address
|
||||
end
|
||||
|
||||
@spec extract_snmp_config(Phoenix.HTML.Form.t()) :: %{
|
||||
ip_address: String.t() | nil,
|
||||
snmp_community: String.t() | nil,
|
||||
|
|
|
|||
|
|
@ -239,122 +239,129 @@ defmodule ToweropsWeb.EquipmentLive.Show do
|
|||
|
||||
defp load_sensor_chart_data(equipment_id, sensor_types) when is_list(sensor_types) do
|
||||
case Snmp.get_device_with_associations(equipment_id) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
device ->
|
||||
# Find all sensors of the specified types
|
||||
sensors =
|
||||
device.sensors
|
||||
|> Enum.filter(&(&1.sensor_type in sensor_types))
|
||||
|> Enum.sort_by(& &1.sensor_index)
|
||||
|
||||
if Enum.empty?(sensors) do
|
||||
nil
|
||||
else
|
||||
# Get readings for past 24 hours for each sensor
|
||||
twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||
|
||||
datasets =
|
||||
Enum.map(sensors, fn sensor ->
|
||||
readings =
|
||||
sensor.id
|
||||
|> Snmp.get_sensor_readings(
|
||||
since: twenty_four_hours_ago,
|
||||
limit: 1000
|
||||
)
|
||||
|> Enum.reverse()
|
||||
|
||||
%{
|
||||
label: sensor.sensor_descr,
|
||||
data:
|
||||
Enum.map(readings, fn reading ->
|
||||
%{
|
||||
x: DateTime.to_unix(reading.checked_at, :millisecond),
|
||||
y: if(reading.value, do: Float.round(reading.value, 1))
|
||||
}
|
||||
end)
|
||||
}
|
||||
end)
|
||||
|
||||
Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
nil -> nil
|
||||
device -> build_sensor_datasets_json(device, sensor_types)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_sensor_datasets_json(device, sensor_types) 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
|
||||
twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||
datasets = Enum.map(sensors, &sensor_to_chart_dataset(&1, twenty_four_hours_ago))
|
||||
Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
end
|
||||
|
||||
defp sensor_to_chart_dataset(sensor, since) do
|
||||
readings =
|
||||
sensor.id
|
||||
|> Snmp.get_sensor_readings(since: since, limit: 1000)
|
||||
|> Enum.reverse()
|
||||
|
||||
%{
|
||||
label: sensor.sensor_descr,
|
||||
data: Enum.map(readings, &reading_to_data_point/1)
|
||||
}
|
||||
end
|
||||
|
||||
defp reading_to_data_point(reading) do
|
||||
%{
|
||||
x: DateTime.to_unix(reading.checked_at, :millisecond),
|
||||
y: if(reading.value, do: Float.round(reading.value, 1))
|
||||
}
|
||||
end
|
||||
|
||||
defp load_overall_traffic_chart_data(equipment_id) do
|
||||
case Snmp.get_device_with_associations(equipment_id) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
device ->
|
||||
if Enum.empty?(device.interfaces) do
|
||||
nil
|
||||
else
|
||||
# Get stats for past 24 hours for all interfaces
|
||||
twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||
|
||||
# Get all stats for all interfaces
|
||||
all_stats =
|
||||
device.interfaces
|
||||
|> Enum.flat_map(fn interface ->
|
||||
stats =
|
||||
interface.id
|
||||
|> Snmp.get_interface_stats(since: twenty_four_hours_ago, limit: 1000)
|
||||
|> Enum.reverse()
|
||||
|
||||
Enum.map(stats, fn stat ->
|
||||
{stat.checked_at, stat.if_in_octets, stat.if_out_octets}
|
||||
end)
|
||||
end)
|
||||
|> Enum.group_by(&elem(&1, 0))
|
||||
|> Enum.map(fn {timestamp, stats} ->
|
||||
total_in = Enum.reduce(stats, 0, fn {_, in_octets, _}, acc -> acc + (in_octets || 0) end)
|
||||
total_out = Enum.reduce(stats, 0, fn {_, _, out_octets}, acc -> acc + (out_octets || 0) end)
|
||||
{timestamp, total_in, total_out}
|
||||
end)
|
||||
|> Enum.sort_by(&elem(&1, 0), DateTime)
|
||||
|
||||
if Enum.empty?(all_stats) do
|
||||
nil
|
||||
else
|
||||
# Calculate bits per second from counter differences
|
||||
# Inbound shown as negative (below zero axis)
|
||||
in_data =
|
||||
all_stats
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{t1, in1, _out1}, {t2, in2, _out2}] ->
|
||||
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
|
||||
bps = ((in2 - in1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
|
||||
|
||||
%{
|
||||
x: DateTime.to_unix(t2, :millisecond),
|
||||
y: -bps
|
||||
}
|
||||
end)
|
||||
|
||||
# Outbound shown as positive (above zero axis)
|
||||
out_data =
|
||||
all_stats
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{t1, _in1, out1}, {t2, _in2, out2}] ->
|
||||
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
|
||||
bps = ((out2 - out1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
|
||||
|
||||
%{
|
||||
x: DateTime.to_unix(t2, :millisecond),
|
||||
y: bps
|
||||
}
|
||||
end)
|
||||
|
||||
datasets = [
|
||||
%{label: "Outbound", data: out_data},
|
||||
%{label: "Inbound", data: in_data}
|
||||
]
|
||||
|
||||
Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
end
|
||||
nil -> nil
|
||||
device -> build_overall_traffic_chart_json(device)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_overall_traffic_chart_json(device) do
|
||||
if Enum.empty?(device.interfaces) do
|
||||
nil
|
||||
else
|
||||
twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)
|
||||
all_stats = aggregate_interface_traffic_stats(device.interfaces, twenty_four_hours_ago)
|
||||
|
||||
if Enum.empty?(all_stats) do
|
||||
nil
|
||||
else
|
||||
build_traffic_json_datasets(all_stats)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp aggregate_interface_traffic_stats(interfaces, since) do
|
||||
interfaces
|
||||
|> Enum.flat_map(fn interface ->
|
||||
stats =
|
||||
interface.id
|
||||
|> Snmp.get_interface_stats(since: since, limit: 1000)
|
||||
|> Enum.reverse()
|
||||
|
||||
Enum.map(stats, &interface_stat_to_tuple/1)
|
||||
end)
|
||||
|> Enum.group_by(&elem(&1, 0))
|
||||
|> Enum.map(&sum_traffic_stats_by_timestamp/1)
|
||||
|> Enum.sort_by(&elem(&1, 0), DateTime)
|
||||
end
|
||||
|
||||
defp interface_stat_to_tuple(stat) do
|
||||
{stat.checked_at, stat.if_in_octets, stat.if_out_octets}
|
||||
end
|
||||
|
||||
defp sum_traffic_stats_by_timestamp({timestamp, stats}) do
|
||||
total_in = Enum.reduce(stats, 0, fn {_, in_octets, _}, acc -> acc + (in_octets || 0) end)
|
||||
total_out = Enum.reduce(stats, 0, fn {_, _, out_octets}, acc -> acc + (out_octets || 0) end)
|
||||
{timestamp, total_in, total_out}
|
||||
end
|
||||
|
||||
defp build_traffic_json_datasets(all_stats) do
|
||||
in_data = calculate_bps_data(all_stats, :inbound)
|
||||
out_data = calculate_bps_data(all_stats, :outbound)
|
||||
|
||||
datasets = [
|
||||
%{label: "Outbound", data: out_data},
|
||||
%{label: "Inbound", data: in_data}
|
||||
]
|
||||
|
||||
Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
|
||||
defp calculate_bps_data(all_stats, :inbound) do
|
||||
all_stats
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{t1, in1, _out1}, {t2, in2, _out2}] ->
|
||||
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
|
||||
bps = ((in2 - in1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
|
||||
|
||||
%{
|
||||
x: DateTime.to_unix(t2, :millisecond),
|
||||
y: -bps
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp calculate_bps_data(all_stats, :outbound) do
|
||||
all_stats
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{t1, _in1, out1}, {t2, _in2, out2}] ->
|
||||
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
|
||||
bps = ((out2 - out1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
|
||||
|
||||
%{
|
||||
x: DateTime.to_unix(t2, :millisecond),
|
||||
y: bps
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -89,45 +89,45 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
nil
|
||||
|
||||
device ->
|
||||
# Find all sensors of the specified types
|
||||
sensors =
|
||||
device.sensors
|
||||
|> Enum.filter(&(&1.sensor_type in sensor_types))
|
||||
|> Enum.sort_by(& &1.sensor_index)
|
||||
|
||||
if Enum.empty?(sensors) do
|
||||
nil
|
||||
else
|
||||
# Get readings based on range
|
||||
since = get_datetime_from_range(range)
|
||||
|
||||
datasets =
|
||||
Enum.map(sensors, fn sensor ->
|
||||
readings =
|
||||
sensor.id
|
||||
|> Snmp.get_sensor_readings(
|
||||
since: since,
|
||||
limit: get_limit_for_range(range)
|
||||
)
|
||||
|> Enum.reverse()
|
||||
|
||||
%{
|
||||
label: sensor.sensor_descr,
|
||||
data:
|
||||
Enum.map(readings, fn reading ->
|
||||
%{
|
||||
x: DateTime.to_unix(reading.checked_at, :millisecond),
|
||||
y: if(reading.value, do: Float.round(reading.value, 1))
|
||||
}
|
||||
end)
|
||||
}
|
||||
end)
|
||||
|
||||
Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
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)
|
||||
|
|
@ -146,81 +146,89 @@ defmodule ToweropsWeb.GraphLive.Show do
|
|||
|
||||
defp load_traffic_chart_data(equipment_id, range) do
|
||||
case Snmp.get_device_with_associations(equipment_id) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
device ->
|
||||
if Enum.empty?(device.interfaces) do
|
||||
nil
|
||||
else
|
||||
since = get_datetime_from_range(range)
|
||||
limit = get_limit_for_range(range)
|
||||
|
||||
# Get all stats for all interfaces
|
||||
all_stats =
|
||||
device.interfaces
|
||||
|> Enum.flat_map(fn interface ->
|
||||
stats =
|
||||
interface.id
|
||||
|> Snmp.get_interface_stats(since: since, limit: limit)
|
||||
|> Enum.reverse()
|
||||
|
||||
Enum.map(stats, fn stat ->
|
||||
{stat.checked_at, stat.if_in_octets, stat.if_out_octets}
|
||||
end)
|
||||
end)
|
||||
|> Enum.group_by(&elem(&1, 0))
|
||||
|> Enum.map(fn {timestamp, stats} ->
|
||||
total_in =
|
||||
Enum.reduce(stats, 0, fn {_, in_octets, _}, acc -> acc + (in_octets || 0) end)
|
||||
|
||||
total_out =
|
||||
Enum.reduce(stats, 0, fn {_, _, out_octets}, acc -> acc + (out_octets || 0) end)
|
||||
|
||||
{timestamp, total_in, total_out}
|
||||
end)
|
||||
|> Enum.sort_by(&elem(&1, 0), DateTime)
|
||||
|
||||
if Enum.empty?(all_stats) do
|
||||
nil
|
||||
else
|
||||
# Calculate bits per second from counter differences
|
||||
# Inbound shown as negative (below zero axis)
|
||||
in_data =
|
||||
all_stats
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{t1, in1, _out1}, {t2, in2, _out2}] ->
|
||||
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
|
||||
bps = ((in2 - in1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
|
||||
|
||||
%{
|
||||
x: DateTime.to_unix(t2, :millisecond),
|
||||
y: -bps
|
||||
}
|
||||
end)
|
||||
|
||||
# Outbound shown as positive (above zero axis)
|
||||
out_data =
|
||||
all_stats
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{t1, _in1, out1}, {t2, _in2, out2}] ->
|
||||
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
|
||||
bps = ((out2 - out1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
|
||||
|
||||
%{
|
||||
x: DateTime.to_unix(t2, :millisecond),
|
||||
y: bps
|
||||
}
|
||||
end)
|
||||
|
||||
datasets = [
|
||||
%{label: "Outbound", data: out_data},
|
||||
%{label: "Inbound", data: in_data}
|
||||
]
|
||||
|
||||
Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
end
|
||||
nil -> nil
|
||||
device -> build_traffic_chart_json(device, range)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_traffic_chart_json(device, range) do
|
||||
if Enum.empty?(device.interfaces) do
|
||||
nil
|
||||
else
|
||||
since = get_datetime_from_range(range)
|
||||
limit = get_limit_for_range(range)
|
||||
all_stats = aggregate_interface_stats(device.interfaces, since, limit)
|
||||
|
||||
if Enum.empty?(all_stats) do
|
||||
nil
|
||||
else
|
||||
build_traffic_datasets(all_stats)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp aggregate_interface_stats(interfaces, since, limit) do
|
||||
interfaces
|
||||
|> Enum.flat_map(fn interface ->
|
||||
stats =
|
||||
interface.id
|
||||
|> Snmp.get_interface_stats(since: since, limit: limit)
|
||||
|> Enum.reverse()
|
||||
|
||||
Enum.map(stats, &stat_to_tuple/1)
|
||||
end)
|
||||
|> Enum.group_by(&elem(&1, 0))
|
||||
|> Enum.map(&sum_stats_by_timestamp/1)
|
||||
|> Enum.sort_by(&elem(&1, 0), DateTime)
|
||||
end
|
||||
|
||||
defp stat_to_tuple(stat) do
|
||||
{stat.checked_at, stat.if_in_octets, stat.if_out_octets}
|
||||
end
|
||||
|
||||
defp sum_stats_by_timestamp({timestamp, stats}) do
|
||||
total_in = Enum.reduce(stats, 0, fn {_, in_octets, _}, acc -> acc + (in_octets || 0) end)
|
||||
total_out = Enum.reduce(stats, 0, fn {_, _, out_octets}, acc -> acc + (out_octets || 0) end)
|
||||
{timestamp, total_in, total_out}
|
||||
end
|
||||
|
||||
defp build_traffic_datasets(all_stats) do
|
||||
in_data = calculate_traffic_data(all_stats, :inbound)
|
||||
out_data = calculate_traffic_data(all_stats, :outbound)
|
||||
|
||||
datasets = [
|
||||
%{label: "Outbound", data: out_data},
|
||||
%{label: "Inbound", data: in_data}
|
||||
]
|
||||
|
||||
Jason.encode!(%{datasets: datasets})
|
||||
end
|
||||
|
||||
defp calculate_traffic_data(all_stats, :inbound) do
|
||||
all_stats
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{t1, in1, _out1}, {t2, in2, _out2}] ->
|
||||
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
|
||||
bps = ((in2 - in1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
|
||||
|
||||
%{
|
||||
x: DateTime.to_unix(t2, :millisecond),
|
||||
y: -bps
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp calculate_traffic_data(all_stats, :outbound) do
|
||||
all_stats
|
||||
|> Enum.chunk_every(2, 1, :discard)
|
||||
|> Enum.map(fn [{t1, _in1, out1}, {t2, _in2, out2}] ->
|
||||
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
|
||||
bps = ((out2 - out1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
|
||||
|
||||
%{
|
||||
x: DateTime.to_unix(t2, :millisecond),
|
||||
y: bps
|
||||
}
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -416,37 +416,52 @@ defmodule ToweropsWeb.UserAuth do
|
|||
|
||||
defp mount_current_scope(socket, session) do
|
||||
Phoenix.Component.assign_new(socket, :current_scope, fn ->
|
||||
# Check if currently impersonating
|
||||
if session["impersonating"] do
|
||||
superuser_id = session["superuser_id"]
|
||||
target_user_id = session["target_user_id"]
|
||||
|
||||
# Validate we have both IDs before attempting to fetch users
|
||||
if superuser_id && target_user_id do
|
||||
with superuser when not is_nil(superuser) <- Accounts.get_user(superuser_id),
|
||||
target_user when not is_nil(target_user) <- Accounts.get_user(target_user_id) do
|
||||
Scope.for_impersonation(superuser, target_user)
|
||||
else
|
||||
_ -> Scope.for_user(nil)
|
||||
end
|
||||
else
|
||||
# Missing IDs, return nil user
|
||||
Scope.for_user(nil)
|
||||
end
|
||||
else
|
||||
# Normal authentication flow
|
||||
if user_token = session["user_token"] do
|
||||
case Accounts.get_user_by_session_token(user_token) do
|
||||
{user, _token_inserted_at} -> Scope.for_user(user)
|
||||
nil -> Scope.for_user(nil)
|
||||
end
|
||||
else
|
||||
Scope.for_user(nil)
|
||||
end
|
||||
end
|
||||
build_scope_from_session(session)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_scope_from_session(session) do
|
||||
if session["impersonating"] do
|
||||
build_impersonation_scope(session)
|
||||
else
|
||||
build_normal_scope(session)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_impersonation_scope(session) do
|
||||
superuser_id = session["superuser_id"]
|
||||
target_user_id = session["target_user_id"]
|
||||
|
||||
if superuser_id && target_user_id do
|
||||
fetch_impersonation_users(superuser_id, target_user_id)
|
||||
else
|
||||
Scope.for_user(nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_impersonation_users(superuser_id, target_user_id) do
|
||||
with superuser when not is_nil(superuser) <- Accounts.get_user(superuser_id),
|
||||
target_user when not is_nil(target_user) <- Accounts.get_user(target_user_id) do
|
||||
Scope.for_impersonation(superuser, target_user)
|
||||
else
|
||||
_ -> Scope.for_user(nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_normal_scope(session) do
|
||||
case session["user_token"] do
|
||||
nil -> Scope.for_user(nil)
|
||||
user_token -> fetch_user_from_token(user_token)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_user_from_token(user_token) do
|
||||
case Accounts.get_user_by_session_token(user_token) do
|
||||
{user, _token_inserted_at} -> Scope.for_user(user)
|
||||
nil -> Scope.for_user(nil)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Start impersonating a user as a superuser.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue