fix(snmp): scale agent-polled temperatures + quiet two info logs
- ToweropsWeb.AgentChannel now runs sensor readings through Towerops.Snmp.SensorScale.normalize/2 in both resolve_sensor_value/2 and the GraphQL publisher. Without this, MikroTik mtxrHl* deci-degree readings (e.g. Culleoka reporting 294 -> 29.4°C) were stored and alerted on at 294°C. The DevicePollerWorker path already did this; only the agent path was missing it. - Demote "Received SNMP result from agent" and "Processing polling result for X" from info to debug — they fire every poll cycle on every device and were dominating prod logs.
This commit is contained in:
parent
bdfb20efdf
commit
6338556945
3 changed files with 60 additions and 4 deletions
|
|
@ -1,3 +1,20 @@
|
||||||
|
2026-05-10
|
||||||
|
fix(snmp): apply SensorScale on the agent-polled path
|
||||||
|
ToweropsWeb.AgentChannel.resolve_sensor_value/2 and the GraphQL
|
||||||
|
publisher now run readings through Towerops.Snmp.SensorScale.normalize/2
|
||||||
|
before storing/publishing. Previously only the direct DevicePollerWorker
|
||||||
|
path normalized values, so MikroTik mtxrHl* deci-degree readings (e.g.
|
||||||
|
Culleoka router reporting 294 → expected 29.4 °C) were stored and
|
||||||
|
alerted on as 294 °C.
|
||||||
|
Test: agent_channel_test.exs gains "poll result rescales implausibly
|
||||||
|
hot temperature readings" — pushing 294 lands as 29.4 in last_value.
|
||||||
|
|
||||||
|
chore(logs): demote two noisy info → debug
|
||||||
|
AgentChannel "Received SNMP result from agent" and
|
||||||
|
"Processing polling result for X" now log at debug. They fire on
|
||||||
|
every successful agent poll cycle and were dominating production
|
||||||
|
log volume.
|
||||||
|
|
||||||
2026-05-10
|
2026-05-10
|
||||||
feat(insights): superuser "Regenerate insights" button on /insights
|
feat(insights): superuser "Regenerate insights" button on /insights
|
||||||
ToweropsWeb.InsightsLive.Index — new handle_event("regenerate_insights")
|
ToweropsWeb.InsightsLive.Index — new handle_event("regenerate_insights")
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ defmodule ToweropsWeb.AgentChannel do
|
||||||
alias Towerops.Snmp.AgentDiscovery
|
alias Towerops.Snmp.AgentDiscovery
|
||||||
alias Towerops.Snmp.Discovery
|
alias Towerops.Snmp.Discovery
|
||||||
alias Towerops.Snmp.SensorChangeDetector
|
alias Towerops.Snmp.SensorChangeDetector
|
||||||
|
alias Towerops.Snmp.SensorScale
|
||||||
alias Towerops.Topology
|
alias Towerops.Topology
|
||||||
alias ToweropsWeb.GraphQL.Subscriptions
|
alias ToweropsWeb.GraphQL.Subscriptions
|
||||||
alias ToweropsWeb.RemoteIp
|
alias ToweropsWeb.RemoteIp
|
||||||
|
|
@ -705,7 +706,7 @@ defmodule ToweropsWeb.AgentChannel do
|
||||||
# Private helpers
|
# Private helpers
|
||||||
|
|
||||||
defp process_snmp_result_message(binary_b64, socket) do
|
defp process_snmp_result_message(binary_b64, socket) do
|
||||||
Logger.info("Received SNMP result from agent (binary size: #{byte_size(binary_b64)})")
|
Logger.debug("Received SNMP result from agent (binary size: #{byte_size(binary_b64)})")
|
||||||
|
|
||||||
with {:ok, binary} <- safe_base64_decode(binary_b64),
|
with {:ok, binary} <- safe_base64_decode(binary_b64),
|
||||||
{:ok, result} <- SnmpResult.decode(binary) do
|
{:ok, result} <- SnmpResult.decode(binary) do
|
||||||
|
|
@ -1932,7 +1933,7 @@ defmodule ToweropsWeb.AgentChannel do
|
||||||
# Use server time to avoid clock drift issues between agents
|
# Use server time to avoid clock drift issues between agents
|
||||||
timestamp = Towerops.Time.now()
|
timestamp = Towerops.Time.now()
|
||||||
|
|
||||||
Logger.info(
|
Logger.debug(
|
||||||
"Processing polling result for #{device.name}: #{map_size(oid_values)} OIDs, #{length(snmp_device.sensors)} sensors, #{length(snmp_device.interfaces)} interfaces"
|
"Processing polling result for #{device.name}: #{map_size(oid_values)} OIDs, #{length(snmp_device.sensors)} sensors, #{length(snmp_device.interfaces)} interfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -2140,7 +2141,13 @@ defmodule ToweropsWeb.AgentChannel do
|
||||||
normalized_oid = String.trim_leading(sensor.sensor_oid, ".")
|
normalized_oid = String.trim_leading(sensor.sensor_oid, ".")
|
||||||
raw_value = Map.get(oid_values, normalized_oid)
|
raw_value = Map.get(oid_values, normalized_oid)
|
||||||
parsed = parse_float(raw_value)
|
parsed = parse_float(raw_value)
|
||||||
value = if parsed, do: Float.round(parsed / sensor.sensor_divisor, 1)
|
|
||||||
|
value =
|
||||||
|
if parsed do
|
||||||
|
divided = parsed / sensor.sensor_divisor
|
||||||
|
scaled = SensorScale.normalize(sensor.sensor_type, divided)
|
||||||
|
Float.round(scaled / 1.0, 2)
|
||||||
|
end
|
||||||
|
|
||||||
%{
|
%{
|
||||||
sensor_id: sensor.id,
|
sensor_id: sensor.id,
|
||||||
|
|
@ -2168,7 +2175,9 @@ defmodule ToweropsWeb.AgentChannel do
|
||||||
|
|
||||||
with value when not is_nil(value) <- Map.get(oid_values, normalized_oid),
|
with value when not is_nil(value) <- Map.get(oid_values, normalized_oid),
|
||||||
parsed when not is_nil(parsed) <- parse_float(value) do
|
parsed when not is_nil(parsed) <- parse_float(value) do
|
||||||
Float.round(parsed / sensor.sensor_divisor, 1)
|
divided = parsed / sensor.sensor_divisor
|
||||||
|
normalized = SensorScale.normalize(sensor.sensor_type, divided)
|
||||||
|
Float.round(normalized / 1.0, 2)
|
||||||
else
|
else
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -993,6 +993,36 @@ defmodule ToweropsWeb.AgentChannelTest do
|
||||||
assert updated_sensor.last_value == 45.0
|
assert updated_sensor.last_value == 45.0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "poll result rescales implausibly hot temperature readings", %{
|
||||||
|
socket: socket,
|
||||||
|
device: device,
|
||||||
|
sensor: sensor
|
||||||
|
} do
|
||||||
|
# Some MikroTik models (e.g. RB-series with mtxrHl* OIDs) report
|
||||||
|
# temperature in deci-degrees so a "29.4°C" reading lands on the
|
||||||
|
# wire as 294. SensorScale.normalize/2 must run on the agent path
|
||||||
|
# so it doesn't store 294°C.
|
||||||
|
result = %SnmpResult{
|
||||||
|
device_id: device.id,
|
||||||
|
job_type: :POLL,
|
||||||
|
job_id: "poll:#{device.id}",
|
||||||
|
timestamp: DateTime.to_unix(DateTime.utc_now()),
|
||||||
|
oid_values: %{
|
||||||
|
sensor.sensor_oid => "294"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push(socket, "result", encode_payload(result))
|
||||||
|
|
||||||
|
updated_sensor =
|
||||||
|
poll_until(fn ->
|
||||||
|
s = Towerops.Repo.get!(Sensor, sensor.id)
|
||||||
|
if s.last_value && s.last_value < 100.0, do: s
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert_in_delta updated_sensor.last_value, 29.4, 0.01
|
||||||
|
end
|
||||||
|
|
||||||
test "poll result stores interface stats", %{
|
test "poll result stores interface stats", %{
|
||||||
socket: socket,
|
socket: socket,
|
||||||
device: device,
|
device: device,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue