package stats import ( "context" "os" "path/filepath" "strconv" "testing" "time" ) func TestProc(t *testing.T) { root := t.TempDir() old := procRoot procRoot = root defer func() { procRoot = old }() proc := func(pid, comm, cmdline string, utime, stime int) { d := filepath.Join(root, pid) os.MkdirAll(d, 0o755) os.WriteFile(filepath.Join(d, "comm"), []byte(comm+"\n"), 0o644) os.WriteFile(filepath.Join(d, "cmdline"), []byte(cmdline), 0o644) // The command name may hold spaces and parentheses. stat := pid + " (" + comm + " (x)) S 1 1 1 0 -1 4194560 100 0 0 0 " + strconv.Itoa(utime) + " " + strconv.Itoa(stime) + " 0 0 20 0 9 0 100 0 0\n" os.WriteFile(filepath.Join(d, "stat"), []byte(stat), 0o644) os.WriteFile(filepath.Join(d, "status"), []byte("Name:\t"+comm+"\nVmRSS:\t 51200 kB\nThreads:\t9\n"), 0o644) } proc("90", "named", "/usr/sbin/named\x00-c\x00/etc/other.conf", 1, 1) proc("120", "named", "/usr/sbin/named\x00-c\x00/etc/bind/named.conf\x00-u\x00bind", 300, 100) proc("130", "bash", "bash", 0, 0) if pid := findNamed("/etc/bind/named.conf"); pid != 120 { t.Errorf("findNamed = %d, want the one with the config", pid) } if pid := findNamed("/nowhere.conf"); pid != 90 { t.Errorf("findNamed without a match = %d, want the oldest", pid) } now := time.Unix(1_000_000, 0) p, ok := readNamedProc(120, now) if !ok || p.RSS != 51200*1024 || p.Threads != 9 || p.CPU != 400 { t.Fatalf("readNamedProc = %+v, %v", p, ok) } os.WriteFile(filepath.Join(root, "meminfo"), []byte("MemTotal: 4000000 kB\nMemFree: 100 kB\nMemAvailable: 3000000 kB\n"), 0o644) os.WriteFile(filepath.Join(root, "loadavg"), []byte("0.52 0.40 0.31 1/300 4242\n"), 0o644) os.WriteFile(filepath.Join(root, "uptime"), []byte("86400.12 1000.0\n"), 0o644) h := readHost() if h.MemTotal != 4000000*1024 || h.MemAvailable != 3000000*1024 || len(h.Load) != 3 || h.Load[0] != 0.52 || h.Uptime != 86400 { t.Errorf("readHost = %+v", h) } // Two samples a minute apart: 30 seconds of CPU time is 50%. c := &Collector{minutes: map[int64]map[string]int64{}, opts: Options{MainConfig: "/etc/bind/named.conf"}} snap := &Snapshot{Gauges: map[string]int64{"named:mem": 1000, "cache:rrsets": 7, "cache:mem": 300, "sockets:tcp": 2}, Version: "9.20"} c.sampleServer(-1, now, snap) if len(c.minutes) != 0 { t.Errorf("minute -1 recorded gauges: %v", c.minutes) } proc("120", "named", "/usr/sbin/named\x00-c\x00/etc/bind/named.conf", 2800, 600) // +3000 ticks = 30 s c.sampleServer(600, now.Add(time.Minute), snap) s := c.Server() if s.PID != 120 || s.CPUPercent != 50 || s.RSS != 51200*1024 || s.CacheRRsets != 7 || s.SocketsTCP != 2 || s.Version != "9.20" { t.Errorf("server = %+v", s) } m := c.minutes[600] if m["g:"+GaugeCPU] != 500 || m["gn:"+GaugeCPU] != 1 || m["g:"+GaugeMem] != 1000 || m["g:"+GaugeHostLoad] != 52 { t.Errorf("gauges = %v", m) } } func TestGaugeTimeline(t *testing.T) { store, err := OpenStore(":memory:") if err != nil { t.Fatal(err) } defer store.Close() now := time.Unix(1_800_000_000, 0) start := now.Unix() - now.Unix()%300 - 600 // two 5-minute buckets ago b := &Batch{} for i := int64(0); i < 5; i++ { b.AddCounters(ResMinute, start+i*60, map[string]int64{"g:named:rss": 100 + i*10, "gn:named:rss": 1}) } // The next bucket has no samples. if err := store.Commit(context.Background(), b); err != nil { t.Fatal(err) } q := &Querier{Store: store, Opts: Options{RetainMinute: 7 * 24 * time.Hour, RetainHour: 90 * 24 * time.Hour}, Now: func() time.Time { return now }} tl, err := q.Timeline(context.Background(), 15*time.Minute, "5m", []string{"g:named:rss"}) if err != nil { t.Fatal(err) } vals := map[int64]int64{} for i, ts := range tl.Times { vals[ts] = tl.Series[0].Values[i] } if vals[start] != 120 || vals[start+300] != -1 { t.Errorf("gauge timeline = %v (times %v)", tl.Series[0].Values, tl.Times) } }