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>
113 lines
4.2 KiB
Go
113 lines
4.2 KiB
Go
package stats
|
|
|
|
import "time"
|
|
|
|
// Server is the latest server statistics: named's process, memory, cache
|
|
// and sockets, and the host's memory and load. It is sampled with the
|
|
// counters, once a minute.
|
|
type Server struct {
|
|
At time.Time `json:"at"`
|
|
Version string `json:"version,omitempty"`
|
|
BootTime string `json:"boot_time,omitempty"`
|
|
ConfigTime string `json:"config_time,omitempty"`
|
|
// From /proc (0 when named's process isn't visible).
|
|
PID int `json:"pid,omitempty"`
|
|
RSS int64 `json:"rss,omitempty"` // resident memory, bytes
|
|
Threads int64 `json:"threads,omitempty"`
|
|
CPUPercent float64 `json:"cpu_percent"` // of one CPU, over the last minute
|
|
// From the statistics channel.
|
|
MemInUse int64 `json:"mem_in_use"` // named's own count of the memory it uses
|
|
CacheRRsets int64 `json:"cache_rrsets"`
|
|
CacheMem int64 `json:"cache_mem"`
|
|
SocketsUDP int64 `json:"sockets_udp"`
|
|
SocketsTCP int64 `json:"sockets_tcp"`
|
|
RecursionHighwater int64 `json:"recursion_highwater"` // most recursive clients at once since named started
|
|
TCPHighwater int64 `json:"tcp_highwater"`
|
|
Host *Host `json:"host"`
|
|
}
|
|
|
|
// Gauge keys: each is stored as two counters, "g:KEY" (the sum of the
|
|
// minute samples) and "gn:KEY" (their number), so hours and days add up
|
|
// like counters and a bucket's value is their average (see Timeline).
|
|
const (
|
|
GaugeRSS = "named:rss"
|
|
GaugeMem = "named:mem"
|
|
GaugeCPU = "named:cpu" // tenths of a percent of one CPU
|
|
GaugeCacheMem = "cache:mem"
|
|
GaugeCacheRRs = "cache:rrsets"
|
|
GaugeHostMem = "host:mem-used"
|
|
GaugeHostLoad = "host:load" // the 1-minute load average, times 100
|
|
)
|
|
|
|
// gauge records a sample of a gauge for a minute (none when minute < 0).
|
|
func (c *Collector) gauge(minute int64, key string, v int64) {
|
|
if minute < 0 {
|
|
return
|
|
}
|
|
c.count(minute, "g:"+key, v)
|
|
c.count(minute, "gn:"+key, 1)
|
|
}
|
|
|
|
// sampleServer records the gauges for a minute and keeps the latest
|
|
// values (minute -1: only those). snap is nil when named's statistics
|
|
// channel didn't answer.
|
|
// Called with c.mu held.
|
|
func (c *Collector) sampleServer(minute int64, now time.Time, snap *Snapshot) {
|
|
srv := &Server{At: now, Host: readHost()}
|
|
if c.proc == nil || c.proc.PID == 0 {
|
|
c.proc = &namedProc{PID: findNamed(c.opts.MainConfig)}
|
|
}
|
|
p, ok := readNamedProc(c.proc.PID, now)
|
|
if !ok {
|
|
// named restarted with a new PID: look again next minute.
|
|
c.proc = nil
|
|
} else {
|
|
srv.PID, srv.RSS, srv.Threads = p.PID, p.RSS, p.Threads
|
|
if prev := c.proc; prev.At.Before(now) && !prev.At.IsZero() && p.CPU >= prev.CPU {
|
|
// Tenths of a percent of one CPU: ticks / clockTicks is CPU seconds.
|
|
permille := (p.CPU - prev.CPU) * 1000 / clockTicks / max(int64(now.Sub(prev.At)/time.Second), 1)
|
|
srv.CPUPercent = float64(permille) / 10
|
|
c.gauge(minute, GaugeCPU, permille)
|
|
}
|
|
c.proc = p
|
|
c.gauge(minute, GaugeRSS, p.RSS)
|
|
}
|
|
if h := srv.Host; h.MemTotal > 0 {
|
|
c.gauge(minute, GaugeHostMem, h.MemTotal-h.MemAvailable)
|
|
}
|
|
if h := srv.Host; len(h.Load) > 0 {
|
|
c.gauge(minute, GaugeHostLoad, int64(h.Load[0]*100))
|
|
}
|
|
if snap != nil && snap.Gauges != nil {
|
|
g := snap.Gauges
|
|
srv.Version, srv.BootTime, srv.ConfigTime = snap.Version, snap.BootTime, snap.ConfigTime
|
|
srv.MemInUse, srv.CacheRRsets, srv.CacheMem = g["named:mem"], g["cache:rrsets"], g["cache:mem"]
|
|
srv.SocketsUDP, srv.SocketsTCP = g["sockets:udp"], g["sockets:tcp"]
|
|
srv.RecursionHighwater, srv.TCPHighwater = g["named:recursion-highwater"], g["named:tcp-highwater"]
|
|
c.gauge(minute, GaugeMem, srv.MemInUse)
|
|
c.gauge(minute, GaugeCacheMem, srv.CacheMem)
|
|
c.gauge(minute, GaugeCacheRRs, srv.CacheRRsets)
|
|
}
|
|
c.server = srv
|
|
}
|
|
|
|
// Server returns the latest server statistics (nil before the first
|
|
// minute).
|
|
func (c *Collector) Server() *Server {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.server == nil {
|
|
return nil
|
|
}
|
|
s := *c.server
|
|
return &s
|
|
}
|
|
|
|
// Disk is the space on a file system Jarl and named use.
|
|
type Disk struct {
|
|
Path string `json:"path"` // the first of the directories on it
|
|
Holds []string `json:"holds"` // what is on it: zone files, logs, ...
|
|
Total int64 `json:"total"`
|
|
Free int64 `json:"free"` // available to unprivileged users
|
|
}
|