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 over sysUpTime.0.
  • :unrateable (rates/3) - :skip (default) leaves out entries that could not be rated; :error returns {: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

sample()

@type sample() :: map() | {atom(), number()} | number()

Functions

delta(previous, current, opts \\ [])

@spec delta(sample(), sample(), keyword()) :: {:ok, number()} | {:error, term()}

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}

interval_from_uptime(previous_ticks, current_ticks)

@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}

rate(previous, current, interval_ms, opts \\ [])

@spec rate(sample(), sample(), pos_integer(), keyword()) ::
  {:ok, float()} | {:error, term()}

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}

rates(previous, current, opts \\ [])

@spec rates([map()], [map()], keyword()) :: {:ok, [map()]} | {:error, term()}

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.