package stats import ( "bytes" "os" "path/filepath" "strconv" "strings" ) // What the collector reads from the kernel without privileges: the // interfaces' counters (/sys/class/net), the connection tracking table's // size, and housecarld's and the host's memory and load (/proc). // clockTicks is USER_HZ, the unit of the CPU times in /proc/PID/stat. It // is 100 on every Linux architecture Housecarl runs on. const clockTicks = 100 // ifCounters are an interface's totals since it came up. type ifCounters struct { RxBytes, TxBytes, RxPackets, TxPackets int64 } // readInterfaces reads every interface's counters but the loopback's. func readInterfaces(sysNet string) map[string]ifCounters { out := map[string]ifCounters{} entries, err := os.ReadDir(sysNet) if err != nil { return out } for _, e := range entries { name := e.Name() if name == "lo" { continue } dir := filepath.Join(sysNet, name, "statistics") read := func(f string) (int64, bool) { b, err := os.ReadFile(filepath.Join(dir, f)) if err != nil { return 0, false } n, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64) return n, err == nil } var c ifCounters var ok bool if c.RxBytes, ok = read("rx_bytes"); !ok { continue } c.TxBytes, _ = read("tx_bytes") c.RxPackets, _ = read("rx_packets") c.TxPackets, _ = read("tx_packets") out[name] = c } return out } // readConntrack returns how many connections the kernel tracks, and how // many it can (ok false without connection tracking). func readConntrack(proc string) (count, max int64, ok bool) { read := func(f string) (int64, bool) { b, err := os.ReadFile(filepath.Join(proc, "sys/net/netfilter", f)) if err != nil { return 0, false } n, err := strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64) return n, err == nil } count, ok1 := read("nf_conntrack_count") max, ok2 := read("nf_conntrack_max") return count, max, ok1 && ok2 } // readSelf returns housecarld's resident memory (bytes) and CPU time // (clock ticks). func readSelf(proc string) (rss, cpu int64, ok bool) { dir := filepath.Join(proc, "self") stat, err := os.ReadFile(filepath.Join(dir, "stat")) if err != nil { return 0, 0, false } // The command name is in parentheses and may contain spaces: the // fields that follow start after the last ')'. i := bytes.LastIndexByte(stat, ')') if i < 0 { return 0, 0, false } f := strings.Fields(string(stat[i+1:])) // f[0] is field 3 (state); utime and stime are fields 14 and 15. if len(f) < 13 { return 0, 0, false } utime, _ := strconv.ParseInt(f[11], 10, 64) stime, _ := strconv.ParseInt(f[12], 10, 64) status, err := os.ReadFile(filepath.Join(dir, "status")) if err != nil { return 0, 0, false } for _, line := range strings.Split(string(status), "\n") { if k, v, ok := strings.Cut(line, ":"); ok && k == "VmRSS" { rss = kib(v) } } return rss, utime + stime, true } // readHost returns the memory in use (bytes) and the 1-minute load // average. func readHost(proc string) (memUsed int64, load float64, ok bool) { b, err := os.ReadFile(filepath.Join(proc, "meminfo")) if err != nil { return 0, 0, false } var total, avail int64 for _, line := range strings.Split(string(b), "\n") { k, v, _ := strings.Cut(line, ":") switch k { case "MemTotal": total = kib(v) case "MemAvailable": avail = kib(v) } } if b, err := os.ReadFile(filepath.Join(proc, "loadavg")); err == nil { if f := strings.Fields(string(b)); len(f) > 0 { load, _ = strconv.ParseFloat(f[0], 64) } } return total - avail, load, total > 0 } // kib parses a "1234 kB" value from /proc into bytes. func kib(v string) int64 { n, _ := strconv.ParseInt(strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(v), "kB")), 10, 64) return n * 1024 }