Explore the statistics: a query log to drill down by client, name, view and more; storage on the dashboard

jarld keeps every answer for a while (stats.retention.queries, default 7 days) in the statistics database: time, client, view, name, type, result and what blocked it. Every client, name, view, record type, result and blocklist on the Statistics page leads to an explore page for it: answers over time, the top values of every other dimension (each a link that narrows further) and the latest answers. GET /stats/explore.

The dashboard shows the size of Jarl's databases (and the query log's rows) and, per disk Jarl uses, how full it is and the databases' share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-13 18:15:36 +01:00
1 parent fef916b7ab
commit 01ec37b9b2
21 files changed
+922 -27

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
package server
import (
"context"
"os"
"path/filepath"
"slices"
"jarl/internal/api"
"jarl/internal/stats"
)
// storage reports the size of Jarl's databases and how full the disks
// Jarl uses are (the dashboard's Storage card).
func (s *Server) storage(ctx context.Context) *api.Storage {
out := &api.Storage{Databases: []api.Database{}, Disks: []api.Disk{}}
dbFile := func(label, path string) *api.Database {
if path == "" || path == ":memory:" {
return nil
}
d := api.Database{Label: label, Path: path}
for _, suffix := range []string{"", "-wal", "-shm"} {
if fi, err := os.Stat(path + suffix); err == nil {
d.Bytes += fi.Size()
}
}
out.Databases = append(out.Databases, d)
out.Total += d.Bytes
return &out.Databases[len(out.Databases)-1]
}
labels := []string{}
dirs := map[string]string{}
add := func(label, dir string) {
if dir != "" {
labels = append(labels, label)
dirs[label] = dir
}
}
const jarlDB, statsDB = "Jarl's database", "statistics database"
if p := s.store.Path(); p != "" {
dbFile(jarlDB, p)
add(jarlDB, filepath.Dir(p))
}
if s.cfg.Stats != nil {
if p := s.cfg.Stats.Store().Path(); p != "" && p != ":memory:" {
if d := dbFile(statsDB, p); d != nil && s.cfg.Stats.Options().RetainQueries > 0 {
if n, err := s.cfg.Stats.Store().QueryLogSize(ctx); err == nil {
d.QueryLogRows = &n
}
}
add(statsDB, filepath.Dir(p))
}
if lf := s.cfg.Stats.Options().LogFile; lf != "" {
add("named's logs", filepath.Dir(lf))
}
}
paths := s.eng.Config().Paths
add("zone files", paths.ZoneDir)
add("named.conf", paths.ConfigDir)
add("Jarl's cache and blocklists", paths.CacheDir)
add("backups", s.cfg.BackupDir)
for _, d := range stats.DiskUsage(labels, dirs) {
disk := api.Disk{Path: d.Path, Holds: d.Holds, Total: d.Total, Free: d.Free}
for _, db := range out.Databases {
if slices.Contains(d.Holds, db.Label) {
disk.Databases += db.Bytes
}
}
out.Disks = append(out.Disks, disk)
}
return out
}