Operators coming from LibreNMS expect the same figures under a port graph. Add them: - Totals: bytes transferred in and out over the selected range (GraphResponse.totals), integrated from the average rate across the stitched raw and hourly tiers without double counting; partial coverage is flagged. - Summaries read Now / Avg / Max / 95th / Total per direction, plus an aggregate (in+out) 95th (p95.aggregate_bps, hot store only). - Utilisation as a percentage of port speed, and a "% of speed" axis. - "Compare previous period" overlay on the graph view and expanded row. - Errors and discards graphs (GET /api/v1/graph/counters), hot-store range only since they have no rollups. Packet and broadcast/multicast graphs stay disabled: those counters are not polled (DEVIATIONS 56). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017TuKgyKve2XLFtrrXWMi2w
34 KiB
Operating nmon
How to run, secure, back up and maintain a production nmon. Design background: docs/spec/spec.md ("Auth", "Operations").
This page, Adding a device profile and Collection coverage are also built into the binary and shown in the UI under Help (/help, any signed-in user, no network needed). The help page always matches the running version; the Settings forms link their section's "?" to the matching heading here.
Starting
nmon serve --data-dir /var/lib/nmon --secret-key-file /etc/nmon/secret.key \
--tls-cert /etc/nmon/tls.crt --tls-key /etc/nmon/tls.key
- HTTPS is the default. Pass a certificate pair (
--tls-cert,--tls-key;SIGHUPreloads them without dropping connections) or Let's Encrypt (--acme-host nmon.example.netrepeatable,--acme-email you@example.net; nmon answers the http-01 challenge on--acme-listen, default:80, certificates are cached in<data-dir>/acme). With neither (and no--http-listen) nmon generates a self-signed certificate, see The self-signed certificate.--insecure-httpis for development: plain HTTP, cookies withoutSecure. - A sign-in method is required over TLS (
auth.oidcand/orauth.break_glass_fileinconfig.yaml). On a first start with none configured nmon opens the setup wizard instead of refusing. With--insecure-httpand neither configured nmon runs in dev-open mode (everyone is admindev; a banner says so); never expose that port. - Behind a reverse proxy, see Behind a reverse proxy:
--http-listen ADDRadds an opt-in plain-HTTP listener, and the proxy goes inauth.trusted_proxies;X-Forwarded-*is ignored from anyone else.
Installing nmon / first start
On a fresh install nmon needs no config.yaml: start it, open https://<host>:8443/ and the browser lands on /setup, a five-step wizard (sign-in, basics, first devices, alerting, finish). Steps 2 to 4 can be skipped and done later in Settings.
nmon serve --data-dir /var/lib/nmon --secret-key-file /etc/nmon/secret.key
Install mode
Install mode is on only while no sign-in method is configured (no auth.oidc, no auth.break_glass_file) and setup has never been completed. While it is on:
- Anyone who can reach nmon can run setup, as an admin. There is no setup token and no localhost restriction: nmon is meant to sit in a DMZ or an internal network (
docs/DEVIATIONS.mdrow 51). Finish setup right after starting, or keep the port firewalled until you have. - nmon logs a
WARNat start and every 10 minutes:INSTALL MODE: setup is open to anyone who can reach this address. - Only the wizard works. The UI,
GET /api/v1/setup, the settings form, webhook presets/preview/test, adding devices (and CSV import), alert policies, the profile list, secrets andGET /api/v1/metaare served; every other API route answers503"setup not finished" until setup completes.
Install mode ends for good when setup completes (POST /api/v1/setup/complete, the wizard's last step). The completion is stored in the inventory database and nothing on the web can undo it. If you later delete the break-glass file or the auth: block, nmon does not reopen setup: it refuses to start with "no way to sign in", as before. An existing install that already has a sign-in method is marked completed the first time it starts on this version, so an upgrade can never open install mode. --insecure-http (development) never enters install mode.
To skip install mode altogether, configure sign-in before the first start, as the Ansible role does (nmon_break_glass_file / nmon_oidc_*): such an install is treated as already set up.
Setup wizard: sign-in
Required. Pick the break-glass administrator (default admin) and a password of at least 12 characters. nmon hashes it with bcrypt, writes <data-dir>/breakglass (mode 0600, user:hash) and sets auth.break_glass_file. Optionally add an OpenID Connect provider: issuer, client id, redirect URL (prefilled from the address you are using plus /auth/callback), groups claim, admin group and the client secret, which is stored encrypted as the secret oidc-client. nmon checks the fields now and contacts the provider at the first sign-in. Sign-in to the web UI has no login page: OIDC redirects the browser to the provider, and the break-glass admin signs in at /auth/breakglass (HTTP Basic).
Setup wizard: basics
The time zone (site default for p95 months), retention (raw samples retention.hot 30d, five-minute rates 120d, hourly rates 2y) and the default poll interval (poller.interval, 30s to 1h, default 5m). All of them are also in Settings, General and Retention.
Setup wizard: first devices
Choose an SNMP credential (v2c community, or v3 user with auth and privacy), then paste IP addresses (one per line, IPv6 or IPv4) or upload a CSV. Each address is added like the Add device dialog does it, so a device that does not answer SNMP is reported and not added. Host names are not resolved; use the address. The step shows each device's discovery state live.
Setup wizard: alerting
Add one receiver, a webhook (with the Teams, Slack, Discord and generic JSON presets, a live preview and Send test) or email, and optionally turn on the recommended alert policies: device_down, device_warning, interface_errors, cpu_high, memory_high and disk_full, each for all devices. Receivers get every notification, so the policies need no binding to the new receiver. Running the step again creates nothing twice. Interface-down alerts stay opt-in (see below).
Setup wizard: finish
The wizard summarises what it set up and calls POST /api/v1/setup/complete. nmon checks that a sign-in method now exists (otherwise 409), stores the completed flag, and switches authentication on without a restart: the live auth service is replaced by one built from the new configuration, and install mode closes. The browser is then sent to sign in.
The self-signed certificate
When none of --tls-cert/--tls-key, --acme-host or --http-listen is given, nmon serves HTTPS with a self-signed ECDSA P-256 certificate kept in <data-dir>/tls/ (cert.pem, key.pem mode 0600). It is created once and reused on every start; it is valid for about two years and replaced within 30 days of expiry. Its names are the host name, localhost, ::1, 127.0.0.1 and every non-loopback interface address (IPv6 included), as they were when it was created: delete the two files to make a new one after the host's addresses change. Browsers warn about it, and nmon logs a WARN at each start. Replace it with --tls-cert/--tls-key (reloaded on SIGHUP) or --acme-host. With --http-listen alone nmon serves plain HTTP only and creates no certificate.
Behind a reverse proxy
nmon serve --data-dir /var/lib/nmon --secret-key-file /etc/nmon/secret.key --http-listen [::1]:8080
--http-listen ADDR (default off; any interface is allowed, but loopback or an internal address is the point) adds a plain-HTTP listener that serves the same UI and API. HTTPS stays the default and is unchanged.
- Beside HTTPS: with
--tls-cert/--tls-keyor--acme-hosttoo, nmon serves HTTPS on--listenand plain HTTP on--http-listen, so the proxy may reach it either way. Alone: with no TLS configured nmon starts with only the HTTP listener (--listenis not bound) instead of refusing. SIGHUP still reloads the certificate for the HTTPS listener. - Sign-in stays on over this listener (OIDC, break-glass, API tokens), exactly as on HTTPS. It is not dev-open mode: with no
auth.oidc/auth.break_glass_filenmon refuses to start.--http-listenand--insecure-httpare mutually exclusive (--insecure-httpis development mode with no sign-in). - List the proxy in
auth.trusted_proxies(addresses or CIDRs, live-reloadable). Only a request from a trusted proxy has its headers honoured:X-Forwarded-Forgives the client address used by the audit log and the break-glass lockout (the rightmost hop that is not itself a trusted proxy), andX-Forwarded-Proto: httpsmarks the browser connection as HTTPS. From anyone else those headers are ignored and removed, so they cannot be spoofed. With the list empty nmon logs a WARN at startup: every client is seen as the proxy's address, so the lockout is per-proxy. - Cookies: the session and sign-in-flow cookies are always
Secureon the HTTPS listener. On the HTTP listener they areSecureonly when a trusted proxy sentX-Forwarded-Proto: https; a direct plain-HTTP client gets them withoutSecure(a browser would drop them otherwise). - OIDC:
auth.oidc.redirect_urlis configured explicitly and is never derived from the request, so set it to the public URL (https://nmon.example.net/auth/callback). CSRF uses a per-session token, not the Origin header, so a different public host or scheme needs nothing else. - nmon uses no websockets or long-lived streams; the proxy needs no upgrade headers. Raise the proxy's body-size limit if you import large CSV files.
nginx, proxying to the plain-HTTP listener:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name nmon.example.net;
ssl_certificate /etc/ssl/nmon.example.net.crt;
ssl_certificate_key /etc/ssl/nmon.example.net.key;
client_max_body_size 64m;
location / {
proxy_pass http://[::1]:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
}
with auth: {trusted_proxies: ["::1"]} in config.yaml. To talk TLS to nmon instead, run nmon with its normal --tls-cert/--tls-key (no --http-listen needed), use proxy_pass https://[::1]:8443; with proxy_ssl_verify off; (or proxy_ssl_trusted_certificate for a CA-signed pair), and the same proxy_set_header lines; the listener is HTTPS, so cookies are Secure. Ansible: set nmon_http_listen: "[::1]:8080" (and nmon_tls_mode: none to serve only that listener).
Sign-in
config.yaml:
auth:
oidc:
issuer: https://idp.example.net/realms/net
client_id: nmon
client_secret_name: oidc-client # the secret itself is never in this file
redirect_url: https://nmon.example.net/auth/callback
groups_claim: groups
admin_group: nmon-admins # members are admins; every other user is a viewer
break_glass_file: /etc/nmon/breakglass # bcrypt hash, "hash" or "user:hash", created by you
Store the client secret before the first start: printf '%s\n' "$SECRET" | nmon secret set oidc-client --data-dir D --secret-key-file K (it can also be set later with PUT /api/v1/secrets/oidc-client; OIDC stays off until the secret exists). The break-glass file holds one bcrypt hash ($2a$/$2b$), made with any bcrypt tool (the source tree's nmon-devtools bcrypt PASSWORD works); keep it readable by the nmon user only.
- There is no login page. Opening nmon without a session redirects to the IdP and back; API clients use a token. If the IdP is down, open
/auth/breakglass: the browser asks for the local admin user and password (HTTP Basic), and a successful sign-in lands on the page you asked for. - Break-glass: five wrong passwords lock the source address for 15 minutes; every use and every lock is audited and raises an alert.
nmon unlock [IP|all] --data-dir Dclears locks, also while nmon runs. - API tokens: an admin mints viewer, admin or push tokens (
POST /api/v1/tokens, or Settings, API tokens). UseAuthorization: Bearer nma_.... A push token works for ingest only. Revoking takes effect on the next request. GET /api/v1/audit(admin) lists who did what.
Backup and restore
A running nmon owns the stores, so copying the data folder is not a backup.
nmon backup /backups/nmon.tar --api https://nmon.example.net --token nma_... # key NOT included
nmon backup /backups/nmon.tar --include-key --api ... --token ... # key included
Every run prints whether the key file is in the archive. Lose the key file and a restore has an inventory with no SNMP credentials. The nightly schedule is backup: {at: "02:30", dir: /var/backups/nmon, keep: 7} (time in the configured timezone, default dir <data-dir>/backups); a failed run raises the backup_failed alert. A backup is refused when the disk is below its 5% reserve.
Restore with nmon stopped, into a clean directory:
nmon restore /backups/nmon.tar --data-dir /var/lib/nmon-restored --secret-key-file /etc/nmon/secret.key
It verifies inventory row counts and a sample of series before it says done. Then start nmon on that directory. Restore needs the embedded stores (the normal release build).
Keys
nmon key rotate --data-dir D --secret-key-file K (nmon stopped) adds a new key version, re-encrypts every SNMP credential and secret under it, and keeps the old key in the file so older backups still restore. Remove old keys only after a release.
Configuration
Settings (admin) is point-and-click: a section list (General, Retention, Sites, Notifications, Authentication, Backup, Disk guard, L2 and OUI, Proxmox, Checks, Alerting) with a form per section, its own Save and Revert, and the applied / restart-required result under it. Unset fields show the default as a placeholder; durations take 30d, 12h, 5m. A form that refers to a secret (OIDC client secret, Proxmox token) takes the secret's name and has a "Set secret value" button that stores the value write-only through PUT /api/v1/secrets/{name}; it is never shown again. The same sections are available as typed JSON: GET /api/v1/settings/form and PUT /api/v1/settings/form (each section present in the body replaces that section; the others are untouched). Alert rules and policies are not here, they are under Alerts; alerting.rules in config.yaml is preserved by the forms. The Advanced (YAML) tab and PUT /api/v1/settings still edit the whole document, and config.yaml + SIGHUP still works; all three go through the same validation. Saving rewrites config.yaml from the parsed configuration, so comments in the file are not kept. An invalid document is rejected and the running configuration stays. Alert receivers, the outbound allow-list, trusted proxies, the backup schedule, disk thresholds and the purge period apply live; everything else is listed as restart_required until the next restart. disk: {alert_used: 0.8, admit_used: 0.9, reserve_free: 0.05}: alert at 80% used, no new series at 90%, ingest throttled / rollup paused / backups refused below 5% free.
Metric stores
The supervisor restarts a crashed store; after three restarts in ten minutes it raises store_failed and stops. nmon store restart [hot|long] --api URL --token T (or SIGHUP) resumes it.
Interface-down alerts are opt-in
Since v1.1 the bell and the group switch only mark ports: interface_down alerts for them only where an alert policy for interface_down uses the alert_down matcher (template "Bell-marked ports: interface_down"). Mark ports in one at a time (the bell on the device page's interface table), all at once (the "Alert on down" control in the interfaces card header, or POST /api/v1/devices/{slug}/interfaces/alert-down), or through a group (the group's "Alert when member ports go down" switch; PUT /api/v1/interfaces/{id} with {"alert_down": true} for one port). A port in a group with the switch on alerts for as long as its membership is in force. Changes reach the evaluator at once; opting a port out closes its open alert silently as retired.
Alert policies: nothing alerts until you add one
Alerts fire only where an alert policy binds a rule to a scope. A fresh install has no policies and records no alerts at all: no metric rule, no device_down or device_warning, no system alarm (ingest caps, late p95, rollup, disk guard, break-glass, backup). The startup log says so once ("no alert policies: nothing will alert"). Upgrading from v1.0 seeds nothing: the first cycle closes every open alert that no policy binds, silently, as retired (no "resolved" notices go out).
Manage policies under Alerts > Alert policies (admins edit, viewers read) or with GET/POST /api/v1/alert-policies and GET/PUT/DELETE /api/v1/alert-policies/{id}. GET /api/v1/alert-rules lists the rule names. POST /api/v1/alert-policies/preview counts the devices and ports a draft scope covers. Changes apply on the next cycle with no restart. Removing or disabling a policy retires its open alerts quietly.
A policy has a rule (a metric rule, device_down, device_warning, or system), an action (alert or ignore), a priority, optional severity, threshold and for overrides, and a scope. Scope fields are ANDed and any entry of a list may match: all, groups (a port is in force as a member now; a device matches if any port does), devices (slugs globs, profiles, roles, sites, kinds) and interfaces (if_types, if_name and if_alias as RE2, min_speed_mbps, max_speed_mbps, alert_down). For one entity and rule the matching policy with the highest priority decides; on a tie ignore beats alert, then the lowest name. A policy threshold beats a rule's group override, the profile threshold and the default. Interface matchers are refused on device-level rules, and a system policy takes only all: true.
Fiber and uplinks alert on any error, WiFi never alerts on errors:
{"name": "Fiber + uplinks: errors", "rule": "interface_errors", "action": "alert", "threshold": 0,
"scope": {"interfaces": {"if_types": [6], "if_alias": "(?i)fib|uplink|core"}}}
{"name": "WiFi: no error alerts", "rule": "interface_errors", "action": "ignore", "priority": 10,
"scope": {"interfaces": {"if_types": [71]}}}
Alert rules: built-in and custom
The shipped rules (interface_down, interface_errors, cpu_high, memory_high, disk_full, ups_on_battery, ups_runtime_low, printer_supply_low, printer_supply_empty, temp_high) are read-only built-in rules, listed with their generated query under Alerts > Alert rules. cpu_high, ups_on_battery and printer_supply_empty have Copy to custom, which clones them into an editable builder rule; the others join, filter or union series and say why they cannot be copied. Rules in config.yaml (alerting.rules) still work but are deprecated, and the startup log says so; define rules in the UI instead. A rule name is unique across all kinds, and device_down, device_warning, system and the subsystem alarm names stay reserved.
Admins add their own rules with the builder (UI or POST /api/v1/alert-rules). There is no free-form query: nmon builds it from whitelisted parts, and the generated MetricsQL is shown read-only. Test runs the draft once and shows how many entities breach now. Edits apply on the next cycle; disabling a rule or deleting it retires its open alerts silently, and deleting a rule that a policy still names is refused (409, naming the policies).
- Metric rule, "CPU over 80 % on role=server for 15 min": metric
hrProcessorLoad, aggregatemax(one value per device), op>, threshold80, hold time15m; then a policy for this rule withdevices.roles: [server]. Transforms arevalue,rate(counters, per second) andpercent_of(metrics with anof, such ashrStorageUsed); the window defaults to 5 min (at least twice the poll interval). Push-ingested metrics are device-level. - Inventory rule, "Uplink without description": interface fields
if_alias emptyANDif_speed_mbps >= 10000. - Inventory rule, "Device not seen for 1 h": device field
last_seen_age older_than 1h.
A custom rule, like a built-in, alerts nowhere until an alert policy binds it: the rule defines what is measured and its default threshold and hold time, and the policy defines where it applies and may override severity, threshold and for. Several policies can bind one rule to different scopes.
Webhook templates
Each webhook receiver (Settings > Notifications) has a Go text/template that renders the request body. It must produce one valid JSON document (max 256 KB); it is sent as application/json. Empty means the default, {{json .}}, the whole notification. SMTP receivers use the same editor for Subject: ..., a blank line and the body.
Fields (a template sees nothing else): .IncidentID, .AlertID, .Rule, .State (firing, resolved, flapping, stale, suppressed_summary), .Severity (critical, warning, info), .Device.Slug, .Device.Name, .Interface.ID, .Interface.Name, .Interface.Alias (empty for device-level alerts), .Summary, .Description, .FiredAt, .ClearedAt (nil until resolved), .Labels, .Parents, .SuppressedChildren, .URL (link to the device in nmon).
Functions: json, upper, lower, join, timefmt ({{timefmt .FiredAt "2006-01-02 15:04:05 MST"}}, UTC), plus the standard printf, eq, or, if.
The {{json}} rule: wrap every value you put inside a JSON string or value in {{json ...}} ("text": {{json .Summary}}, never "text": "{{.Summary}}"). json adds the quotes and escapes quotes, newlines and control characters, so an alert text can never break the document. Secret-looking keys and key=value pairs are redacted from the rendered payload before it is stored or sent.
Presets: "Start from preset" in the receiver form fills the template (it asks before replacing a non-empty one). Built in, also at GET /api/v1/notify/presets:
| Preset | For |
|---|---|
| Generic JSON | the default; the whole notification |
| Microsoft Teams (Workflows) | Adaptive Card 1.4 for the Workflows trigger "When a Teams webhook request is received" |
| Slack | incoming webhook: text fallback and a coloured attachment with blocks |
| Discord | webhook with one embed, coloured by state and severity |
Teams: Microsoft is retiring the Office 365 connectors ("Incoming Webhook" in a channel's Connectors). Create a Workflow from the template "Post to a channel when a webhook request is received" and paste its URL. The preset is an Adaptive Card message and will not work with a legacy connector URL.
Preview and test: the editor shows the server-rendered payload for a sample notification in any state as you type, or the template or JSON error. "Send test" delivers one sample immediately (no retry queue; nothing stored except an notify.test audit row) and shows the HTTP status, a response excerpt (4 KB, redacted), the duration and any error. Blank header values and a blank SMTP password use the stored receiver of the same name, so you can test a saved receiver without retyping credentials. Admin only; at most two tests run at once. POST /api/v1/notify/preview and POST /api/v1/notify/test take {"receiver": {...form shape...}, "state": "firing"}.
Outbound allow-list: test sends and real deliveries go through the same gate. A webhook to a private, loopback or link-local address is refused unless it matches alerting.outbound_allow (Settings > Notifications), and the test result says so.
Retired devices
POST /api/v1/devices/{slug}/purge (admin) deletes a retired device's series from both stores once it has been retired for retired_purge_after (default 1y).
CSV import
Devices (name,address,site,role,credential,parents,...) and port descriptions (device,ifname,alias) from Settings or POST /api/v1/devices/import. Port descriptions are stored in the operator-owned description field, which discovery never overwrites. A dependency cycle is rejected with its path; nothing is written unless the whole import is valid.
Find host: MAC, ARP and FDB lookup
The L2 table job walks the bridge forwarding tables, VLAN names and ARP / IPv6 neighbour tables of every SNMP device every l2.interval and keeps them with first and last seen in the inventory, so the Find host page (and the header search, for a MAC, IP, CIDR or vendor:name) answers "which switch port is this host plugged in at, in which VLAN, and when did we last see it", with the uplinks that also saw it collapsed underneath. A MAC is seen on every switch it passes through, so the answer is the edge port: one that carries at most l2.edge_max_macs MACs, has no LLDP neighbour and is not a LAG.
l2:
interval: 15m # default 15m, minimum 5m
keep: 90d # history kept by last_seen, pruned daily
edge_max_macs: 8 # a port with more MACs is an uplink
concurrency: 4 # devices walked at once
device_timeout: 2m # a slower device is skipped for the cycle
oui:
fetch: true # IEEE vendor registry; false never touches the network
refresh: 7d
All of these apply from the next cycle after a reload (Settings save or SIGHUP) with no restart. The job never runs on the poll path and yields to the poller the way the rollup job does. A device that does not answer in time is skipped for that cycle and named on the Poller page (l2 status and an l2_devices_skipped note); it never delays the other devices. A Cisco IOS switch keeps one bridge table per VLAN: the cisco-ios profile sets l2.per_vlan_context: true, which lists the VLANs from CISCO-VTP-MIB and walks each with community <community>@<vlan> (v3: context vlan-<vlan>). Other devices use Q-BRIDGE-MIB, then BRIDGE-MIB (VLAN unknown).
API: GET /api/v1/l2/search?q=, GET /api/v1/devices/{slug}/{l2,fdb,arp,l2/ports}, GET /api/v1/interfaces/{id}/macs. On a device, the L2 tab (MAC table with a VLAN filter, ARP / ND, VLAN list) appears once the job has read it, and the Ports tab shows each port's MAC count linking to that port's MACs.
MAC vendors (optional IEEE registry)
If a registry is loaded, each MAC shows its vendor, a locally administered address (typical of randomised phone and VM MACs) shows a "private" badge, and vendor:ubiquiti lists the current MACs of a vendor. nmon works fully without it: the vendor is simply empty, and the Poller page notes oui_unavailable (not an alert).
The registry is the IEEE's public data: MA-L (https://standards-oui.ieee.org/oui/oui.csv), MA-M (.../oui28/mam.csv) and MA-S (.../oui36/oui36.csv), downloaded weekly through the outbound allow-list (standards-oui.ieee.org must be allowed when an allow-list is configured) with timeouts, a size cap and a row-count sanity check, so a truncated or wrong download keeps the previous file. The compact result is <data-dir>/oui/registry.gz (checksummed, written atomically, included in nmon backup). Where nmon may not fetch, set oui.fetch: false and load the CSVs by hand: nmon oui import <dir-or-file> --data-dir DIR (a running nmon notices within an hour). The CSVs are not part of this repository.
BGP
Router profiles collect BGP session state, uptime, accepted prefixes per address family and update rates every 5 minutes (see Collection coverage for which profiles and MIBs). The BGP page lists every session on every device, the ones that are down first, filterable by state, AS, family and text; a device's Routing tab shows its sessions with click-to-graph on prefixes and updates.
The built-in rule bgp_peer_down (critical, state not established for 2 minutes) fires only where a policy binds it, like every rule: use the template "Routers: BGP peer down" under Alerts > Alert policies. Its notification names the peer and, from discovery, the peer's AS and description. A prefix-drop alert is a custom rule (v1.2); an example will be added here once custom rules are in.
Poll interval
SNMP interface counters are polled every 300 s by default. An admin can set a different interval for one device (Device settings, "Poll interval", 30 to 3600 s; blank returns it to the default) or through PUT /api/v1/devices/{slug} with poll_interval_s. The poller picks it up at once, no restart. Graph rate windows, reachability fallbacks and the rollup follow the device's own interval (see docs/DEVIATIONS.md row 49). ICMP is still probed every 15 s.
Port totals. A port graph (GET /api/v1/graph) also returns totals: bytes in and out over the requested range, the average-rate series integrated over each point's own step (hourly and raw data stitched without double counting). covered_seconds against range_seconds says how much of the range had data; the UI marks a total with * when coverage is below 99.5%. Unreliable (32-bit fast link) ports get none. The port grid shows total beside avg and p95; the full graph has a Total column. Per port only.
Port graph parity with LibreNMS. The port summary and the full graph show, per direction, Now / Avg / Max / 95th / Total. Max is the highest plotted point (with max=true the finer "peak within a step" is a separate Peak column). The aggregate 95th (p95.aggregate_bps) is the 95th percentile of in+out per 5-minute slot, counted over slots where both directions have a sample; it is live from the hot store (also next to a materialised monthly figure) and is absent when only one direction was asked for or the range has no raw data. When the port speed is known, the full legend prints each rate's share of that speed, and a toggle plots the y axis as percent of speed (hidden for unknown or zero speed). "Compare previous period" (device port row and graph view) fetches the same-length range just before the current one with the ordinary from/to of GET /api/v1/graph, shifts it forward onto the current axis and draws it faded and dashed, with its own avg / max / 95th / total rows in the legend; it is off while a p95 month is selected.
The graph-type selector offers Bits, Errors and Discards. Errors and discards come from GET /api/v1/graph/counters?iface=&kind=errors|discards, the per-second rate of ifInErrors/ifOutErrors or ifInDiscards/ifOutDiscards with the same step and rate-window rules as bits. These counters have no long-store rollup, so the endpoint answers from the hot store only; a range that starts before hot retention is cut to its start (clamped: true, and the UI says so). Packets (unicast pps) and Broadcast/Multicast appear disabled: the generic profile does not poll ifHC*UcastPkts, ifHC*MulticastPkts or ifHC*BroadcastPkts. Adding them means polling three more columns on every port of every device and regenerating every fixture, so it is a separate decision (DEVIATIONS 56).
The default itself is poller.interval in config.yaml (Settings, General, "Poll interval"; 30s to 1h, default 5m). A change applies without a restart to the poller, reachability, the rollup and the API. The dev flag nmon serve --poll-interval, when given, wins over the setting.
Testing and debugging a device's polling
Three admin tools answer "why is this device not polling right", from the device page (Actions menu) or the API. None of them shows or logs a credential: the trace names the SNMP version, never the community or the v3 secrets.
- Poll now (
POST /api/v1/devices/{slug}/poll-now, 202) runs the device's primary group right away, outside its schedule. The result lands in the normal places (graphs, reachability). It answers 409 when the device is retired, disabled, has no SNMP credential, or is not loaded by the poller yet. - Test poll (
POST /api/v1/devices/{slug}/poll-test, optional?interval_seconds=300for one interval group) runs one synchronous cycle of the device's real poll plan: the stored credential, the resolved profile, the address and port, and the same fetch code the poller uses (the varbind count starts at the default and halves on tooBig or a lost response, exactly as in polling, but is forgotten afterwards). It writes no metrics and changes no poller state, so it cannot cause a reboot event, a backoff or a down alert. The answer lists each request (the session, number of varbinds, first OID, start and duration in ms, outcomeok,timeout,too_big,snmp_error,dial_errororerror, and whether it was a retry in smaller pieces) and every requested OID with the series it feeds, its label (interface ULID or component), the SNMP type, the raw value and what the poller would store, or why not:no_such_object,no_such_instance,end_of_mib,skipped(text, type mismatch, not available),not_fetched(its request failed) oroid_mismatch. Theduration_msof a request includes the SNMP library's own retries; they are not listed separately. Limits: one test per device at a time (409), four at once overall (429), 60 s at most, 5,000 OID rows at most (truncated: true; failures and misses are kept first). Every test is audited asdevice.poll_test. - SNMP debug (
PUT /api/v1/devices/{slug}/snmp-debugwith{"minutes": N}; 0 turns it off, 60 at most;GETreads it) records every scheduled cycle of one device while it is on: the same request and OID detail as a test poll, for the last 20 cycles (500 OID rows each, failures first), newest first. It also logs one INFO line per cycle (snmp debug cycle, with the counts, duration and error). It switches off by itself when the minutes are up. Recordings are kept in memory only and are cleared on restart; starting a new recording drops the previous one, and switching off keeps what was captured so you can still read it. Turning it on or off is audited asdevice.snmp_debug. Use Poll now while it is on to get a cycle at once.
From a shell: nmon device poll <slug> --api URL --token TOKEN queues a poll now; add --test for the traced test poll (--interval SECONDS for one group, --json for the full trace).
Reading a trace: a request that times out with every OID not_fetched is a reachability, community or ACL problem (check the target address, the SNMP version and the device's allowed managers). A too_big request followed by retry requests means the device cannot answer 40 varbinds at once; the poller adapts, so this is only a note. Many no_such_instance rows for interface OIDs mean the stored interfaces no longer match the device: run Rediscover.