Frontend Labs
⋯

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 := &registry{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"} 5 means 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's histogram_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 +Inf bucket. 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:

  1. Point a local Prometheus at it (scrape_configs: [{job_name: probe, static_configs: [{targets: ['localhost:9115']}]}]) and graph histogram_quantile(0.99, rate(probe_tcp_connect_seconds_bucket[5m])).
  2. 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.
  3. 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

  1. What question do counters answer that flow records can't, and vice versa?
  2. Why must you use the 64-bit interface counters?
  3. Why does a 60-second polling interval hide microbursts?
  4. 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_exporter and snmp_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.

Generated from real-life-guide-v1.md · 8 Oct 2026 16:36