A timeout should not become a zero counter. A missing OID should not mark every metric on the host as unavailable. Keep the original failure boundary in the poll result so the dashboard can distinguish a missing value from missing communication and a broken collector.
Scope and evidence: Python 3.10+ on Linux with Net-SNMP snmpget installed. This example wraps one numeric SNMPv3 authPriv metric per process; it does not depend on a changing Python SNMP SDK. Agent 192.0.2.50, user labPoller and the counter instance are documentation examples. Authentication/privacy settings and secrets already live in the collector account’s protected Net-SNMP configuration. No live agent was polled for this article; fixtures below are synthetic.
Define Python SNMP Results for Values, Missing OIDs and Timeouts
The wrapper returns value, missing_object, missing_instance, no_response, collector_deadline, collector_error or unexpected_value. The value is None for every failure and remains a real integer for a valid zero. Preserve agent and OID on each result. A timeout is an observation from the poller, not proof that the device is down; credentials, ACLs or a path can suppress responses.
The Net-SNMP command reference defines the retry/time limit and v3 options. The output tutorial describes -Oqv. This example pins English diagnostic matching through the C locale and accepts only a single unsigned decimal Counter64 value within its 64-bit range; it intentionally rejects strings, enums and timeticks formats. An API exposing protocol exceptions directly is preferable when broader type support is needed.
Python Example: Collect One SNMP Counter with Bounded Timeouts
Save this Python code as snmp_metric.py. Defining the functions performs no polling. Calling poll_counter invokes a real subprocess and network request, so restrict calls to an approved inventory. Arguments use an array with shell=False; validate inventory and OIDs before a scheduler supplies them. A protected profile is still a secret-bearing file and needs restricted permissions.
import os
import re
import subprocess
def classify(returncode, stdout, stderr):
out = stdout.strip()
text = (stdout + "\n" + stderr).lower()
if "no such object" in text:
return {"status": "missing_object", "value": None}
if "no such instance" in text:
return {"status": "missing_instance", "value": None}
if returncode and "timeout: no response from" in text:
return {"status": "no_response", "value": None}
if returncode:
return {"status": "collector_error", "value": None}
if not re.fullmatch(r"[0-9]+", out):
return {"status": "unexpected_value", "value": None}
value = int(out)
if value > 2**64 - 1:
return {"status": "unexpected_value", "value": None}
return {"status": "value", "value": value}
def poll_counter(agent, oid, runner=subprocess.run):
if not re.fullmatch(r"\.?[0-9]+(?:\.[0-9]+)+", oid):
raise ValueError("Use a numeric OID from approved inventory")
if agent.startswith("-") or not agent:
raise ValueError("Invalid agent")
command = ["snmpget", "-v3", "-l", "authPriv", "-u", "labPoller",
"-t", "2", "-r", "0", "-Oqv", agent, oid]
identity = {"agent": agent, "oid": oid}
try:
reply = runner(command, capture_output=True, text=True,
timeout=5, check=False, shell=False,
env={**os.environ, "LC_ALL": "C"})
except subprocess.TimeoutExpired:
return {**identity, "status": "collector_deadline", "value": None}
except OSError:
return {**identity, "status": "collector_error", "value": None}
return {**identity, **classify(reply.returncode, reply.stdout, reply.stderr)}
-r 0 prevents automatic retries; -t 2 is the request timeout, while Python’s five-second limit bounds a stuck process separately. The outer limit is not a precise global scheduler deadline—process creation and OS scheduling have their own costs. A local deadline cannot establish whether a packet reached the agent. Python documents subprocess timeout and return-code behavior.
The function avoids returning raw stderr because it may reveal operational details. In production, retain sanitized diagnostic codes and bounded local evidence under the collector’s security policy. Do not convert an unrecognized authentication/security failure into no_response unless that is what the client actually reported.
Test SNMP Result Classification with Offline Fixtures
Append these synthetic Python fixtures to the same file and run python snmp_metric.py. The injected runner returns a constructed process result; it never executes snmpget:
if __name__ == "__main__":
fixtures = [
(0, "0\n", "", "value"),
(0, "123456\n", "", "value"),
(0, "No Such Object available on this agent at this OID", "", "missing_object"),
(0, "No Such Instance currently exists at this OID", "", "missing_instance"),
(1, "", "Timeout: No Response from 192.0.2.50", "no_response"),
(1, "", "Authentication failure", "collector_error"),
(0, "unexpected text", "", "unexpected_value"),
]
for rc, out, err, expected in fixtures:
def fake_runner(command, **kwargs):
return subprocess.CompletedProcess(command, rc, out, err)
result = poll_counter("192.0.2.50", ".1.3.6.1.2.1.31.1.1.1.6.7", fake_runner)
assert result["status"] == expected
if out == "0\n":
assert result["value"] == 0
print("7 offline fixtures passed")
Expected output is exactly 7 offline fixtures passed. This verifies the documented classifier for those strings, not compatibility with every Net-SNMP build. The example recognizes only the pinned client’s English forms. Unknown responses remain explicit errors until reviewed; never add a blanket exception handler that returns zero.
Integrate Per-OID SNMP Outcomes into Monitoring and Rollback
Store poll time, elapsed duration, identity and status alongside the value. A host summary may be partial when one object is readable and another is missing. Avoid a label such as host-down unless independent evidence and policy justify it. On a no-response status, follow the SNMP timeout guide; for an exception value, follow the OID/view guide.
Collector rollout change: save the current parser, dashboard mappings and alert rules. Pilot this outcome contract on one approved metric, with concurrency/rate limits and a fixed Net-SNMP version. Keep SNMP credentials out of command arguments and general logs. Add separate collector-health and missing-data handling instead of suppressing all errors. Counter values still need continuity-aware rate conversion; the utilization guide covers that stage.
Rollback restores the saved parser/template and starts a fresh metric baseline. Do not splice a zero-filled legacy interval into a new valid rate calculation. Retain fixture coverage and raw-to-sanitized classification evidence so a future client upgrade can be reviewed before deployment. No SNMP agent configuration change is required by this collector example.