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
|
||||
feat(insights): superuser "Regenerate insights" button on /insights
|
||||
ToweropsWeb.InsightsLive.Index — new handle_event("regenerate_insights")
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
alias Towerops.Snmp.AgentDiscovery
|
||||
alias Towerops.Snmp.Discovery
|
||||
alias Towerops.Snmp.SensorChangeDetector
|
||||
alias Towerops.Snmp.SensorScale
|
||||
alias Towerops.Topology
|
||||
alias ToweropsWeb.GraphQL.Subscriptions
|
||||
alias ToweropsWeb.RemoteIp
|
||||
|
|
@ -705,7 +706,7 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
# Private helpers
|
||||
|
||||
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),
|
||||
{:ok, result} <- SnmpResult.decode(binary) do
|
||||
|
|
@ -1932,7 +1933,7 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
# Use server time to avoid clock drift issues between agents
|
||||
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"
|
||||
)
|
||||
|
||||
|
|
@ -2140,7 +2141,13 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
normalized_oid = String.trim_leading(sensor.sensor_oid, ".")
|
||||
raw_value = Map.get(oid_values, normalized_oid)
|
||||
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,
|
||||
|
|
@ -2168,7 +2175,9 @@ defmodule ToweropsWeb.AgentChannel do
|
|||
|
||||
with value when not is_nil(value) <- Map.get(oid_values, normalized_oid),
|
||||
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
|
||||
_ -> nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -993,6 +993,36 @@ defmodule ToweropsWeb.AgentChannelTest do
|
|||
assert updated_sensor.last_value == 45.0
|
||||
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", %{
|
||||
socket: socket,
|
||||
device: device,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue