///|
/// Sample the interface counters twice and print the rates.
///
/// moon run examples/interface_rates
/// moon run examples/interface_rates -- 192.0.2.10 public 161 5
///
/// A rate is two reads and a division, and four things make it wrong if
/// they are not handled:
///
/// * the counters wrap. `ifInOctets` is a Counter32, and on a 10G link
/// that is four seconds of traffic — so the 64-bit `ifHCInOctets`
/// columns are read first, and the 32-bit ones are only a fallback for
/// devices that predate `ifXTable` (RFC 2863);
/// * the interface list can change between the reads, so an interface
/// that disappears is dropped rather than paired with a lookalike;
/// * the interval has to be the time that actually passed, not the time
/// the program asked for;
/// * an agent caches its interface table. net-snmp serves `ifTable` from
/// a cache of a few seconds, so two reads a second apart return the
/// same counters and honestly report zero. The default interval is
/// five seconds for that reason, not for round numbers.
///|
/// One interface's counters, as one sample found them.
struct Counters {
index : UInt
descr : String
in_octets : UInt64
out_octets : UInt64
}
///|
/// The same interface, as a rate.
struct Rate {
index : UInt
descr : String
in_bps : Int64
out_bps : Int64
}
///|
fn main {
let cli = @common.Cli::read()
let target = cli.target()
// Seconds, because that is what a person types; the socket counts
// milliseconds.
let interval = cli.number(5, 5)
println(
"sampling \{target.host}:\{target.port} over \{interval}s (community \"\{@utf8.decode_lossy(target.community)}\")\n",
)
let socket = @transport.Transport::open(0, 3000) catch {
@transport.TransportError::NotOpen => {
println("could not open a socket")
@common.fail(1)
return
}
e => {
println("could not open a socket: \{Repr(e)}")
@common.fail(1)
return
}
}
let session = @session.Session::new(
socket,
target.host,
target.port,
@pdu.V2c,
target.community,
3000,
1,
1000,
)
let outcome = Some(measure(session, interval)) catch {
@session.SnmpError::NoResponse => {
println(
"no reply. Check the address, the community, and that the agent is running.",
)
None
}
e => {
println("sampling failed: \{Repr(e)}")
None
}
}
socket.close()
match outcome {
Some((rates, wide)) => print_rates(rates, wide)
None => @common.fail(1)
}
}
///|
/// Read the counters, wait, read them again, and turn the difference into
/// a rate.
///
/// Which pair of columns to read is decided once, from the first sample:
/// an agent that implements `ifXTable` answers the 64-bit walk, and one
/// that does not answers it with nothing.
///
/// The clock is read around the wait so a slow device stretches the
/// interval rather than being reported as faster than it is. The second
/// walk happens after the clock is stopped, which makes the rate a little
/// conservative — the honest direction to be wrong in.
fn measure(
session : @session.Session[@transport.Transport],
interval : Int,
) -> (Array[Rate], Bool) raise {
let mut wide = true
let mut first = sample(session, wide)
if first.length() == 0 {
// No ifXTable on this device: the 32-bit columns are all there is.
wide = false
first = sample(session, wide)
}
let started = @common.now_ms()
@common.sleep_ms(interval * 1000)
let second = sample(session, wide)
let elapsed_ms = @common.now_ms() - started
(rates_between(first, second, elapsed_ms, wide), wide)
}
///|
/// One pass over the three columns this program needs.
///
/// Column walks rather than one walk of `ifEntry`: the whole table is
/// dozens of columns of data this program would throw away.
fn sample(
session : @session.Session[@transport.Transport],
wide : Bool,
) -> Array[Counters] raise {
let names = session.walk_bulk(@mib.if_descr(), 25, 500)
let in_column = if wide {
@mib.if_hc_in_octets()
} else {
@mib.if_in_octets()
}
let out_column = if wide {
@mib.if_hc_out_octets()
} else {
@mib.if_out_octets()
}
let ins = session.walk_bulk(in_column, 25, 500)
let outs = session.walk_bulk(out_column, 25, 500)
let out : Array[Counters] = []
for entry in ins {
match last_arc(entry.name) {
Some(index) =>
match counter_value(entry.value, wide) {
Some(in_octets) =>
match counter_at(outs, index, wide) {
Some(out_octets) =>
out.push({
index,
descr: descr_at(names, index),
in_octets,
out_octets,
})
// An interface with an input counter and no output counter
// is not something to guess about.
None => ()
}
// Anything but the counter type this column should carry means
// it is not an octet counter, whatever the OID says.
None => ()
}
None => ()
}
}
out
}
///|
/// The last arc of an OID, which for a table cell is its row index.
fn last_arc(oid : @oid.Oid) -> UInt? {
let parts = oid.parts()
if parts.length() == 0 {
None
} else {
Some(parts[parts.length() - 1])
}
}
///|
fn entry_at(
entries : Array[@session.WalkEntry],
index : UInt,
) -> @session.WalkEntry? {
for e in entries {
match last_arc(e.name) {
Some(i) => if i == index { return Some(e) }
None => ()
}
}
None
}
///|
fn descr_at(names : Array[@session.WalkEntry], index : UInt) -> String {
match entry_at(names, index) {
Some(e) =>
match e.value {
@ber.BerValue::OctetString(b) => @utf8.decode_lossy(b)
_ => ""
}
None => ""
}
}
///|
/// A counter as a 64-bit value, whichever width the column carries.
fn counter_value(value : @ber.BerValue, wide : Bool) -> UInt64? {
if wide {
match value {
@ber.BerValue::Counter64(n) => Some(n)
_ => None
}
} else {
match value {
@ber.BerValue::Counter32(n) => Some(n.to_uint64())
_ => None
}
}
}
///|
fn counter_at(
entries : Array[@session.WalkEntry],
index : UInt,
wide : Bool,
) -> UInt64? {
match entry_at(entries, index) {
Some(e) => counter_value(e.value, wide)
None => None
}
}
///|
fn find_counters(entries : Array[Counters], index : UInt) -> Counters? {
for e in entries {
if e.index == index {
return Some(e)
}
}
None
}
///|
/// The difference at the width the counter actually has: a 32-bit value
/// promoted to 64 bits still wraps at 32, so the two cannot share one
/// subtraction.
fn counter_delta(earlier : UInt64, later : UInt64, wide : Bool) -> UInt64 {
if wide {
@mib.counter64_delta(earlier, later)
} else {
@mib.counter_delta(earlier.to_uint(), later.to_uint()).to_uint64()
}
}
///|
/// Pair up two samples and divide.
fn rates_between(
first : Array[Counters],
second : Array[Counters],
elapsed_ms : Int64,
wide : Bool,
) -> Array[Rate] {
let ms = if elapsed_ms <= 0L { 1L } else { elapsed_ms }
let out : Array[Rate] = []
for a in first {
match find_counters(second, a.index) {
Some(b) => {
// Octets to bits, then over the time that actually passed.
// Integer arithmetic throughout: a float here would only add
// rounding of its own.
let bits_in = counter_delta(a.in_octets, b.in_octets, wide).reinterpret_as_int64() *
8L
let bits_out = counter_delta(a.out_octets, b.out_octets, wide).reinterpret_as_int64() *
8L
out.push({
index: a.index,
descr: a.descr,
in_bps: bits_in * 1000L / ms,
out_bps: bits_out * 1000L / ms,
})
}
// The interface went away between the samples; a rate for it would
// be a rate for a different interface.
None => ()
}
}
out
}
///|
fn print_rates(rates : Array[Rate], wide : Bool) -> Unit {
if rates.length() == 0 {
println(
"no interface counters came back: an agent that does not implement ifTable, or a device with no interfaces?",
)
return
}
// Saying which counters were read is the difference between a number
// and a number a reader can trust.
let source = if wide {
"64-bit (ifHCInOctets / ifHCOutOctets)"
} else {
"32-bit (ifInOctets / ifOutOctets, wraps every 4 GiB)"
}
println("counters \{source}\n")
println(
"\{@common.pad("index", 5)} \{@common.pad("interface", 20)} \{@common.pad_left("in", 12)} \{@common.pad_left("out", 12)}",
)
for r in rates {
let index = "\{r.index}"
println(
"\{@common.pad(index, 5)} \{@common.pad(r.descr, 20)} \{@common.pad_left(@mib.format_bit_rate(r.in_bps), 12)} \{@common.pad_left(@mib.format_bit_rate(r.out_bps), 12)}",
)
}
println("")
println("\{rates.length()} interfaces")
}