Add metadata support for storage sensors

Storage sensors (disk/memory usage) need to fetch two OIDs to calculate
percentages: the 'used' value and the 'size' value. Previously, only the
'used' OID was stored and polled, resulting in meaningless raw values.

Changes:
- Added metadata JSONB field to snmp_sensors table
- Updated Sensor schema to include metadata field
- Updated MikroTik profile to store size_oid in metadata for storage sensors
- Updated PollerWorker to handle percentage calculation sensors:
  - poll_simple_sensor: Standard OID fetch with divisor
  - poll_percentage_sensor: Fetches both used and size OIDs, calculates percentage

Storage sensors now properly calculate and store percentage values during polling.

NOTE: Existing MikroTik devices need to re-run SNMP discovery to populate
the metadata field for storage sensors.
This commit is contained in:
Graham McIntire 2026-01-04 13:09:47 -06:00
parent 9918f1633c
commit bd91e2a7c3
No known key found for this signature in database
4 changed files with 77 additions and 29 deletions

View file

@ -99,40 +99,36 @@ defmodule Towerops.Snmp.PollerWorker do
defp poll_sensors(sensors, client_opts, timestamp) do
Enum.each(sensors, fn sensor ->
case Client.get(client_opts, sensor.sensor_oid) do
{:ok, raw_value} ->
decoded_value = decode_snmp_value(raw_value)
# 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
if is_number(decoded_value) do
# Calculate actual value using divisor
value = decoded_value / sensor.sensor_divisor
case result do
{:ok, value} ->
Snmp.create_sensor_reading(%{
sensor_id: sensor.id,
value: value,
status: "ok",
checked_at: timestamp
})
# Determine status based on value (could be enhanced with thresholds)
status = "ok"
{:error, :non_numeric} ->
Logger.debug("Non-numeric SNMP value for sensor #{sensor.sensor_descr}")
# Save the reading
Snmp.create_sensor_reading(%{
sensor_id: sensor.id,
value: value,
status: status,
checked_at: timestamp
})
else
Logger.debug("Non-numeric SNMP value for sensor #{sensor.sensor_descr}")
# Save reading with nil value
Snmp.create_sensor_reading(%{
sensor_id: sensor.id,
value: nil,
status: "unknown",
checked_at: timestamp
})
end
Snmp.create_sensor_reading(%{
sensor_id: sensor.id,
value: nil,
status: "unknown",
checked_at: timestamp
})
{:error, reason} ->
Logger.warning("Failed to poll sensor #{sensor.sensor_descr}: #{inspect(reason)}")
# Save error status
Snmp.create_sensor_reading(%{
sensor_id: sensor.id,
value: nil,
@ -143,6 +139,43 @@ defmodule Towerops.Snmp.PollerWorker do
end)
end
defp poll_simple_sensor(sensor, client_opts) do
case Client.get(client_opts, sensor.sensor_oid) do
{:ok, raw_value} ->
decoded_value = decode_snmp_value(raw_value)
if is_number(decoded_value) do
# Calculate actual value using divisor
value = decoded_value / sensor.sensor_divisor
{:ok, value}
else
{:error, :non_numeric}
end
{:error, reason} ->
{:error, reason}
end
end
defp poll_percentage_sensor(sensor, client_opts) do
# For percentage sensors (like storage), fetch both used and size
size_oid = sensor.metadata["size_oid"]
used_oid = sensor.sensor_oid
with {:ok, used_raw} <- Client.get(client_opts, used_oid),
{:ok, size_raw} <- Client.get(client_opts, size_oid),
used when is_number(used) <- decode_snmp_value(used_raw),
size when is_number(size) <- decode_snmp_value(size_raw),
true <- size > 0 do
percentage = used / size * 100
{:ok, percentage}
else
{:error, reason} -> {:error, reason}
false -> {:error, :division_by_zero}
_ -> {:error, :non_numeric}
end
end
defp poll_interfaces(interfaces, client_opts, timestamp) do
Enum.each(interfaces, fn interface ->
# Poll interface stats: ifInOctets, ifOutOctets, ifInErrors, ifOutErrors

View file

@ -235,7 +235,11 @@ defmodule Towerops.Snmp.Profiles.Mikrotik do
sensor_unit: "%",
sensor_divisor: 1,
last_value: percent,
status: storage_status(type, percent)
status: storage_status(type, percent),
metadata: %{
size_oid: size_oid,
calculation: "percentage"
}
}
end
end)

View file

@ -16,6 +16,7 @@ defmodule Towerops.Snmp.Sensor do
field :monitored, :boolean, default: true
field :last_value, :float
field :last_checked_at, :utc_datetime
field :metadata, :map, default: %{}
belongs_to :snmp_device, Towerops.Snmp.Device
@ -37,7 +38,8 @@ defmodule Towerops.Snmp.Sensor do
:sensor_divisor,
:monitored,
:last_value,
:last_checked_at
:last_checked_at,
:metadata
])
|> validate_required([:snmp_device_id, :sensor_type, :sensor_index, :sensor_oid])
|> unique_constraint([:snmp_device_id, :sensor_index])

View file

@ -0,0 +1,9 @@
defmodule Towerops.Repo.Migrations.AddSensorMetadata do
use Ecto.Migration
def change do
alter table(:snmp_sensors) do
add :metadata, :map, default: %{}
end
end
end