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:
1 parent
fef916b7ab
commit
01ec37b9b2
21 files changed
+922
-27
No files matched your search
@@ -121,3 +121,58 @@ func TestStatsAPI(t *testing.T) {
|
||||
vtok := e.do("POST", "/users/"+itoa(viewerID)+"/tokens", e.token, map[string]any{"name": "t"}, 201)["token"].(string)
|
||||
e.do("GET", "/stats/summary", vtok, nil, 200)
|
||||
}
|
||||
|
||||
func TestStatsExploreAPI(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ss, err := stats.OpenStore(":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ss.Close()
|
||||
now := time.Now().Unix()
|
||||
b := &stats.Batch{}
|
||||
b.AddQueries([]stats.QueryRow{
|
||||
{Time: now - 60, Client: "10.0.0.7", View: "internal", Name: "www.example.com", Type: "A", RCode: "NOERROR"},
|
||||
{Time: now - 50, Client: "10.0.0.7", View: "internal", Name: "ads.tracker.test", Type: "A", RCode: "NXDOMAIN", Blocked: "hagezi"},
|
||||
{Time: now - 40, Client: "198.51.100.9", View: "external", Name: "nope.example.com", Type: "A", RCode: "NXDOMAIN"},
|
||||
})
|
||||
if err := ss.Commit(ctx, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
opts := stats.Options{QueryLog: true, StatisticsChannel: "127.0.0.1:1", TopN: 10, RetainMinute: time.Hour, RetainHour: 24 * time.Hour, RetainDay: 24 * time.Hour, RetainQueries: 7 * 24 * time.Hour}
|
||||
c := stats.NewCollector(ss, opts, nil, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
e := newAPIEnv(t, c)
|
||||
e.do("POST", "/zones", e.token, map[string]any{"name": "example.com", "records": []map[string]any{{"name": "ns1", "type": "A", "data": "192.0.2.1"}, {"name": "laptop", "type": "A", "data": "10.0.0.7"}}}, 201)
|
||||
|
||||
ex := e.do("GET", "/stats/explore?rcode=NXDOMAIN&since=1h", e.token, nil, 200)
|
||||
if ex["total"].(float64) != 2 || ex["blocked"].(float64) != 1 {
|
||||
t.Errorf("NXDOMAIN: %v", ex)
|
||||
}
|
||||
clients := ex["by"].(map[string]any)["client"].([]any)
|
||||
if len(clients) != 2 {
|
||||
t.Fatalf("clients %v", clients)
|
||||
}
|
||||
named := false
|
||||
for _, c := range clients {
|
||||
m := c.(map[string]any)
|
||||
named = named || m["key"] == "10.0.0.7" && m["name"] == "laptop.example.com"
|
||||
}
|
||||
if !named {
|
||||
t.Errorf("client names %v", clients)
|
||||
}
|
||||
ex = e.do("GET", "/stats/explore?client=10.0.0.7", e.token, nil, 200)
|
||||
recent := ex["recent"].([]any)
|
||||
if ex["total"].(float64) != 2 || ex["client_name"] != "laptop.example.com" || len(recent) != 2 || recent[0].(map[string]any)["client_name"] != "laptop.example.com" {
|
||||
t.Errorf("laptop: %v", ex)
|
||||
}
|
||||
if ex := e.do("GET", "/stats/explore?client=10.0.0.7&views=external", e.token, nil, 200); ex["total"].(float64) != 0 {
|
||||
t.Errorf("laptop in external only: %v", ex["total"])
|
||||
}
|
||||
|
||||
// The dashboard reports the databases' size.
|
||||
st := e.do("GET", "/status", e.token, nil, 200)
|
||||
sg, ok := st["storage"].(map[string]any)
|
||||
if !ok || len(sg["databases"].([]any)) == 0 {
|
||||
t.Errorf("storage %v", st["storage"])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user