Added:
- Server statistics: named's memory and CPU (from /proc), cache size,
sockets and client counts against their limits (statistics channel,
rndc status), host memory and load, and free disk space. Memory and
CPU are kept over time as gauges (sum and sample count, averaged in
timelines). Statistics page Server section, jarl stats server,
GET /stats/server.
- jarl record add|set|delete|disable|enable --view a,b | all: the same
change in each view, applied as one changeset.
- Blocking can be paused until resumed (jarl block pause indefinitely,
"Until I resume", {"indefinitely": true}).
Changed:
- List options (allow-transfer, forwarders, ...) get their braces added
when given without them.
- The dashboard lists named-checkconf's warnings about the running
configuration; the CLI prints them after an apply.
- Stats page: tile and card rows fill their width, and the queries and
failed answers charts share margins so their time axes line up.
- Dashboard: options have their own tile.
- 24-hour times in the web UI.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.8 KiB
Go
97 lines
3.8 KiB
Go
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)
|
|
}
|
|
}
|