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
@@ -286,3 +286,59 @@ func (s *Server) statsServer(w http.ResponseWriter, r *http.Request, p *auth.Pri
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
return nil
|
||||
}
|
||||
|
||||
// statsExplore: GET /stats/explore?client=&name=&domain=&view=&type=&rcode=
|
||||
// &blocked=&since=24h explores the query log (docs/stats.md#exploring):
|
||||
// totals, a series, the top values of each dimension left open, and the
|
||||
// latest answers, with client host names.
|
||||
func (s *Server) statsExplore(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
q, err := s.statsQuerier(r, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
since, err := statsSince(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v := r.URL.Query()
|
||||
f := stats.ExploreFilter{Client: v.Get("client"), Name: v.Get("name"), Domain: v.Get("domain"), View: v.Get("view"),
|
||||
Type: v.Get("type"), RCode: v.Get("rcode"), Blocked: v.Get("blocked"), Since: since}
|
||||
ex, err := q.Explore(r.Context(), f, 15, 100)
|
||||
if errors.Is(err, stats.ErrNoQueryLog) {
|
||||
return &httpError{http.StatusNotFound, "query_log_off", err.Error()}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.nameClients(r, p, ex.By["client"]); err != nil {
|
||||
return err
|
||||
}
|
||||
// The recent answers' clients, named once each.
|
||||
var es []stats.Entry
|
||||
seen := map[string]int{}
|
||||
for _, row := range ex.Recent {
|
||||
if _, ok := seen[row.Client]; !ok {
|
||||
seen[row.Client] = len(es)
|
||||
es = append(es, stats.Entry{Key: row.Client})
|
||||
}
|
||||
}
|
||||
if err := s.nameClients(r, p, es); err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range ex.Recent {
|
||||
ex.Recent[i].ClientName = es[seen[ex.Recent[i].Client]].Name
|
||||
}
|
||||
// The filter's client, named too.
|
||||
out := struct {
|
||||
*stats.Exploration
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
}{Exploration: ex}
|
||||
if f.Client != "" {
|
||||
one := []stats.Entry{{Key: f.Client}}
|
||||
if err := s.nameClients(r, p, one); err == nil {
|
||||
out.ClientName = one[0].Name
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user