v0.26.2
This commit is contained in:
1 parent
b9d137eee1
commit
32e6875980
51 files changed
+1255
-452
No files matched your search
@@ -344,7 +344,7 @@ function authReturnDestination() {
|
||||
}
|
||||
function lock(notify = true) {
|
||||
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
|
||||
$("workspace").querySelectorAll("form").forEach(form => form.reset());
|
||||
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
|
||||
@@ -355,7 +355,7 @@ function lock(notify = true) {
|
||||
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
|
||||
}
|
||||
|
||||
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-open").hidden = false; $("file-logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyFileLogs?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences); await loadConfig(); $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false; renderSession(); await refresh(true); startLive(); document.documentElement.classList.remove("app-loading"); }
|
||||
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-menu").hidden = false; $("logs-open").hidden = false; $("file-logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyFileLogs?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences); await loadConfig(); $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false; renderSession(); await refresh(true); startLive(); document.documentElement.classList.remove("app-loading"); }
|
||||
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [new Date(item.time * 1000).toLocaleString(locale()), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
|
||||
let tunnelRefreshTimer = null;
|
||||
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
|
||||
|
||||
@@ -6,7 +6,7 @@ function renderSettings() {
|
||||
if (!settingsDirty.has("response-form")) {
|
||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||
$("expose-request-id").checked = !!diagnostics.request_id;
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.1";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.2";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
|
||||
+115
-53
@@ -3,44 +3,39 @@
|
||||
let currentEntries = [], selectedIndex = null;
|
||||
let preferencesLoaded = false, preferenceSaveRunning = false, preferenceSaveQueued = false;
|
||||
let searchTimer = null;
|
||||
|
||||
const columns = [
|
||||
{ id: "time", label: "time", value: entry => entryTime(entry) },
|
||||
{ id: "event", label: "fileLogsEvent", value: entry => entry.event || "—" },
|
||||
{ id: "status", label: "status", value: entry => entryStatus(entry) },
|
||||
{ id: "client_ip", label: "clientIp", value: entry => entry.client_ip || entry.source_ip || "—" },
|
||||
{ id: "vhost", label: "vhost", value: entry => entry.vhost || "—" },
|
||||
{ id: "transport", label: "logTransport", value: entry => entry.transport ? `${entry.transport}${entry.http_version ? ` / HTTP ${entry.http_version}` : ""}` : "—" },
|
||||
{ id: "matched_by", label: "fileLogsMatchedBy", value: entry => entry.matched_by || "—" },
|
||||
{ id: "sni", label: "fileLogsSni", value: entry => entry.sni || entry.tls_sni || "—" },
|
||||
{ id: "authority", label: "fileLogsAuthority", value: entry => entry.authority || entry.host || "—" },
|
||||
{ id: "record", label: "fileLogsRecord", value: entry => entrySummary(entry) },
|
||||
{ id: "source", label: "fileLogsSource", value: entry => entry._source || "—" },
|
||||
];
|
||||
const defaultColumnIds = ["time", "event", "status", "client_ip", "vhost", "record", "source"];
|
||||
const specialLabels = new Map([
|
||||
["time", "time"], ["event", "fileLogsEvent"], ["status", "status"],
|
||||
["client_ip", "clientIp"], ["vhost", "vhost"], ["record", "fileLogsRecord"],
|
||||
["source", "fileLogsSource"],
|
||||
]);
|
||||
let availableColumns = new Set(defaultColumnIds);
|
||||
let visibleColumns = new Set(defaultColumnIds);
|
||||
|
||||
|
||||
const sourceSelect = () => $("file-log-source");
|
||||
const currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100));
|
||||
|
||||
function sourceOption(source) {
|
||||
const label = `${source.label} · ${source.format} · ${bytes(source.size || 0)}${source.enabled ? ` · ${t("enabled")}` : ""}`;
|
||||
const label = `${source.label} · ${source.format} · ${bytes(Number(source.size) || 0)}`;
|
||||
const option = el("option", label); option.value = source.id; return option;
|
||||
}
|
||||
function renderInventory(value) {
|
||||
inventory = value;
|
||||
const selected = sourceSelect().value || "all";
|
||||
const select = sourceSelect();
|
||||
const selected = select.value || "all";
|
||||
const sources = Array.isArray(value?.sources) ? value.sources : [];
|
||||
const options = [Object.assign(el("option", t("fileLogsAllSources")), { value: "all" }), ...sources.map(sourceOption)];
|
||||
sourceSelect().replaceChildren(...options);
|
||||
sourceSelect().value = ["all", ...sources.map(source => source.id)].includes(selected) ? selected : "all";
|
||||
$("file-log-inventory").textContent = sources.length
|
||||
? sources.map(source => `${source.label}: ${source.path} (${bytes(source.size || 0)})`).join(" · ")
|
||||
: t("fileLogsNoSources");
|
||||
select.replaceChildren(...options);
|
||||
select.value = sources.some(source => source.id === selected) ? selected : "all";
|
||||
}
|
||||
async function loadInventory() {
|
||||
const value = await api("/api/file-logs"); renderInventory(value); return value;
|
||||
const value = await api("/api/file-logs");
|
||||
renderInventory(value);
|
||||
return value;
|
||||
}
|
||||
|
||||
const currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100));
|
||||
|
||||
function entryTime(entry) {
|
||||
if (entry.time_ms) return new Date(Number(entry.time_ms)).toLocaleString(locale());
|
||||
if (entry.timestamp) return String(entry.timestamp);
|
||||
@@ -54,24 +49,67 @@
|
||||
if (entry.raw) return entry.raw;
|
||||
return entry.outcome || "—";
|
||||
}
|
||||
function entryStatus(entry) {
|
||||
const status = Number(entry.status) || 0;
|
||||
return status ? String(status) : "—";
|
||||
function nestedValue(entry, path) {
|
||||
let value = entry;
|
||||
for (const part of path.split(".")) {
|
||||
if (!value || typeof value !== "object" || !Object.hasOwn(value, part)) return undefined;
|
||||
value = value[part];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function displayValue(value) {
|
||||
if (value === undefined || value === null || value === "") return "—";
|
||||
if (typeof value === "object") return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
function columnValue(entry, id) {
|
||||
if (id === "time") return entryTime(entry);
|
||||
if (id === "record") return entrySummary(entry);
|
||||
if (id === "source") return entry._source || "—";
|
||||
return displayValue(nestedValue(entry, id));
|
||||
}
|
||||
function columnLabel(id) {
|
||||
const key = specialLabels.get(id);
|
||||
return key ? t(key) : id;
|
||||
}
|
||||
function normalizedField(field) {
|
||||
if (field === "_source") return "source";
|
||||
if (field === "_source_kind" || field === "_source_format") return null;
|
||||
return field;
|
||||
}
|
||||
function orderedColumns() {
|
||||
const preferred = defaultColumnIds.filter(id => availableColumns.has(id));
|
||||
const rest = [...availableColumns].filter(id => !defaultColumnIds.includes(id)).sort((a, b) => a.localeCompare(b, locale(), { sensitivity: "base" }));
|
||||
return [...preferred, ...rest];
|
||||
}
|
||||
function applyFields(fields = []) {
|
||||
for (const field of fields) {
|
||||
if (typeof field !== "string") continue;
|
||||
const normalized = normalizedField(field);
|
||||
if (normalized) availableColumns.add(normalized);
|
||||
}
|
||||
for (const id of visibleColumns) availableColumns.add(id);
|
||||
syncColumnPicker(); syncHeaders();
|
||||
}
|
||||
function syncHeaders() {
|
||||
document.querySelectorAll("#file-logs-table-head [data-file-log-column]").forEach(header => {
|
||||
header.hidden = !visibleColumns.has(header.dataset.fileLogColumn);
|
||||
const row = $("file-logs-table-head").querySelector("tr");
|
||||
const headers = orderedColumns().filter(id => visibleColumns.has(id)).map(id => {
|
||||
const header = el("th", columnLabel(id)); header.dataset.fileLogColumn = id;
|
||||
const key = specialLabels.get(id); if (key) header.dataset.i18n = key;
|
||||
return header;
|
||||
});
|
||||
row.replaceChildren(...headers);
|
||||
}
|
||||
function syncColumnPicker() {
|
||||
const picker = $("file-log-column-options");
|
||||
picker.replaceChildren(...columns.map(column => {
|
||||
picker.replaceChildren(...orderedColumns().map(id => {
|
||||
const label = el("label", undefined, "check log-column-option");
|
||||
const input = document.createElement("input"); input.type = "checkbox"; input.checked = visibleColumns.has(column.id); input.dataset.column = column.id;
|
||||
const text = el("span", t(column.label)); text.dataset.i18n = column.label;
|
||||
const input = document.createElement("input"); input.type = "checkbox"; input.checked = visibleColumns.has(id); input.dataset.column = id;
|
||||
const text = el("span", columnLabel(id));
|
||||
const key = specialLabels.get(id); if (key) text.dataset.i18n = key;
|
||||
input.addEventListener("change", () => {
|
||||
if (!input.checked && visibleColumns.size === 1) { input.checked = true; toast(t("fileLogsColumnsRequired"), true); return; }
|
||||
if (input.checked) visibleColumns.add(column.id); else visibleColumns.delete(column.id);
|
||||
if (input.checked) visibleColumns.add(id); else visibleColumns.delete(id);
|
||||
syncHeaders(); renderRows(); void savePreferences();
|
||||
});
|
||||
label.append(input, text); return label;
|
||||
@@ -79,9 +117,10 @@
|
||||
}
|
||||
function applyPreferences(preferences) {
|
||||
const selected = Array.isArray(preferences?.file_log_columns)
|
||||
? preferences.file_log_columns.filter(id => columns.some(column => column.id === id))
|
||||
? preferences.file_log_columns.filter(id => typeof id === "string" && id.length <= 128)
|
||||
: [];
|
||||
visibleColumns = new Set(selected.length ? selected : defaultColumnIds);
|
||||
for (const id of visibleColumns) availableColumns.add(id);
|
||||
preferencesLoaded = true;
|
||||
syncColumnPicker(); syncHeaders(); renderRows();
|
||||
}
|
||||
@@ -92,45 +131,64 @@
|
||||
try {
|
||||
do {
|
||||
preferenceSaveQueued = false;
|
||||
const selected = columns.filter(column => visibleColumns.has(column.id)).map(column => column.id);
|
||||
const selected = orderedColumns().filter(id => visibleColumns.has(id));
|
||||
const preferences = await window.rustProxyPreferences.update({ file_log_columns: selected });
|
||||
if (!preferenceSaveQueued) applyPreferences(preferences);
|
||||
} while (preferenceSaveQueued);
|
||||
} catch (error) { toast(error.message, true); }
|
||||
finally { preferenceSaveRunning = false; }
|
||||
}
|
||||
function flattenFields(value, prefix = "", output = []) {
|
||||
if (output.length >= 128) return output;
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
if (key === "_source_kind") continue;
|
||||
const path = prefix ? `${prefix}.${key}` : key;
|
||||
if (child && typeof child === "object" && !Array.isArray(child)) flattenFields(child, path, output);
|
||||
else output.push([path, displayValue(child)]);
|
||||
if (output.length >= 128) break;
|
||||
}
|
||||
} else if (prefix) output.push([prefix, displayValue(value)]);
|
||||
return output;
|
||||
}
|
||||
function detailRow(entry) {
|
||||
const row = el("tr", undefined, "file-log-detail-row");
|
||||
const cell = el("td"); cell.colSpan = Math.max(1, visibleColumns.size);
|
||||
const detail = el("div", undefined, "file-log-inline-detail");
|
||||
const heading = el("strong", t("fileLogsParsedFields")); heading.dataset.i18n = "fileLogsParsedFields";
|
||||
const fields = el("div", undefined, "file-log-field-grid");
|
||||
for (const [key, value] of flattenFields(entry)) {
|
||||
const item = el("div", undefined, "file-log-field");
|
||||
item.append(el("code", key), el("span", value)); fields.append(item);
|
||||
}
|
||||
const raw = el("details", undefined, "file-log-raw-json");
|
||||
const summary = el("summary", t("fileLogsRawJson")); summary.dataset.i18n = "fileLogsRawJson";
|
||||
raw.append(summary, el("pre", JSON.stringify(entry, null, 2), "metrics-example"));
|
||||
detail.append(heading, fields, raw); cell.append(detail); row.append(cell); return row;
|
||||
}
|
||||
function renderRows(entries = currentEntries) {
|
||||
currentEntries = entries;
|
||||
const ids = orderedColumns().filter(id => visibleColumns.has(id));
|
||||
const nodes = [];
|
||||
entries.forEach((entry, index) => {
|
||||
const row = el("tr"); row.tabIndex = 0; row.classList.toggle("selected", selectedIndex === index);
|
||||
row.setAttribute("aria-expanded", selectedIndex === index ? "true" : "false");
|
||||
for (const column of columns) {
|
||||
if (!visibleColumns.has(column.id)) continue;
|
||||
const value = column.value(entry);
|
||||
const cell = el("td", String(value)); cell.title = String(value); row.append(cell);
|
||||
for (const id of ids) {
|
||||
const value = columnValue(entry, id), cell = el("td", value); cell.title = value; row.append(cell);
|
||||
}
|
||||
const select = () => { selectedIndex = selectedIndex === index ? null : index; renderRows(); };
|
||||
row.addEventListener("click", select);
|
||||
row.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); select(); } });
|
||||
nodes.push(row);
|
||||
if (selectedIndex === index) {
|
||||
const detailRow = el("tr", undefined, "file-log-detail-row");
|
||||
const cell = el("td"); cell.colSpan = Math.max(1, visibleColumns.size);
|
||||
const heading = el("strong", t("fileLogsDetails")); heading.dataset.i18n = "fileLogsDetails";
|
||||
const pre = el("pre", JSON.stringify(entry, null, 2), "metrics-example");
|
||||
const detail = el("div", undefined, "file-log-inline-detail"); detail.append(heading, pre); cell.append(detail); detailRow.append(cell); nodes.push(detailRow);
|
||||
}
|
||||
nodes.push(row); if (selectedIndex === index) nodes.push(detailRow(entry));
|
||||
});
|
||||
$("file-log-rows").replaceChildren(...nodes);
|
||||
$("file-logs-empty").hidden = entries.length !== 0;
|
||||
}
|
||||
function queryString() {
|
||||
const params = new URLSearchParams();
|
||||
params.set("source", sourceSelect().value || "all");
|
||||
for (const [id, name] of [["file-log-q", "q"], ["file-log-event", "event"], ["file-log-status", "status_class"], ["file-log-client", "client_ip"], ["file-log-vhost", "vhost"]]) {
|
||||
const value = $(id).value.trim(); if (value && value !== "all") params.set(name, value);
|
||||
}
|
||||
const source = sourceSelect().value || "all";
|
||||
if (source !== "all") params.set("source", source);
|
||||
const q = $("file-log-q").value.trim(); if (q) params.set("q", q);
|
||||
params.set("offset", String(offset)); params.set("limit", String(currentLimit()));
|
||||
return params.toString();
|
||||
}
|
||||
@@ -142,6 +200,7 @@
|
||||
if (!inventory) await loadInventory();
|
||||
const value = await api(`/api/file-logs/query?${queryString()}`);
|
||||
lastTotal = Number(value.total) || 0; selectedIndex = null;
|
||||
applyFields(Array.isArray(value.fields) ? value.fields : []);
|
||||
renderRows(Array.isArray(value.entries) ? value.entries : []);
|
||||
const from = lastTotal ? offset + 1 : 0, to = Math.min(offset + currentLimit(), lastTotal);
|
||||
$("file-log-summary").textContent = t("fileLogsSummary")
|
||||
@@ -156,16 +215,19 @@
|
||||
function reset() {
|
||||
inventory = null; offset = 0; lastTotal = 0; currentEntries = []; selectedIndex = null;
|
||||
preferencesLoaded = false; preferenceSaveRunning = false; preferenceSaveQueued = false;
|
||||
visibleColumns = new Set(defaultColumnIds); $("file-log-rows").replaceChildren(); syncColumnPicker(); syncHeaders();
|
||||
availableColumns = new Set(defaultColumnIds); visibleColumns = new Set(defaultColumnIds);
|
||||
sourceSelect().replaceChildren(Object.assign(el("option", t("fileLogsAllSources")), { value: "all" }));
|
||||
$("file-log-rows").replaceChildren(); syncColumnPicker(); syncHeaders();
|
||||
}
|
||||
$("file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
||||
$("file-logs-open").addEventListener("click", () => { $("logs-menu").open = false; setPanel("filelogs"); });
|
||||
$("settings-file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
||||
$("file-logs-refresh").addEventListener("click", () => refresh(true));
|
||||
$("file-logs-filter").addEventListener("submit", event => { event.preventDefault(); void refresh(true); });
|
||||
$("file-log-q").addEventListener("input", () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => void refresh(true), 250); });
|
||||
$("file-log-limit").addEventListener("change", () => refresh(true));
|
||||
sourceSelect().addEventListener("change", () => refresh(true));
|
||||
$("file-logs-prev").addEventListener("click", () => { offset = Math.max(0, offset - currentLimit()); void refresh(); });
|
||||
$("file-logs-next").addEventListener("click", () => { if (offset + currentLimit() < lastTotal) { offset += currentLimit(); void refresh(); } });
|
||||
sourceSelect().addEventListener("change", () => refresh(true));
|
||||
syncColumnPicker(); syncHeaders();
|
||||
window.rustProxyFileLogs = { refresh: () => refresh(false), reset, inventory: loadInventory, applyPreferences };
|
||||
})();
|
||||
+48
-62
@@ -64,13 +64,18 @@
|
||||
<header class="topbar"><span class="breadcrumb">CONTROL ROOM <span>/</span> <b
|
||||
id="page-label">Overview</b></span>
|
||||
<div class="top-actions"><a class="quiet" href="/docs" data-i18n="documentation" id="api-docs"
|
||||
hidden="">Documentation</a><button class="quiet" data-i18n="logs" hidden="" id="file-logs-open"
|
||||
type="button">Logs</button><button class="quiet" data-i18n="liveLogs" hidden="" id="logs-open"
|
||||
type="button">Live
|
||||
access logs</button><select aria-label="Language" class="language-select"
|
||||
data-i18n-aria="language" id="language"></select><button class="quiet" data-i18n="about"
|
||||
id="about-open" type="button">About</button><button class="quiet" data-i18n="logout" hidden=""
|
||||
id="logout">Lock</button></div>
|
||||
hidden="">Documentation</a>
|
||||
<details class="top-log-menu" hidden="" id="logs-menu">
|
||||
<summary class="quiet" data-i18n="logs">Logs</summary>
|
||||
<div class="top-log-menu-items"><button class="quiet" data-i18n="storedLogs" hidden=""
|
||||
id="file-logs-open" type="button">Stored logs</button><button class="quiet"
|
||||
data-i18n="liveLogs" hidden="" id="logs-open" type="button">Live access logs</button>
|
||||
</div>
|
||||
</details><select aria-label="Language" class="language-select" data-i18n-aria="language"
|
||||
id="language"></select><button class="quiet" data-i18n="about" id="about-open"
|
||||
type="button">About</button><button class="quiet" data-i18n="logout" hidden=""
|
||||
id="logout">Lock</button>
|
||||
</div>
|
||||
</header>
|
||||
<section class="login-section" hidden="" id="login">
|
||||
<div class="eyebrow" data-i18n="firstSlice">THE FIRST WORKING SLICE</div>
|
||||
@@ -551,7 +556,7 @@
|
||||
<option data-i18n="vhLastRecovery" value="lastRecovery">Last recovery</option>
|
||||
</select></label>
|
||||
<button class="quiet backend-sort-direction" data-i18n-aria="backendSortAscending"
|
||||
id="pool-sort-direction" type="button">A-Z</button>
|
||||
id="pool-sort-direction" type="button">↑</button>
|
||||
<details class="backend-column-picker">
|
||||
<summary class="quiet" data-i18n="backendColumns">Columns</summary>
|
||||
<div class="backend-column-options">
|
||||
@@ -595,8 +600,6 @@
|
||||
vhosts</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="fine" data-i18n="vhHealthHint">Live state: leases cover the entire exchange; traffic
|
||||
is an EWMA of payload bytes.</p>
|
||||
<div class="table-scroll">
|
||||
<table class="pool-table">
|
||||
<thead>
|
||||
@@ -1502,75 +1505,58 @@
|
||||
<div><span class="small-label" data-i18n="fileLogsMode">LOCAL / READ-ONLY / BOUNDED</span>
|
||||
<h2 data-i18n="fileLogsTitle">Stored log explorer</h2>
|
||||
</div>
|
||||
<button class="outline" data-i18n="refresh" id="file-logs-refresh"
|
||||
type="button">Refresh</button>
|
||||
<div class="file-logs-heading-actions">
|
||||
<details class="file-log-search-help">
|
||||
<summary class="quiet" data-i18n="fileLogsSearchHelp">Search syntax</summary>
|
||||
<div class="file-log-search-help-popover">
|
||||
<strong data-i18n="fileLogsSearchHelpTitle">Kibana-style field search</strong>
|
||||
<p class="fine" data-i18n="fileLogsSearchHelpText">Use field:value clauses.
|
||||
Clauses are joined with AND; prefix a clause with ! to exclude it. Quotes
|
||||
keep spaces together.</p>
|
||||
<code>req: GET AND !country: PL</code>
|
||||
<code>status:>=500 AND path:/api/*</code>
|
||||
<code>error:"connection reset"</code>
|
||||
</div>
|
||||
</details>
|
||||
<button class="outline" data-i18n="refresh" id="file-logs-refresh"
|
||||
type="button">Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="fine" data-i18n="fileLogsHelp">Reads only configured RustProxy log files. It is
|
||||
independent from Live Logs and never accepts an arbitrary filesystem path.</p>
|
||||
<p class="fine" data-i18n="fileLogsHelp">Searches bounded RustProxy access/error logs and parses
|
||||
structured fields without exposing filesystem paths.</p>
|
||||
<form class="file-logs-filter" id="file-logs-filter">
|
||||
<label><span data-i18n="fileLogsSource">Source</span><select id="file-log-source">
|
||||
<option value="all">All configured files</option>
|
||||
<label class="file-log-source"><span data-i18n="fileLogsSource">Source</span><select
|
||||
id="file-log-source">
|
||||
<option data-i18n="fileLogsAllSources" value="all">All configured files</option>
|
||||
</select></label>
|
||||
<label class="file-log-search"><span data-i18n="search">Search</span><input id="file-log-q"
|
||||
maxlength="256" data-i18n-placeholder="fileLogsSearchPlaceholder"
|
||||
placeholder="IP, path, error, request id..." /></label>
|
||||
<label><span data-i18n="fileLogsEvent">Event</span><select id="file-log-event">
|
||||
<option data-i18n="all" value="all">All</option>
|
||||
<option value="request">request</option>
|
||||
<option value="protocol_error">protocol_error</option>
|
||||
<option value="error">error</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="status">Status</span><select id="file-log-status">
|
||||
<option data-i18n="all" value="all">All</option>
|
||||
<option value="1xx">1xx</option>
|
||||
<option value="2xx">2xx</option>
|
||||
<option value="3xx">3xx</option>
|
||||
<option value="4xx">4xx</option>
|
||||
<option value="5xx">5xx</option>
|
||||
</select></label>
|
||||
<label><span data-i18n="clientIp">Client IP</span><input id="file-log-client"
|
||||
maxlength="128" placeholder="192.0.2.10" /></label>
|
||||
<label><span data-i18n="vhost">Vhost</span><input id="file-log-vhost"
|
||||
maxlength="256" /></label>
|
||||
<label><span data-i18n="fileLogsPageSize">Page size</span><select id="file-log-limit">
|
||||
maxlength="512" data-i18n-placeholder="fileLogsSearchPlaceholder"
|
||||
placeholder="req: GET AND !country: Poland" type="search" /></label>
|
||||
<label class="file-log-limit"><span data-i18n="fileLogsPageSize">Page size</span><select
|
||||
id="file-log-limit">
|
||||
<option>50</option>
|
||||
<option selected>100</option>
|
||||
<option>250</option>
|
||||
<option>500</option>
|
||||
</select></label>
|
||||
<button class="primary" data-i18n="applyFilters" type="submit">Apply filters</button>
|
||||
</form>
|
||||
<div class="file-logs-table-toolbar">
|
||||
<p class="fine" id="file-log-summary" role="status"></p>
|
||||
<details class="log-column-picker file-log-column-picker">
|
||||
<summary data-i18n="logColumns">Columns</summary>
|
||||
<div class="log-column-options" id="file-log-column-options"></div>
|
||||
</details>
|
||||
</form>
|
||||
<details class="file-log-inventory-details">
|
||||
<summary data-i18n="fileLogsSourcesRead">Configured files</summary>
|
||||
<p class="fine file-log-inventory" id="file-log-inventory"></p>
|
||||
</details>
|
||||
<p class="fine" id="file-log-summary" role="status"></p>
|
||||
</div>
|
||||
<div class="table-scroll file-logs-table">
|
||||
<table>
|
||||
<thead id="file-logs-table-head">
|
||||
<tr>
|
||||
<th data-file-log-column="time" data-i18n="time">Time</th>
|
||||
<th data-file-log-column="event" data-i18n="fileLogsEvent">Event</th>
|
||||
<th data-file-log-column="status" data-i18n="status">Status</th>
|
||||
<th data-file-log-column="client_ip" data-i18n="clientIp">Client IP</th>
|
||||
<th data-file-log-column="vhost" data-i18n="vhost">Vhost</th>
|
||||
<th data-file-log-column="transport" data-i18n="logTransport" hidden>Transport</th>
|
||||
<th data-file-log-column="matched_by" data-i18n="fileLogsMatchedBy" hidden>Matched by</th>
|
||||
<th data-file-log-column="sni" data-i18n="fileLogsSni" hidden>SNI</th>
|
||||
<th data-file-log-column="authority" data-i18n="fileLogsAuthority" hidden>Authority</th>
|
||||
<th data-file-log-column="record" data-i18n="fileLogsRecord">Request / reason</th>
|
||||
<th data-file-log-column="source" data-i18n="fileLogsSource">Source</th>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
</thead>
|
||||
<tbody id="file-log-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="empty" data-i18n="fileLogsEmpty" id="file-logs-empty">No records match the current
|
||||
filters.</p>
|
||||
search.</p>
|
||||
<div class="file-logs-pager"><button class="quiet" data-i18n="previous" disabled
|
||||
id="file-logs-prev" type="button">Previous</button><button class="quiet"
|
||||
data-i18n="next" disabled id="file-logs-next" type="button">Next</button></div>
|
||||
@@ -1749,7 +1735,7 @@
|
||||
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
||||
</select>
|
||||
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.1"
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.2"
|
||||
spellcheck="false" />
|
||||
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
||||
<div class="switch-grid">
|
||||
@@ -2548,7 +2534,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.26.1</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.26.2</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
@@ -2658,7 +2644,7 @@
|
||||
</div>
|
||||
<article class="card audit-dialog-card">
|
||||
<div class="field-grid tunnel-controls">
|
||||
<div><label data-i18n="tunnelKillReason" for="tunnel-kill-reason">Close reason</label><input
|
||||
<div><label data-i18n="tunnelKillReason" for="tunnel-kill-reason">Kill reason</label><input
|
||||
id="tunnel-kill-reason" maxlength="256" data-i18n-placeholder="tunnelKillReasonPlaceholder"
|
||||
placeholder="Administrative close" /></div>
|
||||
</div>
|
||||
@@ -3259,7 +3245,7 @@ max_execution_time = 30</code></pre>
|
||||
<article class="card about-card">
|
||||
<div class="about-product">
|
||||
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.26.1</strong></div>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.26.2</strong></div>
|
||||
</div>
|
||||
<dl class="about-meta">
|
||||
<div>
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
render();
|
||||
}
|
||||
syncColumnPicker(); syncHeaders();
|
||||
$("logs-open").addEventListener("click", () => open()); $("logs-close").addEventListener("click", () => dialog.close());
|
||||
$("logs-open").addEventListener("click", () => { $("logs-menu").open = false; open(); }); $("logs-close").addEventListener("click", () => dialog.close());
|
||||
$("logs-load-history").addEventListener("click", async () => {
|
||||
const button = $("logs-load-history"); button.disabled = true;
|
||||
try {
|
||||
|
||||
+186
-2
@@ -8240,7 +8240,7 @@ button.config-history-day.is-selected {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-logs-table tbody tr.selected > td {
|
||||
.file-logs-table tbody tr.selected>td {
|
||||
background: var(--panel2);
|
||||
box-shadow: inset 3px 0 0 var(--accent);
|
||||
}
|
||||
@@ -8286,7 +8286,7 @@ button.config-history-day.is-selected {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-log-inventory-details > summary {
|
||||
.file-log-inventory-details>summary {
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
@@ -8316,4 +8316,188 @@ button.config-history-day.is-selected {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.top-log-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top-log-menu>summary,
|
||||
.file-log-search-help>summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.top-log-menu>summary::-webkit-details-marker,
|
||||
.file-log-search-help>summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-log-menu[open]>summary,
|
||||
.file-log-search-help[open]>summary {
|
||||
color: var(--text);
|
||||
border-color: var(--border-hover);
|
||||
}
|
||||
|
||||
.top-log-menu-items {
|
||||
position: absolute;
|
||||
z-index: 120;
|
||||
top: calc(100% + 6px);
|
||||
inset-inline-end: 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 170px;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
box-shadow: 0 12px 28px var(--shadow-strong);
|
||||
}
|
||||
|
||||
.top-log-menu-items .quiet {
|
||||
width: 100%;
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-logs-heading-actions,
|
||||
.file-logs-table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-log-search-help {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-log-search-help-popover {
|
||||
position: absolute;
|
||||
z-index: 90;
|
||||
top: calc(100% + 6px);
|
||||
inset-inline-end: 0;
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
width: min(420px, calc(100vw - 36px));
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel2);
|
||||
box-shadow: 0 12px 28px var(--shadow-strong);
|
||||
}
|
||||
|
||||
.file-log-search-help-popover p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.file-log-search-help-popover code {
|
||||
display: block;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.file-logs-filter {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(180px, 260px) minmax(260px, 1fr) minmax(110px, 150px);
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.file-logs-filter .file-log-search {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.file-logs-filter input,
|
||||
.file-logs-filter select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-logs-table-toolbar {
|
||||
justify-content: space-between;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.file-logs-table-toolbar .fine {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.file-log-column-picker {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.file-log-column-picker .log-column-options {
|
||||
width: min(360px, calc(100vw - 24px));
|
||||
max-height: 420px;
|
||||
overflow: auto;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.file-log-column-picker .log-column-option span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-log-field-grid {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
margin-top: 8px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.file-log-field {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(140px, .5fr) minmax(0, 1.5fr);
|
||||
gap: 12px;
|
||||
padding: 6px 8px;
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.file-log-field code {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.file-log-field span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.file-log-raw-json {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.file-log-raw-json>summary {
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.backend-sort-direction {
|
||||
min-width: 36px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
|
||||
.file-logs-heading-actions,
|
||||
.file-logs-table-toolbar {
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file-logs-filter {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.file-log-limit {
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.file-log-field {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.file-log-column-picker .log-column-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
function syncPoolSortDirection() {
|
||||
const control = $("pool-sort-direction"); if (!control) return;
|
||||
control.textContent = poolTablePrefs.descending ? "Z-A" : "A-Z";
|
||||
control.textContent = poolTablePrefs.descending ? "↓" : "↑";
|
||||
control.setAttribute("aria-label", t(poolTablePrefs.descending ? "backendSortDescending" : "backendSortAscending"));
|
||||
}
|
||||
function applyPoolTableView() {
|
||||
|
||||
Reference in new issue
Block a user