v0.24.10
This commit is contained in:
1 parent
d3f0f060bf
commit
2e4aa4df4d
33 files changed
+3704
-3566
No files matched your search
+103
-2
@@ -1447,6 +1447,13 @@ struct ResponseSection {
|
||||
compression: crate::compression::Settings,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LoggingSection {
|
||||
logging: crate::logging::Settings,
|
||||
error_log: crate::logging::ErrorLogSettings,
|
||||
}
|
||||
|
||||
fn invalid_section(section: &str, error: serde_json::Error) -> ApiError {
|
||||
ApiError(
|
||||
StatusCode::BAD_REQUEST,
|
||||
@@ -1502,8 +1509,17 @@ async fn update_section(
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
}
|
||||
"logging" => {
|
||||
config.logging =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?
|
||||
if value.get("logging").is_some() || value.get("error_log").is_some() {
|
||||
let part: LoggingSection =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?;
|
||||
config.logging = part.logging;
|
||||
config.error_log = part.error_log;
|
||||
} else {
|
||||
// Backward compatibility for clients that still send only the
|
||||
// historical access-logging object to /api/config/logging.
|
||||
config.logging =
|
||||
serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))?;
|
||||
}
|
||||
}
|
||||
"forwarding" => {
|
||||
config.forwarding =
|
||||
@@ -2387,6 +2403,91 @@ mod tests {
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn logging_section_persists_error_log_settings_atomically() {
|
||||
let (router, store, dir, secret, csrf) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
let logging = Config::default().logging;
|
||||
let mut error_log = Config::default().error_log;
|
||||
error_log.enabled = true;
|
||||
error_log.path = "/tmp/rustproxy-error.log".into();
|
||||
|
||||
let response = router
|
||||
.clone()
|
||||
.oneshot(
|
||||
request("/api/config/logging")
|
||||
.method("PUT")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(
|
||||
serde_json::to_vec(&json!({
|
||||
"expected_revision": 0,
|
||||
"value": {
|
||||
"logging": logging,
|
||||
"error_log": error_log,
|
||||
},
|
||||
}))
|
||||
.unwrap(),
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let updated = json_body(response).await;
|
||||
assert_eq!(updated["revision"], 1);
|
||||
assert_eq!(updated["config"]["error_log"]["enabled"], true);
|
||||
assert_eq!(updated["config"]["error_log"]["path"], "/tmp/rustproxy-error.log");
|
||||
|
||||
let configuration = router
|
||||
.oneshot(
|
||||
request("/api/config")
|
||||
.header("cookie", &cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let persisted = json_body(configuration).await;
|
||||
assert_eq!(persisted["config"]["error_log"]["enabled"], true);
|
||||
assert_eq!(persisted["config"]["error_log"]["path"], "/tmp/rustproxy-error.log");
|
||||
drop(router);
|
||||
store.close().await;
|
||||
let reopened = Store::open(&dir.path().join("db"), &Config::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let persisted = reopened.load().await.unwrap();
|
||||
assert_eq!(persisted.revision, 1);
|
||||
assert!(persisted.config.error_log.enabled);
|
||||
assert_eq!(persisted.config.error_log.path, "/tmp/rustproxy-error.log");
|
||||
reopened.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn error_log_is_not_a_standalone_configuration_section() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||
let response = router
|
||||
.oneshot(
|
||||
request("/api/config/error_log")
|
||||
.method("PUT")
|
||||
.header("content-type", "application/json")
|
||||
.header("cookie", &cookie)
|
||||
.header("origin", "http://127.0.0.1:9090")
|
||||
.header("x-rustproxy-request", "1")
|
||||
.header("x-csrf-token", &csrf)
|
||||
.body(Body::from(r#"{"expected_revision":0,"value":{}}"#))
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
store.close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_pages_section_accepts_the_standard_value_payload() {
|
||||
let (router, store, _dir, secret, csrf) = fixture().await;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.24.9",
|
||||
"version": "0.24.10",
|
||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
||||
},
|
||||
"servers": [
|
||||
|
||||
+11
-8
@@ -778,7 +778,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.24.9";
|
||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.24.10";
|
||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
@@ -821,23 +821,23 @@ function renderSettings() {
|
||||
updateTransformResourceSizing();
|
||||
}
|
||||
if (!settingsDirty.has("logging-form")) {
|
||||
const l = config.logging || {}, file = l.file || {}, logstash = l.logstash || {}, loki = l.loki || {}, format = l.format || {}, errorLog = config.error_log || {};
|
||||
const l = config.logging || {}, file = l.file || {}, logstash = l.logstash || {}, loki = l.loki || {}, format = l.format || {};
|
||||
$("log-file-enabled").checked = !!file.enabled; $("log-file-per-vhost").checked = !!file.per_vhost; $("log-file-path").value = file.path || "/var/log/rustproxy/access.jsonl"; $("log-file-directory").value = file.directory || "/var/log/rustproxy/vhosts"; syncLoggingFileMode();
|
||||
$("log-logstash-enabled").checked = !!logstash.enabled; $("log-logstash-address").value = logstash.address || "127.0.0.1:5044";
|
||||
$("log-loki-enabled").checked = !!loki.enabled; $("log-loki-url").value = loki.url || "http://127.0.0.1:3100/loki/api/v1/push"; $("log-loki-token").value = loki.bearer_token_env || "";
|
||||
$("log-loki-labels").value = JSON.stringify(loki.labels || { job: "rustproxy" }, null, 2); $("log-format-fields").value = JSON.stringify(format.fields || {}, null, 2); $("log-format-static").value = JSON.stringify(format.static_fields || {}, null, 2);
|
||||
$("log-format-preset").value = detectLoggingPreset(format.fields || {});
|
||||
}
|
||||
if (!settingsDirty.has("error-log-form")) {
|
||||
const errorLog = config.error_log || {};
|
||||
$("error-log-enabled").checked = !!errorLog.enabled;
|
||||
$("error-log-path").value = errorLog.path || "/var/log/rustproxy/error.log";
|
||||
$("error-log-format").value = ["JSON", "RAW"].includes(errorLog.format) ? errorLog.format : "JSON";
|
||||
syncErrorLogMode();
|
||||
}
|
||||
const sink = latestStatus?.logging?.sinks, errorSink = latestStatus?.logging?.error_file;
|
||||
if (sink) {
|
||||
let status = `${t("loggingQueue")}: ${fmt(sink.queue_dropped)} ${t("loggingDropped")} / ${fmt(sink.sent)} ${t("loggingSent")} / ${fmt(sink.errors)} ${t("errors")} · ${t("loggingCapacity")}: ${fmt(sink.queue_capacity)}`;
|
||||
if (errorSink) status += ` · ${t("errorLogTitle")}: ${fmt(errorSink.queue_dropped)} ${t("loggingDropped")} / ${fmt(errorSink.written)} ${t("loggingSent")} / ${fmt(errorSink.errors)} ${t("errors")}`;
|
||||
$("logging-status").textContent = status;
|
||||
}
|
||||
$("logging-status").textContent = sink ? `${t("loggingQueue")}: ${fmt(sink.queue_dropped)} ${t("loggingDropped")} / ${fmt(sink.sent)} ${t("loggingSent")} / ${fmt(sink.errors)} ${t("errors")} · ${t("loggingCapacity")}: ${fmt(sink.queue_capacity)}` : "";
|
||||
$("error-log-status").textContent = errorSink ? `${t("loggingQueue")}: ${fmt(errorSink.queue_dropped)} ${t("loggingDropped")} / ${fmt(errorSink.written)} ${t("loggingSent")} / ${fmt(errorSink.errors)} ${t("errors")} · ${t("loggingCapacity")}: ${fmt(errorSink.queue_capacity)}` : "";
|
||||
if (!settingsDirty.has("forwarding-form")) { $("trusted-proxies").value = (config.forwarding?.trusted_proxies || []).join("\n"); $("trusted-proto").checked = !!config.forwarding?.trust_forwarded_proto; }
|
||||
if (!settingsDirty.has("headers-form")) { for (const direction of ["request", "response"]) renderHeaderRules(direction, config.headers?.[direction] || []); }
|
||||
}
|
||||
@@ -1120,6 +1120,7 @@ function applyLoggingPreset(name) {
|
||||
$("log-format-fields").value = JSON.stringify(LOGGING_PRESETS[name], null, 2);
|
||||
setSettingsDirty("logging-form");
|
||||
}
|
||||
const selectLoggingSection = next => ({ logging: next.logging, error_log: next.error_log });
|
||||
bindSettingsForm("logging-form", next => {
|
||||
const parseObject = id => parseJsonObjectValue($(id).value, $(id));
|
||||
next.logging = {
|
||||
@@ -1128,12 +1129,14 @@ bindSettingsForm("logging-form", next => {
|
||||
loki: { enabled: $("log-loki-enabled").checked, url: $("log-loki-url").value.trim(), bearer_token_env: $("log-loki-token").value.trim() || null, labels: parseObject("log-loki-labels") },
|
||||
format: { fields: parseObject("log-format-fields"), static_fields: parseObject("log-format-static") }
|
||||
};
|
||||
}, "logging", selectLoggingSection);
|
||||
bindSettingsForm("error-log-form", next => {
|
||||
next.error_log = {
|
||||
enabled: $("error-log-enabled").checked,
|
||||
path: $("error-log-path").value.trim(),
|
||||
format: $("error-log-format").value
|
||||
};
|
||||
}, "logging");
|
||||
}, "logging", selectLoggingSection);
|
||||
bindSettingsForm("forwarding-form", next => { next.forwarding = { trusted_proxies: $("trusted-proxies").value.split(/[\n,]/).map(s => s.trim()).filter(Boolean), trust_forwarded_proto: $("trusted-proto").checked }; }, "forwarding");
|
||||
bindSettingsForm("headers-form", next => { next.headers = { request: collectHeaderRules("request"), response: collectHeaderRules("response") }; }, "headers");
|
||||
$("log-format-preset").addEventListener("change", event => applyLoggingPreset(event.target.value));
|
||||
|
||||
+32
-21
@@ -479,17 +479,22 @@
|
||||
by interface</span><b id="network-interface-chart-value">-</b></div>
|
||||
<div class="network-interface-charts" id="network-interface-charts"></div>
|
||||
</div>
|
||||
<details class="mini-live-chart mini-live-chart-collapsible">
|
||||
<summary class="mini-live-chart-head"><span
|
||||
data-i18n="responseLatencyHistory">Response time history</span><b
|
||||
id="latency-chart-value">-</b></summary>
|
||||
<div class="mini-live-chart">
|
||||
<div class="mini-live-chart-head"><span
|
||||
data-i18n="responseLatencyHistory">Response time history</span><span
|
||||
class="mini-live-chart-actions"><details class="metric-info">
|
||||
<summary data-i18n-aria="responseLatencyInfo" aria-label="Response time information">i</summary>
|
||||
<div class="metric-info-popover">
|
||||
<p class="fine" data-i18n="responseLatencyHint">The value is the cumulative mean duration of completed HTTP exchanges since process start. The chart shows successive samples of that mean; it is not a percentile or the latency of one request.</p>
|
||||
</div>
|
||||
</details><b id="latency-chart-value">-</b></span></div>
|
||||
<svg aria-label="Mean response latency history"
|
||||
data-i18n-aria="responseLatencyHistory" role="img" viewbox="0 0 300 60">
|
||||
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
|
||||
<polyline class="diagnostic-line" id="latency-sparkline" points="0,55 300,55">
|
||||
</polyline>
|
||||
</svg>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -1391,7 +1396,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.24.9"
|
||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.24.10"
|
||||
spellcheck="false" />
|
||||
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
||||
<div class="switch-grid">
|
||||
@@ -1798,20 +1803,6 @@
|
||||
<label><span data-i18n="loggingLabels">Labels (JSON object)</span><textarea
|
||||
id="log-loki-labels" rows="4" spellcheck="false"></textarea></label>
|
||||
</article>
|
||||
<article class="card">
|
||||
<h3 class="subheading" data-i18n="errorLogTitle">Error file</h3>
|
||||
<label class="check"><input id="error-log-enabled" type="checkbox" /><span
|
||||
data-i18n="enabled">Enabled</span></label>
|
||||
<label><span data-i18n="errorLogPath">Error log path</span><input
|
||||
id="error-log-path" maxlength="1024"
|
||||
placeholder="/var/log/rustproxy/error.log" /></label>
|
||||
<label><span data-i18n="errorLogFormat">Format</span><select
|
||||
id="error-log-format">
|
||||
<option value="JSON">JSON</option>
|
||||
<option value="RAW">RAW</option>
|
||||
</select></label>
|
||||
<p class="fine" data-i18n="errorLogHelp">Only ERROR events are written. Rotation is handled by system logrotate.</p>
|
||||
</article>
|
||||
</div>
|
||||
<h3 class="subheading" data-i18n="loggingFormat">Configurable JSON format</h3>
|
||||
<p class="fine" data-i18n="loggingFormatHelp">Map output names to source fields. Leave
|
||||
@@ -1849,6 +1840,26 @@
|
||||
type="submit">Validate, save
|
||||
and apply</button></div>
|
||||
</form>
|
||||
<form class="card edit-form" id="error-log-form">
|
||||
<div class="card-heading">
|
||||
<h2 data-i18n="errorLogTitle">Error file</h2><span class="pill live" data-i18n="hotApply">LIVE APPLY</span>
|
||||
</div>
|
||||
<label class="check"><input id="error-log-enabled" type="checkbox" /><span
|
||||
data-i18n="enabled">Enabled</span></label>
|
||||
<div class="field-grid">
|
||||
<label><span data-i18n="errorLogPath">Error log path</span><input
|
||||
id="error-log-path" maxlength="1024"
|
||||
placeholder="/var/log/rustproxy/error.log" /></label>
|
||||
<label><span data-i18n="errorLogFormat">Format</span><select
|
||||
id="error-log-format">
|
||||
<option value="JSON">JSON</option>
|
||||
<option value="RAW">RAW</option>
|
||||
</select></label>
|
||||
</div>
|
||||
<p class="fine" id="error-log-status" role="status"></p>
|
||||
<div class="form-actions"><button class="primary" data-i18n="apply"
|
||||
type="submit">Validate, save and apply</button></div>
|
||||
</form>
|
||||
</div>
|
||||
<div aria-labelledby="settings-tab-prometheus" class="settings-page"
|
||||
data-settings-page="prometheus" hidden="" id="settings-page-prometheus" role="tabpanel">
|
||||
@@ -2112,7 +2123,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.24.9</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.24.10</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
+12
-29
@@ -1758,6 +1758,14 @@ progress {
|
||||
text-align: right
|
||||
}
|
||||
|
||||
.mini-live-chart-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 7px;
|
||||
min-width: 0
|
||||
}
|
||||
|
||||
.mini-live-chart svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -3773,6 +3781,10 @@ body[data-density="dense"] .stat strong {
|
||||
max-width: none
|
||||
}
|
||||
|
||||
.settings-page>.edit-form + .edit-form {
|
||||
margin-top: var(--ui-gap)
|
||||
}
|
||||
|
||||
.settings-page>.card {
|
||||
padding: var(--ui-card)
|
||||
}
|
||||
@@ -6359,31 +6371,6 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
}
|
||||
|
||||
.mini-live-chart-collapsible>summary {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none
|
||||
}
|
||||
|
||||
.mini-live-chart-collapsible>summary::-webkit-details-marker {
|
||||
display: none
|
||||
}
|
||||
|
||||
.mini-live-chart-collapsible>summary::before {
|
||||
content: "▸";
|
||||
flex: 0 0 auto;
|
||||
color: var(--muted);
|
||||
margin-right: 1px
|
||||
}
|
||||
|
||||
.mini-live-chart-collapsible[open]>summary::before {
|
||||
content: "▾"
|
||||
}
|
||||
|
||||
.mini-live-chart-collapsible>summary>span {
|
||||
flex: 1 1 auto
|
||||
}
|
||||
|
||||
.network-interface-charts {
|
||||
gap: 8px;
|
||||
padding-right: 0
|
||||
@@ -6552,10 +6539,6 @@ body[data-density="compact"] .page-footer {
|
||||
}
|
||||
}
|
||||
|
||||
.mini-live-chart-collapsible:not([open])>summary {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
.docker-route-grid {
|
||||
grid-template-columns: minmax(190px, 1fr) minmax(250px, 1.35fr) minmax(210px, 1fr) minmax(120px, .65fr)
|
||||
}
|
||||
|
||||
@@ -600,7 +600,7 @@
|
||||
authMode.addEventListener("change", syncAuth); syncAuth();
|
||||
g = group("vhResponsePolicy"); const server = value.server_header, diagnostics = value.diagnostic_headers;
|
||||
const serverMode = field(g, "server-mode", "serverHeader", server?.mode || "inherit", "select", { values: [["inherit", "vhInherit"], ["preserve", "serverPreserve"], ["hide", "serverHide"], ["custom", "serverCustom"]] });
|
||||
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy/v0.24.9", "text", { maxLength: 256 });
|
||||
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy/v0.24.10", "text", { maxLength: 256 });
|
||||
const syncServer = () => { serverValue.disabled = serverMode.value !== "custom"; serverValue.required = !serverValue.disabled; }; serverMode.addEventListener("change", syncServer); syncServer();
|
||||
const inheritDiagnostics = field(g, "diag-inherit", "diagInherit", !diagnostics, "checkbox"), diagInputs = [];
|
||||
for (const [name, key] of [["request_id", "diagRequestId"]]) diagInputs.push(field(g, `diag-${name}`, key, diagnostics?.[name] ?? envelope?.config.diagnostic_headers?.[name], "checkbox"));
|
||||
|
||||
+2
-2
@@ -41,7 +41,7 @@ use tracing_subscriber::prelude::*;
|
||||
#[command(
|
||||
name = "rust-proxy",
|
||||
version,
|
||||
about = "RustProxy - independent Rust reverse proxy 0.24.9"
|
||||
about = "RustProxy - independent Rust reverse proxy 0.24.10"
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
@@ -1515,7 +1515,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
|
||||
}
|
||||
});
|
||||
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
|
||||
println!("RustProxy live logs 0.24.9 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
println!("RustProxy live logs 0.24.10 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
if interactive { print!("> "); io::stdout().flush()?; }
|
||||
let mut filters = LiveLogFilters::default();
|
||||
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
|
||||
|
||||
Reference in new issue
Block a user