Add a log view, and Docker packaging
The Log button shows the running daemon live: feed scans, downloads, torrents and every HTTP request. It reads a ring buffer filled by a tracing layer rather than tailing a file, so it works under Docker where logs go to stdout. The access-log middleware skips /api/logs, or the panel's poll would log itself forever. Detached torrents could leave a row stuck in 'downloading' across a restart, where nothing would ever revisit it; those are requeued at startup. Dockerfile, entrypoint and compose: 114 MB runtime, config bound to 0.0.0.0 on first run since container loopback is unreachable, drops to PUID:PGID for Unraid, and a healthcheck that goes through the control socket so a wedged worker reads as unhealthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
48
src/web.rs
48
src/web.rs
@@ -72,9 +72,11 @@ pub fn router(state: WebState) -> Router {
|
||||
.route("/api/fetch", post(fetch_now))
|
||||
.route("/api/opml", get(export_opml).post(import_opml))
|
||||
.route("/api/settings", get(get_settings).patch(patch_settings))
|
||||
.route("/api/logs", get(logs))
|
||||
.route("/api/events", get(events))
|
||||
.route("/media/{id}", get(media))
|
||||
.layer(middleware::from_fn_with_state(state.clone(), auth))
|
||||
.layer(middleware::from_fn(access_log))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -790,3 +792,49 @@ async fn patch_settings(
|
||||
state.ctx.reload_cfg(&state.config_path)?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LogQuery {
|
||||
/// Highest seq the client already has; 0 means "give me the tail".
|
||||
#[serde(default)]
|
||||
after: u64,
|
||||
#[serde(default = "two_hundred")]
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
fn two_hundred() -> usize {
|
||||
200
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LogPage {
|
||||
lines: Vec<crate::logbuf::LogLine>,
|
||||
latest: u64,
|
||||
}
|
||||
|
||||
async fn logs(Query(q): Query<LogQuery>) -> Json<LogPage> {
|
||||
let (lines, latest) = crate::logbuf::since(q.after, q.limit.clamp(1, 2000));
|
||||
Json(LogPage { lines, latest })
|
||||
}
|
||||
|
||||
/// One line per HTTP request, so the web side shows up in the same log as the daemon.
|
||||
///
|
||||
/// The log view polls `/api/logs`, so logging that path would generate a line per poll
|
||||
/// forever -- a feed of nothing but its own requests.
|
||||
async fn access_log(req: Request, next: Next) -> Response {
|
||||
let path = req.uri().path().to_owned();
|
||||
let method = req.method().clone();
|
||||
let quiet = path.starts_with("/api/logs");
|
||||
let started = std::time::Instant::now();
|
||||
let resp = next.run(req).await;
|
||||
if !quiet {
|
||||
let ms = started.elapsed().as_millis();
|
||||
let status = resp.status().as_u16();
|
||||
if resp.status().is_success() || resp.status().is_redirection() {
|
||||
tracing::info!(target: "ipx::http", "{method} {path} -> {status} in {ms}ms");
|
||||
} else {
|
||||
tracing::warn!(target: "ipx::http", "{method} {path} -> {status} in {ms}ms");
|
||||
}
|
||||
}
|
||||
resp
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user