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
@@ -45,6 +45,21 @@ var schema = []string{
|
||||
PRIMARY KEY (res, ts, kind, key)
|
||||
) WITHOUT ROWID;
|
||||
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`,
|
||||
// 2: the query log, every answer for a while (stats.query_retention),
|
||||
// for drilling down: what a client asked, who asked for a name
|
||||
// (docs/stats.md#exploring).
|
||||
`CREATE TABLE queries (
|
||||
ts INTEGER NOT NULL,
|
||||
client TEXT NOT NULL,
|
||||
view TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
rcode TEXT NOT NULL,
|
||||
blocked TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
CREATE INDEX queries_ts ON queries (ts);
|
||||
CREATE INDEX queries_client ON queries (client, ts);
|
||||
CREATE INDEX queries_name ON queries (name, ts);`,
|
||||
}
|
||||
|
||||
// OpenStore opens (creating if needed) the statistics database. Use
|
||||
@@ -123,8 +138,25 @@ type Batch struct {
|
||||
counters []counterRow
|
||||
tops []topsWrite
|
||||
meta map[string]string
|
||||
queries []QueryRow
|
||||
}
|
||||
|
||||
// QueryRow is one answer in the query log.
|
||||
type QueryRow struct {
|
||||
Time int64 `json:"ts"` // Unix seconds
|
||||
Client string `json:"client"`
|
||||
View string `json:"view"` // "_default" without views
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
RCode string `json:"rcode"`
|
||||
Blocked string `json:"blocked,omitempty"` // the blocklist (or "rules") that blocked it
|
||||
// ClientName is the client's host name (set by the API).
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
}
|
||||
|
||||
// AddQueries adds answers to the query log.
|
||||
func (b *Batch) AddQueries(rows []QueryRow) { b.queries = append(b.queries, rows...) }
|
||||
|
||||
type counterRow struct {
|
||||
res string
|
||||
ts int64
|
||||
@@ -220,9 +252,39 @@ func (s *Store) Commit(ctx context.Context, b *Batch) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(b.queries) > 0 {
|
||||
st, err := tx.PrepareContext(ctx, `INSERT INTO queries (ts, client, view, name, type, rcode, blocked) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, q := range b.queries {
|
||||
if _, err := st.ExecContext(ctx, q.Time, q.Client, q.View, q.Name, q.Type, q.RCode, q.Blocked); err != nil {
|
||||
st.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
st.Close()
|
||||
}
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// PruneQueries deletes the query log's answers older than before (Unix
|
||||
// time), a slice at a time so the database isn't locked for long.
|
||||
func (s *Store) PruneQueries(ctx context.Context, before int64) (int64, error) {
|
||||
var n int64
|
||||
for {
|
||||
r, err := s.db.ExecContext(ctx, `DELETE FROM queries WHERE rowid IN (SELECT rowid FROM queries WHERE ts < ? LIMIT 50000)`, before)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
c, _ := r.RowsAffected()
|
||||
n += c
|
||||
if c < 50000 {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Meta returns a stored value ("" if unset).
|
||||
func (s *Store) Meta(ctx context.Context, key string) (string, error) {
|
||||
var v string
|
||||
|
||||
Reference in new issue
Block a user