Server statistics, multi-view record commands, and rc.3 fixes
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>
This commit is contained in:
1 parent
b097e10bef
commit
7be0762f48
48 files changed
+1346
-128
No files matched your search
@@ -376,24 +376,30 @@ func (s *Server) blockingStatus(w http.ResponseWriter, r *http.Request, p *auth.
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"paused_until": st.Settings.BlockingPausedUntil})
|
||||
writeJSON(w, http.StatusOK, map[string]any{"paused_until": st.Settings.BlockingPausedUntil, "paused_indefinitely": st.Settings.BlockingPausedIndefinitely})
|
||||
return nil
|
||||
}
|
||||
|
||||
// pauseBlocking: POST /blocking/pause {"minutes": 10} lets every name
|
||||
// through until then; POST /blocking/resume ends it.
|
||||
// through until then, {"indefinitely": true} until resumed; POST
|
||||
// /blocking/resume ends it.
|
||||
func (s *Server) pauseBlocking(resume bool) func(http.ResponseWriter, *http.Request, *auth.Principal) error {
|
||||
return func(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
var in engine.BlockingPauseInput
|
||||
if !resume {
|
||||
var body struct {
|
||||
Minutes int `json:"minutes"`
|
||||
Minutes int `json:"minutes"`
|
||||
Indefinitely bool `json:"indefinitely"`
|
||||
}
|
||||
if err := decodeJSON(r, &body); err != nil {
|
||||
return err
|
||||
}
|
||||
if body.Indefinitely {
|
||||
in.Indefinitely = true
|
||||
return s.mutate(w, r, p, "", []engine.Request{engine.NewRequest(engine.OpBlockingPause, 0, in)}, nil)
|
||||
}
|
||||
if body.Minutes < 1 || time.Duration(body.Minutes)*time.Minute > engine.MaxBlockingPause {
|
||||
return badRequest("pause for 1 to %d minutes", int(engine.MaxBlockingPause/time.Minute))
|
||||
return badRequest("pause for 1 to %d minutes, or indefinitely", int(engine.MaxBlockingPause/time.Minute))
|
||||
}
|
||||
until := time.Now().Add(time.Duration(body.Minutes) * time.Minute).Truncate(time.Second)
|
||||
in.Until = &until
|
||||
|
||||
@@ -283,6 +283,7 @@ func (s *Server) status(w http.ResponseWriter, r *http.Request, p *auth.Principa
|
||||
resp.BlocklistErrors = s.blocklistErrors(r.Context())
|
||||
if st, err := s.store.LoadState(r.Context()); err == nil {
|
||||
resp.BlockingPausedUntil = st.Settings.BlockingPausedUntil
|
||||
resp.BlockingPausedIndefinitely = st.Settings.BlockingPausedIndefinitely
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -240,3 +241,34 @@ func clientNames(st *model.State, want map[netip.Addr]bool, readable func(*model
|
||||
}
|
||||
return ptr
|
||||
}
|
||||
|
||||
// statsServer: GET /stats/server is the latest server statistics (named's
|
||||
// memory, CPU, cache and sockets, the host), named's client counts and
|
||||
// limits (rndc status), and the space left on the file systems Jarl and
|
||||
// named write to.
|
||||
func (s *Server) statsServer(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
if _, err := s.statsQuerier(p); err != nil {
|
||||
return err
|
||||
}
|
||||
out := map[string]any{"server": s.cfg.Stats.Server()}
|
||||
if ns, err := s.eng.NamedStatus(r.Context()); err == nil {
|
||||
out["named"] = map[string]string{"recursive_clients": ns.RecursiveClients, "tcp_clients": ns.TCPClients, "zones": ns.Zones,
|
||||
"cpus": ns.CPUs, "worker_threads": ns.WorkerThreads, "last_configured": ns.LastConfigured}
|
||||
} else {
|
||||
out["named_error"] = err.Error()
|
||||
}
|
||||
paths := s.eng.Config().Paths
|
||||
dirs := map[string]string{"zone files": paths.ZoneDir, "Jarl's cache and blocklists": paths.CacheDir, "named.conf": paths.ConfigDir}
|
||||
labels := []string{"zone files", "Jarl's cache and blocklists", "named.conf"}
|
||||
if lf := s.cfg.Stats.Options().LogFile; lf != "" {
|
||||
dirs["named's logs"] = filepath.Dir(lf)
|
||||
labels = append(labels, "named's logs")
|
||||
}
|
||||
if db := s.cfg.Stats.Store().Path(); db != "" {
|
||||
dirs["statistics database"] = filepath.Dir(db)
|
||||
labels = append(labels, "statistics database")
|
||||
}
|
||||
out["disks"] = stats.DiskUsage(labels, dirs)
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
return nil
|
||||
}
|
||||
@@ -156,6 +156,7 @@ func (s *Server) routes() {
|
||||
m.HandleFunc("GET "+p+"/stats/timeline", s.authed(s.statsTimeline))
|
||||
m.HandleFunc("GET "+p+"/stats/heatmap", s.authed(s.statsHeatmap))
|
||||
m.HandleFunc("GET "+p+"/stats/breakdown", s.authed(s.statsBreakdown))
|
||||
m.HandleFunc("GET "+p+"/stats/server", s.authed(s.statsServer))
|
||||
m.HandleFunc(p+"/", func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusNotFound, "not_found", "no such API endpoint: "+r.Method+" "+r.URL.Path)
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user