Chapter 46 — Where the numbers come from: counters, flows, and probes
In one sentence
Devices expose counters (SNMP), summarise conversations (flow records), and you generate your own test traffic (probes) — three different tools for three different questions.
SNMP: reading a device's counters
Simple Network Management Protocol is how you ask a switch, router, printer, or UPS for its numbers. It's from 1988 and it's everywhere.
YOUR MONITORING SERVER THE DEVICE
| "what is ifInOctets on interface 3?" |
|------------------- UDP 161 ------------->|
|<------------------- 4823910284 ----------|
| |
|<---- TRAP (UDP 162): "interface 3 down!" | (unsolicited alert)
Every value has a numeric address called an OID, and a MIB is the dictionary that gives them names. The handful you'll actually use:
| Name | Tells you |
|---|---|
sysUpTime |
Time since boot — if it resets, the device rebooted |
ifDescr / ifAlias |
Interface name / the human description someone typed |
ifHCInOctets / ifHCOutOctets |
Bytes in/out — the 64-bit versions |
ifInErrors / ifOutErrors |
Errors |
ifOperStatus |
Up or down |
The classic trap: the original 32-bit byte counters wrap around at ~4.3 GB. On a 10 Gbit/s link that's under 4 seconds. Always poll the 64-bit
ifHC*counters, or your graphs will be nonsense.
Security note: SNMP v1 and v2c send the "community string" (effectively a password) in plain text. Use v3 (which has real authentication and encryption), and restrict SNMP to a management network.
A limitation to understand: if you poll every 60 seconds, you're seeing 60-second averages. A 10-second burst that saturates the link averages into invisibility. That's why "the graph says 45% but users say it's slow" happens.
Flow records: who is using the bandwidth?
Counters tell you how much. Flow records tell you who and what.
A "flow" is one conversation. The router summarises it:
10:00:01 -> 10:00:04 192.168.1.50:51000 -> 93.184.216.34:443 TCP
48 packets, 52,104 bytes
Collect millions of those and you can answer: who are my top talkers? what protocol is eating the uplink? has anything ever connected to this address?
| Technology | How it works | Trade-off |
|---|---|---|
| NetFlow / IPFIX | Router tracks every flow and exports a record when it ends | Accurate byte counts; costs router memory |
| sFlow | Samples 1 packet in N, plus interface counters | Cheap and real-time; statistical, so small flows are invisible |
| VPC Flow Logs | The cloud equivalent — same idea, written to storage | Also shows ACCEPT/REJECT, so you can see what a security group blocked |
A timing detail that confuses people: flow records appear after the flow ends (or after a periodic timer, often 60 seconds). So flow data is minutes behind — never build a real-time alert on it.
Active probes: testing before users do
Passive monitoring only sees traffic that exists. If your service is down at 3am with no users, nothing is broken from the monitoring's point of view. Active probes fix that by generating their own traffic.
Design them in layers, so the alert tells you where the failure is:
ping the host -> is it reachable at all? (Layer 3)
TCP connect to :443 -> is the port open? (Layer 4)
TLS handshake -> is the certificate valid? (Layer 6)
HTTP GET /healthz -> does the app respond correctly? (Layer 7)
full user journey -> does log in -> search -> buy work?
DNS lookup -> does the name still resolve?
Probe from multiple places. A check running in the same data centre as your servers will not notice that a routing problem has made you unreachable from Europe.
Practice (25 min)
A. SNMP against your own router (if it supports it — most home routers do, sometimes needing enabling in the admin page):
sudo apt install snmp # or: brew install net-snmp
snmpwalk -v2c -c public 192.168.1.1 1.3.6.1.2.1.1 # system info
snmpwalk -v2c -c public 192.168.1.1 IF-MIB::ifDescr # interface names
snmpget -v2c -c public 192.168.1.1 IF-MIB::ifHCInOctets.2
To turn a counter into a rate: take two readings, subtract, divide by the seconds between them, multiply by 8 for bits/second.
B. Generate and read your own flow records:
sudo apt install softflowd nfdump
sudo softflowd -i eth0 -n 127.0.0.1:9995 -v 9
nfcapd -w -D -l /tmp/flows -p 9995 &
# ... browse the web for two minutes ...
nfdump -R /tmp/flows -s srcip/bytes -n 10 # your top talkers
nfdump -R /tmp/flows -s dstport/bytes -n 10 # top ports
You just built the thing that answers "what's using my bandwidth?"
C. Write a layered probe script:
#!/bin/bash
H=${1:-example.com}
echo -n "DNS: "; dig +short +time=2 $H | head -1 || echo FAIL
echo -n "PING: "; ping -c2 -W2 $H >/dev/null 2>&1 && echo ok || echo FAIL
echo -n "TCP: "; nc -zv -w3 $H 443 >/dev/null 2>&1 && echo ok || echo FAIL
echo -n "TLS: "; echo | openssl s_client -connect $H:443 -servername $H 2>/dev/null \
| openssl x509 -noout -enddate || echo FAIL
echo -n "HTTP: "; curl -s -o /dev/null -w '%{http_code} in %{time_total}s\n' https://$H
Run it against a few sites. This is a monitoring system in twelve lines — the real ones just do it continuously and store the results.
Build it in Go (45 min) — a blackbox prober with a latency histogram
"Active probes" from this chapter are what Prometheus's blackbox_exporter
does. Writing a small one shows you why monitoring systems store
histograms, not averages (Chapter 45): a histogram is a set of cumulative
counters, cheap to store and aggregate, from which percentiles can be
estimated later.
// prober: a tiny blackbox exporter (Chapters 45-47). It probes TCP targets on
// a schedule and exposes Prometheus-format metrics -- a latency histogram,
// success/failure counters -- on /metrics.
//
// go run ./prober -listen :9115 example.com:443 1.1.1.1:53 10.255.255.1:80
// curl -s localhost:9115/metrics
package main
import (
"flag"
"fmt"
"log"
"net"
"net/http"
"sort"
"sync"
"time"
)
// Bucket upper bounds in seconds. Choose them around your SLO, not at random.
var buckets = []float64{0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5}
type series struct {
counts []uint64 // cumulative-later; raw per-bucket here
sum float64
total uint64
failures uint64
}
type registry struct {
mu sync.Mutex
m map[string]*series
}
func (r *registry) observe(target string, d time.Duration, ok bool) {
r.mu.Lock()
defer r.mu.Unlock()
s := r.m[target]
if s == nil {
s = &series{counts: make([]uint64, len(buckets)+1)}
r.m[target] = s
}
if !ok {
s.failures++
return
}
sec := d.Seconds()
i := sort.SearchFloat64s(buckets, sec) // first bucket >= sec; len(buckets) = +Inf
s.counts[i]++
s.sum += sec
s.total++
}
// ServeHTTP writes the Prometheus text exposition format by hand -- it is
// just lines of "name{labels} value".
func (r *registry) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
r.mu.Lock()
defer r.mu.Unlock()
w.Header().Set("Content-Type", "text/plain; version=0.0.4")
fmt.Fprintln(w, "# HELP probe_tcp_connect_seconds Time to complete the TCP handshake.")
fmt.Fprintln(w, "# TYPE probe_tcp_connect_seconds histogram")
targets := make([]string, 0, len(r.m))
for t := range r.m {
targets = append(targets, t)
}
sort.Strings(targets)
for _, t := range targets {
s := r.m[t]
var cum uint64
for i, le := range buckets {
cum += s.counts[i]
fmt.Fprintf(w, "probe_tcp_connect_seconds_bucket{target=%q,le=\"%g\"} %d\n", t, le, cum)
}
cum += s.counts[len(buckets)]
fmt.Fprintf(w, "probe_tcp_connect_seconds_bucket{target=%q,le=\"+Inf\"} %d\n", t, cum)
fmt.Fprintf(w, "probe_tcp_connect_seconds_sum{target=%q} %g\n", t, s.sum)
fmt.Fprintf(w, "probe_tcp_connect_seconds_count{target=%q} %d\n", t, s.total)
}
fmt.Fprintln(w, "# TYPE probe_failures_total counter")
for _, t := range targets {
fmt.Fprintf(w, "probe_failures_total{target=%q} %d\n", t, r.m[t].failures)
}
}
func main() {
listen := flag.String("listen", ":9115", "address for /metrics")
every := flag.Duration("every", 5*time.Second, "probe interval")
timeout := flag.Duration("timeout", 2*time.Second, "per-probe timeout")
flag.Parse()
reg := ®istry{m: map[string]*series{}}
for _, target := range flag.Args() {
go func() {
for {
start := time.Now()
c, err := net.DialTimeout("tcp", target, *timeout)
if err == nil {
c.Close()
}
reg.observe(target, time.Since(start), err == nil)
time.Sleep(*every)
}
}()
}
http.Handle("/metrics", reg)
log.Printf("probing %v every %v; metrics on http://localhost%s/metrics", flag.Args(), *every, *listen)
log.Fatal(http.ListenAndServe(*listen, nil))
}
$ go run ./prober -every 1s example.com:443 1.1.1.1:53 10.255.255.1:80 &
$ curl -s localhost:9115/metrics | grep -E 'example.com|failures'
probe_tcp_connect_seconds_bucket{target="example.com:443",le="0.05"} 5
probe_tcp_connect_seconds_bucket{target="example.com:443",le="+Inf"} 6
probe_tcp_connect_seconds_count{target="example.com:443"} 6
probe_failures_total{target="1.1.1.1:53"} 0
probe_failures_total{target="10.255.255.1:80"} 2
probe_failures_total{target="example.com:443"} 0
What to notice:
- Buckets are cumulative.
le="0.05"} 5means 5 of the 6 probes finished in 50 ms or less. To find the p99 you look for the first bucket that holds 99% of_count, which is exactly what PromQL'shistogram_quantile()does. - Choose bucket edges around your objective. If the SLO is "connect under 100 ms", you need edges just below and above 0.1. Otherwise the histogram can't tell you whether you're meeting it.
- Failures are a separate counter. Mixing timeouts into the latency
histogram would hide them in the
+Infbucket. Alert on the failure rate (Chapter 47) and track latency separately. - The exposition format is plain text. No client library is needed to
expose metrics, though in production you'd use
github.com/prometheus/client_golang(see the Go guide's observability chapter).
Exercises:
- Point a local Prometheus at it (
scrape_configs: [{job_name: probe, static_configs: [{targets: ['localhost:9115']}]}]) and graphhistogram_quantile(0.99, rate(probe_tcp_connect_seconds_bucket[5m])). - Add a TLS probe type that records handshake time separately from TCP connect time, and an HTTP probe that checks the status code. Now you can tell which layer got slow.
- Write the Chapter 47 alert for it: "more than 10% of probes to a target failed for 5 minutes", plus a runbook link.
Check yourself
- What question do counters answer that flow records can't, and vice versa?
- Why must you use the 64-bit interface counters?
- Why does a 60-second polling interval hide microbursts?
- Why do you need active probes as well as passive monitoring?
Further reading
- Docs: Prometheus
blackbox_exporter— does ICMP, TCP, HTTP, DNS, and TLS certificate-expiry probing in one binary. Read its config examples. - Docs: Prometheus
node_exporterandsnmp_exporter. - Tool:
ntopng— a free web UI that turns flow data into readable graphs. Excellent for learning what flow records can tell you. - Article: "SNMP explained" — search for a recent practical guide; the concepts (OID, MIB, community, trap) are stable.