SnmpKit.SNMP.Rate (snmpkit v2.0.1)
Deltas and per-second rates from successive SNMP samples, with Counter32 and Counter64 wraparound handled.
Works on the enriched varbind maps every SnmpKit.SNMP call returns, on
{type, value} tuples, or on bare integers with an explicit type.
# two polls of the same object
{:ok, %{value: 4_294_967_000, type: :counter32} = t0} = SnmpKit.SNMP.get(host, "ifInOctets.1")
# ... 10 seconds later, the counter wrapped ...
{:ok, %{value: 1_000} = t1} = SnmpKit.SNMP.get(host, "ifInOctets.1")
SnmpKit.SNMP.Rate.delta(t0, t1) #=> {:ok, 1296}
SnmpKit.SNMP.Rate.rate(t0, t1, 10_000) #=> {:ok, 129.6} (per second)Whole walks are paired by OID with rates/3, which reads the interval
from the sysUpTime.0 varbinds when both samples carry them and reports a
device restart instead of inventing rates from reset counters.
Restarts versus wraps
A Counter32 that wrapped and a counter that reset on reboot look the same
from one object alone. delta/2 assumes a wrap, which is right for a
busy 32-bit counter. rates/3 compares sysUpTime.0 when present and
returns {:error, :device_restarted} if it went backwards; pass
sysUpTime.0 along with the objects you poll to get that protection.
sysUpTime is itself a 32-bit TimeTicks value and wraps after about 497
days. A wrap looks exactly like a restart, so rates/3 reports
{:error, :device_restarted} for it too. When a poll may span that
boundary pass interval_ms: from your own clock; it always takes priority
over the uptime varbinds.
Multiple wraps
A counter that wrapped more than once between two samples cannot be told
from one that wrapped once, so its delta is under-reported. Poll fast
enough that no counter can wrap twice (a 32-bit octet counter wraps in
under 35 seconds at 1 Gbps), or give max_rate:, the highest per-second
rate the object can plausibly show, and a rate that only a multiple wrap
explains becomes {:error, :implausible_rate}.
Options
:type- the SNMP type of bare numeric samples.:max_rate- per-second bound; a rate whose magnitude exceeds it is reported as:implausible_rate.:interval_ms(rates/3) - elapsed time between the two samples, takes priority oversysUpTime.0.:unrateable(rates/3) -:skip(default) leaves out entries that could not be rated;:errorreturns{:error, {:unrateable, oid, reason}}for the first one.
Sample values
Integer values are what the decoder produces. Hand-built samples may
carry floats for :gauge32, :integer and :unsigned32; a counter is an
integer by definition, so a float counter is {:error, {:not_a_sample, _}}.
Summary
Functions
The increase from previous to current, wrap-corrected for
:counter32 and :counter64. Other numeric types return the plain
difference (gauges can go down).
Milliseconds elapsed between two sysUpTime readings (TimeTicks, i.e.
centiseconds), or {:error, :device_restarted} when the second is lower.
Per-second rate between two samples taken interval_ms apart. With
max_rate: a rate whose magnitude exceeds the bound is
{:error, :implausible_rate}.
Pairs two lists of enriched varbinds (walk or get results) by OID and returns a delta and rate for every counter or gauge present in both.
Types
Functions
The increase from previous to current, wrap-corrected for
:counter32 and :counter64. Other numeric types return the plain
difference (gauges can go down).
iex> SnmpKit.SNMP.Rate.delta({:counter32, 4_294_967_000}, {:counter32, 1_000})
{:ok, 1296}
iex> SnmpKit.SNMP.Rate.delta(%{type: :gauge32, value: 50}, %{type: :gauge32, value: 20})
{:ok, -30}
iex> SnmpKit.SNMP.Rate.delta({:counter32, 1}, {:counter64, 2})
{:error, :type_mismatch}
@spec interval_from_uptime(non_neg_integer(), non_neg_integer()) :: {:ok, pos_integer()} | {:error, :device_restarted | :no_interval}
Milliseconds elapsed between two sysUpTime readings (TimeTicks, i.e.
centiseconds), or {:error, :device_restarted} when the second is lower.
TimeTicks wrap after 2^32 centiseconds (about 497 days). A wrap is
indistinguishable from a restart here and is reported the same way;
callers whose polls may span that boundary should pass interval_ms: to
rates/3 instead.
iex> SnmpKit.SNMP.Rate.interval_from_uptime(1_000, 2_500)
{:ok, 15_000}
iex> SnmpKit.SNMP.Rate.interval_from_uptime(4_294_967_195, 5)
{:error, :device_restarted}
Per-second rate between two samples taken interval_ms apart. With
max_rate: a rate whose magnitude exceeds the bound is
{:error, :implausible_rate}.
iex> SnmpKit.SNMP.Rate.rate({:counter32, 100}, {:counter32, 1_100}, 10_000)
{:ok, 100.0}
iex> SnmpKit.SNMP.Rate.rate({:counter32, 100}, {:counter32, 50}, 1_000, max_rate: 1.0e9)
{:error, :implausible_rate}
Pairs two lists of enriched varbinds (walk or get results) by OID and returns a delta and rate for every counter or gauge present in both.
The interval comes from interval_ms: or, failing that, from the
sysUpTime.0 varbinds in both samples. Each result is
%{oid: "1.3.6.1.2.1.2.2.1.10.1", name: "ifInOctets.1", type: :counter32,
previous: 100, current: 1100, delta: 1000, rate: 100.0}Returns {:error, :device_restarted} when sysUpTime.0 went backwards
(or wrapped, see the module docs), {:error, :no_interval} when no
interval can be determined.
Only values whose type can carry a rate are candidates: strings, OIDs,
addresses and TimeTicks are always left out. A candidate that could not be
rated (no previous sample, a type that changed between samples, a
non-numeric value, or a rate above max_rate:) is left out by default and
reported as {:error, {:unrateable, oid, reason}} with unrateable: :error.