From d9886e8680b839f9eb80ff9ee946cf871c8785ac Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 23 Mar 2026 17:38:24 -0400 Subject: [PATCH] fix: reduce log noise and guard against non-dict SNMP devices - SNMP scheduler: skip non-dict entries in devices list (prevents AttributeError) - UPS scheduler: downgrade NutError from exception to warning (expected when NUT not running) - UniFi scheduler: downgrade login/poll failures from exception to warning (expected when controller unreachable) Co-Authored-By: Claude Sonnet 4.6 --- snmp/scheduler.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++ unifi/scheduler.py | 12 +++---- ups/scheduler.py | 4 +-- 3 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 snmp/scheduler.py diff --git a/snmp/scheduler.py b/snmp/scheduler.py new file mode 100644 index 0000000..b8809c3 --- /dev/null +++ b/snmp/scheduler.py @@ -0,0 +1,78 @@ +# plugins/snmp/scheduler.py +from __future__ import annotations +import asyncio +import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from quart import Quart + +from fabledscryer.core.scheduler import ScheduledTask + +logger = logging.getLogger(__name__) + + +def make_poll_task(app: "Quart") -> ScheduledTask: + interval = app.config["PLUGINS"]["snmp"].get("poll_interval_seconds", 60) + + async def poll() -> None: + await _do_poll(app) + + return ScheduledTask( + name="snmp_poll", + coro_factory=poll, + interval_seconds=int(interval), + run_on_startup=True, + ) + + +async def _do_poll(app: "Quart") -> None: + from datetime import datetime, timezone + from .poller import poll_device_sync + from fabledscryer.core.alerts import record_metric + + devices: list[dict] = app.config["PLUGINS"]["snmp"].get("devices", []) + if not devices: + return + + loop = asyncio.get_event_loop() + now = datetime.now(timezone.utc) + + async with app.db_sessionmaker() as session: + async with session.begin(): + for device in devices: + if not isinstance(device, dict): + continue + name = device.get("name") or device.get("host", "unknown") + host = device.get("host", "") + port = int(device.get("port", 161)) + community = device.get("community", "public") + version = str(device.get("version", "2c")) + oids = device.get("oids", []) + + if not host or not oids: + continue + + try: + readings = await loop.run_in_executor( + None, + poll_device_sync, + host, port, community, version, oids, + ) + except Exception: + logger.exception("SNMP poll failed for device %s (%s)", name, host) + continue + + for label, value in readings.items(): + await record_metric( + session=session, + source_module="snmp", + resource_name=name, + metric_name=label, + value=value, + ) + + if readings: + logger.debug("SNMP polled %s (%s): %d OID(s)", name, host, len(readings)) + else: + logger.debug("SNMP polled %s (%s): no readings", name, host) diff --git a/unifi/scheduler.py b/unifi/scheduler.py index a50efa5..2a6345d 100644 --- a/unifi/scheduler.py +++ b/unifi/scheduler.py @@ -49,8 +49,8 @@ async def _do_poll(app: "Quart") -> None: ) try: await _client.login() - except Exception: - logger.exception("UniFi initial login failed") + except Exception as exc: + logger.warning("UniFi initial login failed: %s", exc) _client = None return @@ -58,8 +58,8 @@ async def _do_poll(app: "Quart") -> None: health = await _client.get_health() clients = await _client.get_active_clients() devices = await _client.get_devices() - except Exception: - logger.exception("UniFi poll failed — will retry next tick") + except Exception as exc: + logger.warning("UniFi poll failed — will retry next tick: %s", exc) _client = None # force re-auth on next tick return @@ -185,8 +185,8 @@ async def _do_poll(app: "Quart") -> None: # ── Expanded data (best-effort, failures don't abort core poll) ─────────── try: await _do_expanded(app, scraped_at) - except Exception: - logger.exception("UniFi expanded poll failed") + except Exception as exc: + logger.warning("UniFi expanded poll failed: %s", exc) async def _do_expanded(app: "Quart", scraped_at: datetime) -> None: diff --git a/ups/scheduler.py b/ups/scheduler.py index 9752abf..8ae3954 100644 --- a/ups/scheduler.py +++ b/ups/scheduler.py @@ -53,8 +53,8 @@ async def _do_poll(app: "Quart") -> None: try: raw_vars = await client.get_vars(cfg.get("ups_name", "ups")) - except NutError: - logger.exception("UPS poll failed") + except NutError as exc: + logger.warning("UPS poll failed: %s", exc) return parsed = parse_status(raw_vars)