Compare commits
2
Commits
ea26171875
...
a8c7760fc8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8c7760fc8 | ||
|
|
5443e95897 |
No files matched your search
+8
-2
@@ -68,7 +68,9 @@ All notable changes to Jarl are documented here. The format follows
|
||||
an explore page for it: answers over time, the top clients, names,
|
||||
views, types, results and blocklists among them (each a link that
|
||||
narrows it further), and the latest answers. "NXDOMAIN in external" →
|
||||
the clients asking → everything one of them asks. `GET /stats/explore`.
|
||||
the clients asking → everything one of them asks. A filter bar types
|
||||
filters in, and **Explore the answers** opens the page from the
|
||||
Statistics page. `GET /stats/explore`.
|
||||
- **Storage** on the dashboard: the size of Jarl's databases (and how many
|
||||
answers the query log holds), and per disk Jarl uses a bar of how full it
|
||||
is and how much of it the databases take. The Statistics page shows the
|
||||
@@ -95,8 +97,12 @@ All notable changes to Jarl are documented here. The format follows
|
||||
browser console reported a Content Security Policy violation (an inline
|
||||
style the page's policy blocks).
|
||||
|
||||
### Upgrading
|
||||
### Upgrading from 0.5
|
||||
|
||||
- Tested: 0.5.2 packages with views, a linked zone, a reverse zone,
|
||||
blocking, a `tls` statement and statistics, upgraded in place in Debian
|
||||
13 (BIND 9.20.29): everything kept and answered as before, the new
|
||||
features worked on the upgraded data, and a 0.5.2 backup restored.
|
||||
- The database gets two new versions (12 and 13), applied when jarld
|
||||
starts; a 0.5 jarld can't open it afterwards, so take a backup first (`sudo
|
||||
jarld backup`). Nothing changes until you turn `auto_ptr` on for a
|
||||
|
||||
@@ -202,7 +202,12 @@ review step.
|
||||
|
||||
## Status
|
||||
|
||||
Version 0.5.2 is the current release. It adds Prometheus metrics,
|
||||
Version 0.5.2 is the current release, and 0.6.0 is in testing
|
||||
(0.6.0-rc.1): it makes reverse zones easy (suggested from your records,
|
||||
created in the right view and filled, PTR records kept in step), rebuilds
|
||||
comparing a zone's views, lets you explore the statistics down to each
|
||||
client and name (a query log), shows the databases' size, and gives
|
||||
encrypted DNS several host names and views per listen address. 0.5 added Prometheus metrics,
|
||||
address ranges in ACLs and other address match lists, safer changeset
|
||||
handling, internet exposure checks with response rate limiting, TLS
|
||||
certificates Jarl obtains and renews itself, linked zones for split DNS,
|
||||
|
||||
+5
-1
@@ -213,7 +213,11 @@ which shows, for what you clicked:
|
||||
* the latest 100 answers, every cell a link.
|
||||
|
||||
So "external, NXDOMAIN" leads to the clients asking, a client to everything
|
||||
it asks, one of its names to who else asks for it. Filters combine (this
|
||||
it asks, one of its names to who else asks for it. The page also opens
|
||||
from **Explore the answers** at the top of the Statistics page, and has a
|
||||
filter bar to type filters in: a client, a name (or it and the names below
|
||||
it), a view, a record type, a result, blocked or not; it suggests values
|
||||
from what is shown. Filters combine (this
|
||||
client, in this view, blocked only) and each has a ✕ to take it out. A name
|
||||
can be widened to the names below it (`domain=`). The Statistics page's
|
||||
view switches apply here too.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// last days, filtered by any mix of client, name, view, record type, result
|
||||
// and blocking. Every value is a link that adds it to the filter, so a
|
||||
// client leads to its names, a name to the clients asking for it, and so on.
|
||||
import { h, mount, fmtTime, errorBox, badge, backLink } from "../ui.js";
|
||||
import { h, mount, fmtTime, errorBox, badge, backLink, input, select, checkbox } from "../ui.js";
|
||||
import { get } from "../api.js";
|
||||
import { lineChart, chartCard, legend, timeTable, barList, compact } from "../charts.js";
|
||||
import { viewsOff } from "./stats.js";
|
||||
@@ -35,21 +35,18 @@ export async function render(el, params, query) {
|
||||
|
||||
// The Statistics page's view switches apply here too.
|
||||
const off = viewsOff();
|
||||
let views = "";
|
||||
if (off.size) {
|
||||
const all = (await get("/views").catch(() => [])).map((v) => v.name);
|
||||
views = all.filter((v) => !off.has(v)).join(",");
|
||||
}
|
||||
const allViews = (await get("/views").catch(() => [])).map((v) => v.name);
|
||||
const views = off.size ? allViews.filter((v) => !off.has(v)).join(",") : "";
|
||||
let ex;
|
||||
try {
|
||||
ex = await get("/stats/explore", { ...f, since, views: views || undefined });
|
||||
} catch (e) {
|
||||
mount(el, head(f, without, since, null), e.code === "query_log_off"
|
||||
mount(el, head(f, without, since, null), filterBar(f, since, null, allViews), e.code === "query_log_off"
|
||||
? h("div", { class: "msg info" }, e.message) : errorBox(e));
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = [head(f, without, since, ex)];
|
||||
const parts = [head(f, without, since, ex), filterBar(f, since, ex, allViews)];
|
||||
const maxSecs = retentionSecs(ex.retention);
|
||||
parts.push(h("div", { class: "filter-row", role: "group", "aria-label": "Time range" },
|
||||
h("span", { class: "muted" }, "Range"),
|
||||
@@ -86,6 +83,32 @@ export async function render(el, params, query) {
|
||||
mount(el, parts);
|
||||
}
|
||||
|
||||
// filterBar types filters in: a client, a name (or it and the names below
|
||||
// it), a view, a type, a result, blocked or not. It starts from the current
|
||||
// filters and suggests values from what is shown.
|
||||
function filterBar(f, since, ex, views) {
|
||||
const sugg = (id, entries) => h("datalist", { id }, (entries || []).map((e) => h("option", { value: e.key }, e.name || null)));
|
||||
const client = input({ value: f.client || "", placeholder: "address", class: "mono", list: "ex-clients", style: { width: "170px" } });
|
||||
const name = input({ value: f.name || f.domain || "", placeholder: "nas.adbraun.lol", class: "mono", list: "ex-names", style: { width: "220px" } });
|
||||
const below = checkbox("and below", !!f.domain);
|
||||
const view = select([["", "any view"], ...views.map((v) => [v, v])], f.view || "");
|
||||
const type = input({ value: f.type || "", placeholder: "any type", class: "mono", list: "ex-types", style: { width: "110px" } });
|
||||
const rcode = select([["", "any result"], "NOERROR", "NXDOMAIN", "SERVFAIL", "REFUSED"], f.rcode || "");
|
||||
const blocked = select([["", "blocked or not"], ["yes", "blocked"], ["no", "not blocked"], ...((ex && ex.by.blocked) || []).map((e) => [e.key, "blocked by " + blockedLabel(e.key)])], f.blocked || "");
|
||||
if (f.blocked && ![...blocked.options].some((o) => o.value === f.blocked)) blocked.append(h("option", { value: f.blocked, selected: true }, "blocked by " + blockedLabel(f.blocked)));
|
||||
const apply = () => {
|
||||
const n = name.value.trim().toLowerCase().replace(/\.$/, "");
|
||||
const g = { client: client.value.trim(), view: view.value, type: type.value.trim().toUpperCase(), rcode: rcode.value, blocked: blocked.value };
|
||||
if (n) g[below.input.checked ? "domain" : "name"] = n;
|
||||
location.hash = exploreHref(g, since);
|
||||
};
|
||||
const form = h("form", { class: "explore-filters", onsubmit: (e) => { e.preventDefault(); apply(); } },
|
||||
h("label", null, "Client ", client), h("label", null, "Name ", name), below, view, h("label", null, "Type ", type), rcode, blocked,
|
||||
h("button", { type: "submit", class: "primary small" }, "Show"),
|
||||
sugg("ex-clients", ex && ex.by.client), sugg("ex-names", ex && ex.by.name), sugg("ex-types", (ex && ex.by.type) || ["A", "AAAA", "PTR", "MX", "TXT", "HTTPS", "SRV"].map((k) => ({ key: k }))));
|
||||
return h("div", { class: "panel explore-filter-panel" }, form);
|
||||
}
|
||||
|
||||
// head is the title and the filter chips, each with a ✕ to take it out.
|
||||
function head(f, without, since, ex) {
|
||||
const chips = keys.filter((k) => f[k]).map((k) => h("span", { class: "chip filter-chip" },
|
||||
|
||||
@@ -43,7 +43,9 @@ export async function render(el, params, query) {
|
||||
h("div", { class: "muted" }, status.enabled
|
||||
? "From named's counters" + (status.query_log ? " and response log" : "") + " · data since " + (status.first_data ? fmtTime(status.first_data) : "–") +
|
||||
" · kept: minutes " + status.retention.minute + ", hours " + status.retention.hour + ", days " + status.retention.day
|
||||
: "Query statistics are off")));
|
||||
: "Query statistics are off")),
|
||||
status.enabled && status.retention && status.retention.queries
|
||||
? h("div", { class: "actions" }, h("a", { class: "button", href: "#/stats/explore", title: "Every answer of the last " + status.retention.queries + ", by client, name, view, type and result" }, "Explore the answers")) : null);
|
||||
if (!status.enabled) {
|
||||
mount(el, head, disabledPanel());
|
||||
return;
|
||||
|
||||
@@ -614,3 +614,8 @@ table.recent-queries td.oneline-cell { max-width: 40ch; white-space: nowrap; ove
|
||||
.swatch.db { background: var(--accent); }
|
||||
.swatch.other { background: var(--muted); opacity: 0.45; }
|
||||
.card .disk-bar { margin: 6px 0 4px; height: 8px; }
|
||||
|
||||
/* Explore: the filter bar. */
|
||||
form.explore-filters { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; }
|
||||
form.explore-filters label { color: var(--muted); display: inline-flex; gap: 6px; align-items: center; }
|
||||
.explore-filter-panel { padding-top: 10px; padding-bottom: 10px; }
|
||||
Reference in new issue
Block a user