Stats: range buttons follow the retention settings
The Statistics page offers only the ranges kept (longest of the minute, hour and day retentions), adds 1y and 2y when the daily data covers them, caps the heatmap at the hourly retention and links to Explore within retention.queries. Status and explore send retention_seconds. The query log error names stats.retention.queries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7f3564c0a5
commit
1d535c8063
8 files changed
+80
-31
No files matched your search
@@ -42,6 +42,14 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- The Statistics page's range buttons follow `stats.retention`: ranges
|
||||||
|
longer than any data kept are left out, and **1y** and **2y** appear
|
||||||
|
when `retention.day` (or `retention.hour`) covers them (with the
|
||||||
|
default 730d, both do). The busiest-times heatmap says how far back it
|
||||||
|
really goes (at most `retention.hour`), and links to the Explore page
|
||||||
|
keep within `retention.queries` instead of assuming 7 days.
|
||||||
|
`GET /stats/status` and `GET /stats/explore` add `retention_seconds`.
|
||||||
|
|
||||||
- A reverse zone's missing PTR records count only its own view's A and
|
- A reverse zone's missing PTR records count only its own view's A and
|
||||||
AAAA records. An address only another view's records use (an internal
|
AAAA records. An address only another view's records use (an internal
|
||||||
host, seen from the vps view's copy of `10.0.10.in-addr.arpa`) is no
|
host, seen from the vps view's copy of `10.0.10.in-addr.arpa`) is no
|
||||||
@@ -97,6 +105,8 @@ All notable changes to Jarl are documented here. The format follows
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- The "query log is off" error named a setting that doesn't exist,
|
||||||
|
`stats.query_retention`; it's `stats.retention.queries`.
|
||||||
- `jarl zone show` for a reverse zone printed only "and 1 more" under
|
- `jarl zone show` for a reverse zone printed only "and 1 more" under
|
||||||
"Other views:" when the server was older than the CLI (0.6.1-rc.2
|
"Other views:" when the server was older than the CLI (0.6.1-rc.2
|
||||||
sends the count but not the addresses). It now prints just the count
|
sends the count but not the addresses). It now prints just the count
|
||||||
|
|||||||
+2
-2
@@ -579,12 +579,12 @@ Needs `stats:read`. When statistics are disabled, every endpoint but
|
|||||||
|
|
||||||
| Method | Path | Query | Returns |
|
| Method | Path | Query | Returns |
|
||||||
|--------|------|-------|---------|
|
|--------|------|-------|---------|
|
||||||
| GET | `/stats/status` | | `{enabled, query_log, statistics_channel, log_file, last_poll, last_log_line, log_lines_read, first_data, retention, warnings}` |
|
| GET | `/stats/status` | | `{enabled, query_log, statistics_channel, log_file, last_poll, last_log_line, log_lines_read, first_data, retention, retention_seconds, warnings}`: `retention` has `minute`, `hour`, `day`, `queries` as `"7d"`, `retention_seconds` the same in seconds (`queries` 0 without a query log) |
|
||||||
| GET | `/stats/summary` | `since` (90m, 24h, 7d, 2w; default 24h), `top` (default 10) | totals, `avg_qps`, `peak`, `rcodes`, `qtypes`, `views`, `zones`, `answers`, `transport`, `cache_hit_ratio`, `nxdomain_ratio` (without blocked answers), `blocked`, `blocked_ratio`, `blocklists` (blocked per list; `rules` for your rules), `tops` |
|
| GET | `/stats/summary` | `since` (90m, 24h, 7d, 2w; default 24h), `top` (default 10) | totals, `avg_qps`, `peak`, `rcodes`, `qtypes`, `views`, `zones`, `answers`, `transport`, `cache_hit_ratio`, `nxdomain_ratio` (without blocked answers), `blocked`, `blocked_ratio`, `blocklists` (blocked per list; `rules` for your rules), `tops` |
|
||||||
| GET | `/stats/timeline` | `since`, `step` (auto, 1m, 5m, 15m, 1h, 6h, 1d), `key` (repeatable or comma separated; `rcode:*` = every counter of a group; default `queries`) | `{step, times: [unix...], series: [{key, values}]}` |
|
| GET | `/stats/timeline` | `since`, `step` (auto, 1m, 5m, 15m, 1h, 6h, 1d), `key` (repeatable or comma separated; `rcode:*` = every counter of a group; default `queries`) | `{step, times: [unix...], series: [{key, values}]}` |
|
||||||
| GET | `/stats/top` | `kind` (name, client, nxdomain, servfail, refused, blocked, blocked_client), `since`, `limit` (default 20) | `[{key, count}]`; client, refused and blocked_client entries add `name`, the host name of Jarl's record for the address (the summary's client list too) |
|
| GET | `/stats/top` | `kind` (name, client, nxdomain, servfail, refused, blocked, blocked_client), `since`, `limit` (default 20) | `[{key, count}]`; client, refused and blocked_client entries add `name`, the host name of Jarl's record for the address (the summary's client list too) |
|
||||||
| GET | `/stats/heatmap` | `since` (default 28d), `key` (default queries) | `{key, from, avg[7][24], samples[7][24]}`, Monday first, local time, `-1` = no data |
|
| GET | `/stats/heatmap` | `since` (default 28d), `key` (default queries) | `{key, from, avg[7][24], samples[7][24]}`, Monday first, local time, `-1` = no data |
|
||||||
| GET | `/stats/explore` | `?client=&name=&domain=&view=&type=&rcode=&blocked=yes\|no\|LIST&since=24h&views=`; each takes several values, comma separated (any of them), `!value` to leave one out, and names `*.example.com` for it and below | The query log for a filter ([stats.md](stats.md#exploring)): `total`, `blocked`, `series` (per `step` seconds: `t`, `count`, `blocked`), `by` (top 15 of each dimension the filter leaves open: client, name, view, type, rcode, blocked; clients with `name`), `recent` (latest 100: ts, client, client_name, view, name, type, rcode, blocked), `oldest`, `retention`, `client_name`. 404 `query_log_off` without a query log |
|
| GET | `/stats/explore` | `?client=&name=&domain=&view=&type=&rcode=&blocked=yes\|no\|LIST&since=24h&views=`; each takes several values, comma separated (any of them), `!value` to leave one out, and names `*.example.com` for it and below | The query log for a filter ([stats.md](stats.md#exploring)): `total`, `blocked`, `series` (per `step` seconds: `t`, `count`, `blocked`), `by` (top 15 of each dimension the filter leaves open: client, name, view, type, rcode, blocked; clients with `name`), `recent` (latest 100: ts, client, client_name, view, name, type, rcode, blocked), `oldest`, `retention` (`"7d"`) and `retention_seconds`, `client_name`. 404 `query_log_off` without a query log |
|
||||||
| GET | `/stats/server` | | `{server: {rss, mem_in_use, cpu_percent, threads, pid, cache_rrsets, cache_mem, sockets_udp, sockets_tcp, recursion_highwater, tcp_highwater, version, boot_time, config_time, host: {mem_total, mem_available, load, cpus, uptime}}, named: {recursive_clients, tcp_clients, zones, cpus, worker_threads}, disks: [{path, holds, total, free}], database: {path, bytes, query_log_rows}}`; gauges over time: `/stats/timeline?key=g:named:rss` (averages, -1 without a sample) |
|
| GET | `/stats/server` | | `{server: {rss, mem_in_use, cpu_percent, threads, pid, cache_rrsets, cache_mem, sockets_udp, sockets_tcp, recursion_highwater, tcp_highwater, version, boot_time, config_time, host: {mem_total, mem_available, load, cpus, uptime}}, named: {recursive_clients, tcp_clients, zones, cpus, worker_threads}, disks: [{path, holds, total, free}], database: {path, bytes, query_log_rows}}`; gauges over time: `/stats/timeline?key=g:named:rss` (averages, -1 without a sample) |
|
||||||
| GET | `/stats/breakdown` | `group` (qtype, rcode, view, zone, answer, transport, rtt, dnssec, cache, ratelimit, blocklist), `since` | `[{key, count}]` |
|
| GET | `/stats/breakdown` | `group` (qtype, rcode, view, zone, answer, transport, rtt, dnssec, cache, ratelimit, blocklist), `since` | `[{key, count}]` |
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -263,7 +263,11 @@ database, roughly in proportion to the number of views.
|
|||||||
lists for `retention.hour`, daily ones for `retention.day`. Older data is
|
lists for `retention.hour`, daily ones for `retention.day`. Older data is
|
||||||
deleted every hour.
|
deleted every hour.
|
||||||
- Charts use the finest resolution that covers the range: minutes up to 48
|
- Charts use the finest resolution that covers the range: minutes up to 48
|
||||||
hours, hours up to 90 days, days beyond.
|
hours (and `retention.minute`), hours up to `retention.hour`, days beyond.
|
||||||
|
- The Statistics page offers only the ranges the data is kept for: up to 90
|
||||||
|
days with the defaults, plus a year and 2 years when `retention.day` (or
|
||||||
|
`retention.hour`) is that long. The Explore page offers the ranges up to
|
||||||
|
`retention.queries`.
|
||||||
- Top lists are exact within an hour (up to 200,000 distinct names or
|
- Top lists are exact within an hour (up to 200,000 distinct names or
|
||||||
clients per hour, beyond which the rarest are dropped). Daily lists are
|
clients per hour, beyond which the rarest are dropped). Daily lists are
|
||||||
built from the hourly top lists, so for very long tails they are
|
built from the hourly top lists, so for very long tails they are
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ type Status struct {
|
|||||||
Day string `json:"day"`
|
Day string `json:"day"`
|
||||||
Queries string `json:"queries"` // the query log ("" when off)
|
Queries string `json:"queries"` // the query log ("" when off)
|
||||||
} `json:"retention"`
|
} `json:"retention"`
|
||||||
|
// RetentionSeconds is Retention in seconds, for the web UI's ranges.
|
||||||
|
RetentionSeconds struct {
|
||||||
|
Minute int64 `json:"minute"`
|
||||||
|
Hour int64 `json:"hour"`
|
||||||
|
Day int64 `json:"day"`
|
||||||
|
Queries int64 `json:"queries"` // 0 when the query log is off
|
||||||
|
} `json:"retention_seconds"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collector gathers statistics into a Store.
|
// Collector gathers statistics into a Store.
|
||||||
@@ -164,8 +171,12 @@ func NewCollector(store *Store, opts Options, zones ZoneIndex, log *slog.Logger)
|
|||||||
c.status.Retention.Minute = humanDuration(opts.RetainMinute)
|
c.status.Retention.Minute = humanDuration(opts.RetainMinute)
|
||||||
c.status.Retention.Hour = humanDuration(opts.RetainHour)
|
c.status.Retention.Hour = humanDuration(opts.RetainHour)
|
||||||
c.status.Retention.Day = humanDuration(opts.RetainDay)
|
c.status.Retention.Day = humanDuration(opts.RetainDay)
|
||||||
|
c.status.RetentionSeconds.Minute = int64(opts.RetainMinute / time.Second)
|
||||||
|
c.status.RetentionSeconds.Hour = int64(opts.RetainHour / time.Second)
|
||||||
|
c.status.RetentionSeconds.Day = int64(opts.RetainDay / time.Second)
|
||||||
if opts.QueryLog && opts.RetainQueries > 0 {
|
if opts.QueryLog && opts.RetainQueries > 0 {
|
||||||
c.status.Retention.Queries = humanDuration(opts.RetainQueries)
|
c.status.Retention.Queries = humanDuration(opts.RetainQueries)
|
||||||
|
c.status.RetentionSeconds.Queries = int64(opts.RetainQueries / time.Second)
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Exploring the query log (docs/stats.md#exploring): every answer of the
|
// Exploring the query log (docs/stats.md#exploring): every answer of the
|
||||||
// last days (stats.query_retention), filtered by any mix of client, name,
|
// last days (stats.retention.queries), filtered by any mix of client, name,
|
||||||
// view, type, result and blocking, broken down by each of them.
|
// view, type, result and blocking, broken down by each of them.
|
||||||
|
|
||||||
// ExploreFilter narrows the query log. Empty fields don't filter. Each
|
// ExploreFilter narrows the query log. Empty fields don't filter. Each
|
||||||
@@ -47,8 +47,9 @@ type Exploration struct {
|
|||||||
Recent []QueryRow `json:"recent"`
|
Recent []QueryRow `json:"recent"`
|
||||||
// Oldest is the first answer the query log still has.
|
// Oldest is the first answer the query log still has.
|
||||||
Oldest *time.Time `json:"oldest,omitempty"`
|
Oldest *time.Time `json:"oldest,omitempty"`
|
||||||
// Retention is how long it keeps them.
|
// Retention is how long it keeps them, also in seconds.
|
||||||
Retention string `json:"retention"`
|
Retention string `json:"retention"`
|
||||||
|
RetentionSeconds int64 `json:"retention_seconds"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExplorePoint is one step of the series.
|
// ExplorePoint is one step of the series.
|
||||||
@@ -59,7 +60,7 @@ type ExplorePoint struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ErrNoQueryLog is returned when the query log is off.
|
// ErrNoQueryLog is returned when the query log is off.
|
||||||
var ErrNoQueryLog = fmt.Errorf("the query log is off: it needs stats.query_log and a stats.query_retention above 0 in /etc/jarl/jarl.yaml (docs/stats.md#exploring)")
|
var ErrNoQueryLog = fmt.Errorf("the query log is off: it needs stats.query_log and a stats.retention.queries above 0 in /etc/jarl/jarl.yaml (docs/stats.md#exploring)")
|
||||||
|
|
||||||
// values splits a filter field into the values it must be one of and the
|
// values splits a filter field into the values it must be one of and the
|
||||||
// ones (written !VALUE) it must not be: "10.0.0.5, !10.0.0.9".
|
// ones (written !VALUE) it must not be: "10.0.0.5, !10.0.0.9".
|
||||||
@@ -178,7 +179,7 @@ func (q *Querier) Explore(ctx context.Context, f ExploreFilter, topN, recent int
|
|||||||
}
|
}
|
||||||
from := now.Add(-since).Unix()
|
from := now.Add(-since).Unix()
|
||||||
out := &Exploration{From: time.Unix(from, 0), To: now, By: map[string][]Entry{}, Series: []ExplorePoint{}, Recent: []QueryRow{},
|
out := &Exploration{From: time.Unix(from, 0), To: now, By: map[string][]Entry{}, Series: []ExplorePoint{}, Recent: []QueryRow{},
|
||||||
Retention: humanDuration(q.Opts.RetainQueries)}
|
Retention: humanDuration(q.Opts.RetainQueries), RetentionSeconds: int64(q.Opts.RetainQueries / time.Second)}
|
||||||
cond, args := q.where(f, from)
|
cond, args := q.where(f, from)
|
||||||
db := q.Store.db
|
db := q.Store.db
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ var schema = []string{
|
|||||||
PRIMARY KEY (res, ts, kind, key)
|
PRIMARY KEY (res, ts, kind, key)
|
||||||
) WITHOUT ROWID;
|
) WITHOUT ROWID;
|
||||||
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`,
|
CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);`,
|
||||||
// 2: the query log, every answer for a while (stats.query_retention),
|
// 2: the query log, every answer for a while (stats.retention.queries),
|
||||||
// for drilling down: what a client asked, who asked for a name
|
// for drilling down: what a client asked, who asked for a name
|
||||||
// (docs/stats.md#exploring).
|
// (docs/stats.md#exploring).
|
||||||
`CREATE TABLE queries (
|
`CREATE TABLE queries (
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ const filterWords = { client: "client", name: "name", domain: "domain", view: "v
|
|||||||
const keys = Object.keys(filterWords);
|
const keys = Object.keys(filterWords);
|
||||||
const ranges = [["1h", 3600], ["6h", 6 * 3600], ["24h", 86400], ["3d", 3 * 86400], ["7d", 7 * 86400], ["30d", 30 * 86400]];
|
const ranges = [["1h", 3600], ["6h", 6 * 3600], ["24h", 86400], ["3d", 3 * 86400], ["7d", 7 * 86400], ["30d", 30 * 86400]];
|
||||||
|
|
||||||
|
// exploreRange is the longest range here of at most secs ("1h" at least).
|
||||||
|
export function exploreRange(secs) {
|
||||||
|
let out = "1h";
|
||||||
|
for (const [k, s] of ranges) if (s <= secs) out = k;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// exploreHref is the explore page for these filters (and range).
|
// exploreHref is the explore page for these filters (and range).
|
||||||
export function exploreHref(filters, since) {
|
export function exploreHref(filters, since) {
|
||||||
const q = new URLSearchParams();
|
const q = new URLSearchParams();
|
||||||
@@ -74,7 +81,7 @@ export async function render(el, params, query) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const parts = [head(f, go, since, ex), filterBar(f, since, ex, allViews)];
|
const parts = [head(f, go, since, ex), filterBar(f, since, ex, allViews)];
|
||||||
const maxSecs = retentionSecs(ex.retention);
|
const maxSecs = ex.retention_seconds || 7 * 86400;
|
||||||
parts.push(h("div", { class: "filter-row", role: "group", "aria-label": "Time range" },
|
parts.push(h("div", { class: "filter-row", role: "group", "aria-label": "Time range" },
|
||||||
h("span", { class: "muted" }, "Range"),
|
h("span", { class: "muted" }, "Range"),
|
||||||
h("div", { class: "segmented" }, ranges.filter(([, s]) => s <= maxSecs).map(([k]) =>
|
h("div", { class: "segmented" }, ranges.filter(([, s]) => s <= maxSecs).map(([k]) =>
|
||||||
@@ -234,9 +241,3 @@ function stepWord(s) {
|
|||||||
if (s % 3600 === 0) return s === 3600 ? "hour" : s / 3600 + " hours";
|
if (s % 3600 === 0) return s === 3600 ? "hour" : s / 3600 + " hours";
|
||||||
return s === 60 ? "minute" : s / 60 + " minutes";
|
return s === 60 ? "minute" : s / 60 + " minutes";
|
||||||
}
|
}
|
||||||
|
|
||||||
function retentionSecs(r) {
|
|
||||||
const m = /^(\d+)([dhm])$/.exec(r || "");
|
|
||||||
if (!m) return 7 * 86400;
|
|
||||||
return Number(m[1]) * { d: 86400, h: 3600, m: 60 }[m[2]];
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,27 @@ import { h, mount, fmtTime, relTime, errorBox } from "../ui.js";
|
|||||||
import { get } from "../api.js";
|
import { get } from "../api.js";
|
||||||
import { rerender } from "../app.js";
|
import { rerender } from "../app.js";
|
||||||
import { lineChart, chartCard, legend, timeTable, heatmap, heatmapTable, barList, compact, stepSeconds, endLabelWidth } from "../charts.js";
|
import { lineChart, chartCard, legend, timeTable, heatmap, heatmapTable, barList, compact, stepSeconds, endLabelWidth } from "../charts.js";
|
||||||
import { exploreHref } from "./explore.js";
|
import { exploreHref, exploreRange } from "./explore.js";
|
||||||
|
|
||||||
const ranges = [["1h", "1 hour"], ["6h", "6 hours"], ["24h", "24 hours"], ["7d", "7 days"], ["30d", "30 days"], ["90d", "90 days"]];
|
// Ranges: the since value, its words and its button. Only the ones the
|
||||||
const rangeSecs = { "1h": 3600, "6h": 6 * 3600, "24h": 86400, "7d": 7 * 86400, "30d": 30 * 86400, "90d": 90 * 86400 };
|
// statistics are kept for are offered (keptSecs).
|
||||||
|
const ranges = [["1h", "1 hour"], ["6h", "6 hours"], ["24h", "24 hours"], ["7d", "7 days"], ["30d", "30 days"], ["90d", "90 days"],
|
||||||
|
["365d", "year", "1y"], ["730d", "2 years", "2y"]];
|
||||||
|
const rangeSecs = { "1h": 3600, "6h": 6 * 3600, "24h": 86400, "7d": 7 * 86400, "30d": 30 * 86400, "90d": 90 * 86400, "365d": 365 * 86400, "730d": 730 * 86400 };
|
||||||
|
|
||||||
|
// keptSecs is how far back the statistics go: the longest of the minute,
|
||||||
|
// hour and day retentions (a server older than retention_seconds: 2 years).
|
||||||
|
function keptSecs(status) {
|
||||||
|
const r = status.retention_seconds;
|
||||||
|
return r ? Math.max(r.minute, r.hour, r.day) : 730 * 86400;
|
||||||
|
}
|
||||||
|
|
||||||
|
// longestWithin is the longest range of at most secs ("1h" at least).
|
||||||
|
function longestWithin(secs) {
|
||||||
|
let out = "1h";
|
||||||
|
for (const [k] of ranges) if (rangeSecs[k] <= secs) out = k;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// Failed answers keep the same color whatever the range: color follows
|
// Failed answers keep the same color whatever the range: color follows
|
||||||
// the result code, never its rank.
|
// the result code, never its rank.
|
||||||
@@ -36,8 +53,9 @@ const perViewGroups = ["qtype", "rcode", "view", "zone", "blocklist"];
|
|||||||
export async function render(el, params, query) {
|
export async function render(el, params, query) {
|
||||||
const gen = ++generation;
|
const gen = ++generation;
|
||||||
const want = query && query.get ? query.get("since") : null;
|
const want = query && query.get ? query.get("since") : null;
|
||||||
const since = rangeSecs[want] ? want : "24h";
|
|
||||||
const status = await get("/stats/status");
|
const status = await get("/stats/status");
|
||||||
|
const kept = keptSecs(status);
|
||||||
|
const since = rangeSecs[want] && rangeSecs[want] <= kept ? want : longestWithin(Math.min(86400, kept));
|
||||||
const head = h("div", { class: "page-head" },
|
const head = h("div", { class: "page-head" },
|
||||||
h("div", { class: "grow" }, h("h1", null, "Statistics"),
|
h("div", { class: "grow" }, h("h1", null, "Statistics"),
|
||||||
h("div", { class: "muted" }, status.enabled
|
h("div", { class: "muted" }, status.enabled
|
||||||
@@ -50,7 +68,10 @@ export async function render(el, params, query) {
|
|||||||
mount(el, head, disabledPanel());
|
mount(el, head, disabledPanel());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const heatSince = rangeSecs[since] < 7 * 86400 ? "7d" : since;
|
// The heatmap needs hourly counters: at least a week of them, at most
|
||||||
|
// what is kept.
|
||||||
|
const hourSecs = status.retention_seconds ? status.retention_seconds.hour : 90 * 86400;
|
||||||
|
const heatSince = longestWithin(Math.min(Math.max(rangeSecs[since], 7 * 86400), hourSecs));
|
||||||
// Which views: all of them unless some are switched off.
|
// Which views: all of them unless some are switched off.
|
||||||
const allViews = status.query_log ? (await get("/views").catch(() => [])).map((v) => v.name) : [];
|
const allViews = status.query_log ? (await get("/views").catch(() => [])).map((v) => v.name) : [];
|
||||||
const off = viewsOff();
|
const off = viewsOff();
|
||||||
@@ -58,14 +79,14 @@ export async function render(el, params, query) {
|
|||||||
const filtered = allViews.length > 0 && shown.length < allViews.length;
|
const filtered = allViews.length > 0 && shown.length < allViews.length;
|
||||||
const vf = filtered ? { views: shown.join(",") } : {};
|
const vf = filtered ? { views: shown.join(",") } : {};
|
||||||
if (filtered && !shown.length) {
|
if (filtered && !shown.length) {
|
||||||
mount(el, head, filterRow(since, allViews, off), h("div", { class: "empty" }, "No views chosen: switch one on above."));
|
mount(el, head, filterRow(since, kept, allViews, off), h("div", { class: "empty" }, "No views chosen: switch one on above."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = await loadAll(since, heatSince, status.query_log, vf, filtered);
|
data = await loadAll(since, heatSince, status.query_log, vf, filtered);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
mount(el, head, filterRow(since, allViews, off), errorBox(e));
|
mount(el, head, filterRow(since, kept, allViews, off), errorBox(e));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (gen !== generation) return; // the user moved on
|
if (gen !== generation) return; // the user moved on
|
||||||
@@ -74,7 +95,7 @@ export async function render(el, params, query) {
|
|||||||
// sudden drop in traffic.
|
// sudden drop in traffic.
|
||||||
const timeline = complete(data.timeline), errors = complete(data.errors);
|
const timeline = complete(data.timeline), errors = complete(data.errors);
|
||||||
const step = stepSeconds[timeline.step];
|
const step = stepSeconds[timeline.step];
|
||||||
const parts = [head, filterRow(since, allViews, off)];
|
const parts = [head, filterRow(since, kept, allViews, off)];
|
||||||
for (const w of status.warnings || []) parts.push(h("div", { class: "msg warn" }, w));
|
for (const w of status.warnings || []) parts.push(h("div", { class: "msg warn" }, w));
|
||||||
if (filtered) {
|
if (filtered) {
|
||||||
parts.push(h("div", { class: "msg info small" }, "Showing view" + (shown.length > 1 ? "s " : " ") + shown.join(", ") + ": answers from named's response log" +
|
parts.push(h("div", { class: "msg info small" }, "Showing view" + (shown.length > 1 ? "s " : " ") + shown.join(", ") + ": answers from named's response log" +
|
||||||
@@ -120,7 +141,7 @@ export async function render(el, params, query) {
|
|||||||
// Who asks for what. With the query log, every value leads to the
|
// Who asks for what. With the query log, every value leads to the
|
||||||
// explore page: a client to what it asked, a name to who asked.
|
// explore page: a client to what it asked, a name to who asked.
|
||||||
const ql = !!(status.retention || {}).queries;
|
const ql = !!(status.retention || {}).queries;
|
||||||
const ex = (filters) => (ql ? (k) => exploreHref(filters(k), exploreSince(since)) : null);
|
const ex = (filters) => (ql ? (k) => exploreHref(filters(k), exploreSince(since, status)) : null);
|
||||||
if (status.query_log) {
|
if (status.query_log) {
|
||||||
const t = data.tops;
|
const t = data.tops;
|
||||||
parts.push(h("div", { class: "chart-grid" },
|
parts.push(h("div", { class: "chart-grid" },
|
||||||
@@ -201,11 +222,11 @@ function complete(tl) {
|
|||||||
return tl;
|
return tl;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterRow(since, views, off) {
|
function filterRow(since, kept, views, off) {
|
||||||
const range = h("div", { class: "filter-row", role: "group", "aria-label": "Time range" },
|
const range = h("div", { class: "filter-row", role: "group", "aria-label": "Time range" },
|
||||||
h("span", { class: "muted" }, "Range"),
|
h("span", { class: "muted" }, "Range"),
|
||||||
h("div", { class: "segmented" }, ranges.map(([k, label]) =>
|
h("div", { class: "segmented" }, ranges.filter(([k]) => rangeSecs[k] <= kept).map(([k, label, button]) =>
|
||||||
h("a", { href: "#/stats?since=" + k, class: k === since ? "active" : "", "aria-current": k === since ? "true" : null, title: "last " + label }, k))));
|
h("a", { href: "#/stats?since=" + k, class: k === since ? "active" : "", "aria-current": k === since ? "true" : null, title: "last " + label }, button || k))));
|
||||||
if (!views || views.length < 2) return range;
|
if (!views || views.length < 2) return range;
|
||||||
// A toggle per view (together: linked views, say), and all or none.
|
// A toggle per view (together: linked views, say), and all or none.
|
||||||
const set = (next) => { saveViewsOff(next); rerender(); };
|
const set = (next) => { saveViewsOff(next); rerender(); };
|
||||||
@@ -312,10 +333,11 @@ function disabledPanel() {
|
|||||||
"(never individual queries) in a separate database, by default for 7 days per minute, 90 days per hour and 2 years per day. See docs/stats.md."));
|
"(never individual queries) in a separate database, by default for 7 days per minute, 90 days per hour and 2 years per day. See docs/stats.md."));
|
||||||
}
|
}
|
||||||
|
|
||||||
// exploreSince is the Statistics range for the explore page: the query log
|
// exploreSince is the Statistics range for the explore page: at most what
|
||||||
// keeps days, not months.
|
// the query log keeps (days, usually, not months).
|
||||||
function exploreSince(since) {
|
function exploreSince(since, status) {
|
||||||
return rangeSecs[since] > 7 * 86400 ? "7d" : since;
|
const q = status.retention_seconds ? status.retention_seconds.queries : 7 * 86400;
|
||||||
|
return exploreRange(Math.min(rangeSecs[since], q));
|
||||||
}
|
}
|
||||||
|
|
||||||
// dbTile is the statistics database's size: what the statistics cost on
|
// dbTile is the statistics database's size: what the statistics cost on
|
||||||
|
|||||||
Reference in new issue
Block a user