New metrics: section (enabled, listen, allow, token). /metrics is served on the main listeners, the local socket (root and jarld's user) and optional metrics-only addresses; a scrape needs an allowed address or the bearer token. Exports health (named up, problems, drift, backups, blocklists, DNSSEC phases, token expiry) and, with stats on, named's and Jarl's query counters since jarld started plus named's process and cache gauges. docs/metrics.md has example queries and alerts checked with promtool. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
115 lines
4.3 KiB
Go
115 lines
4.3 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
|
|
CPUSeconds float64 `json:"cpu_seconds,omitempty"` // user + system time since named started
|
|
// 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
|
|
srv.CPUSeconds = float64(p.CPU) / clockTicks
|
|
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
|
|
}
|