From 8d22e7fa56cfbceb3c829c6f22dc99234fd20b8d Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 3 Feb 2025 17:05:17 +0100 Subject: live log view --- import/src/lib.rs | 7 ++++- server/src/routes/mod.rs | 3 +- server/src/routes/ui/admin/log.rs | 66 ++++++++++++++++++++++++++++++++------- web/script/log_stream.ts | 43 +++++++++++++++++++++++++ web/script/main.ts | 1 + 5 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 web/script/log_stream.ts diff --git a/import/src/lib.rs b/import/src/lib.rs index a4f3668..36014ea 100644 --- a/import/src/lib.rs +++ b/import/src/lib.rs @@ -15,7 +15,7 @@ use jellybase::{ CONF, SECRETS, }; use jellyclient::{Appearance, PeopleGroup, TmdbKind, TraktKind, Visibility}; -use log::warn; +use log::{info, warn}; use matroska::matroska_metadata; use rayon::iter::{ParallelBridge, ParallelIterator}; use std::{ @@ -178,18 +178,21 @@ fn import_file( let filename = path.file_name().unwrap().to_string_lossy(); match filename.as_ref() { "poster.jpeg" | "poster.webp" | "poster.png" => { + info!("import poster at {path:?}"); db.update_node_init(parent, |node| { node.poster = Some(AssetInner::Media(path.to_owned()).ser()); Ok(()) })?; } "backdrop.jpeg" | "backdrop.webp" | "backdrop.png" => { + info!("import backdrop at {path:?}"); db.update_node_init(parent, |node| { node.backdrop = Some(AssetInner::Media(path.to_owned()).ser()); Ok(()) })?; } "node.yaml" => { + info!("import node info at {path:?}"); let data = serde_yaml::from_str::(&read_to_string(path)?)?; db.update_node_init(parent, |node| { fn merge_option(a: &mut Option, b: Option) { @@ -211,6 +214,7 @@ fn import_file( })?; } "channel.info.json" => { + info!("import channel info.json at {path:?}"); let data = serde_json::from_reader::<_, YVideo>(BufReader::new(File::open(path)?))?; db.update_node_init(parent, |node| { node.kind = NodeKind::Channel; @@ -252,6 +256,7 @@ fn import_media_file( parent: NodeID, visibility: Visibility, ) -> Result<()> { + info!("media file {path:?}"); let Some(m) = (*matroska_metadata(path)?).to_owned() else { return Ok(()); }; diff --git a/server/src/routes/mod.rs b/server/src/routes/mod.rs index b7d63da..e0b955a 100644 --- a/server/src/routes/mod.rs +++ b/server/src/routes/mod.rs @@ -30,7 +30,7 @@ use ui::{ settings::{r_account_settings, r_account_settings_post}, }, admin::{ - log::r_admin_log, + log::{r_admin_log, r_admin_log_stream}, r_admin_dashboard, r_admin_delete_cache, r_admin_import, r_admin_invite, r_admin_remove_invite, r_admin_transcode_posters, r_admin_update_search, user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users}, @@ -135,6 +135,7 @@ pub fn build_rocket(database: Database, federation: Federation) -> Rocket r_admin_delete_cache, r_admin_transcode_posters, r_admin_log, + r_admin_log_stream, r_admin_import, r_admin_update_search, r_account_settings, diff --git a/server/src/routes/ui/admin/log.rs b/server/src/routes/ui/admin/log.rs index cc34dbc..f962138 100644 --- a/server/src/routes/ui/admin/log.rs +++ b/server/src/routes/ui/admin/log.rs @@ -13,12 +13,16 @@ use crate::{ }; use chrono::{DateTime, Utc}; use log::Level; +use markup::Render; use rocket::get; +use rocket_ws::{Message, Stream, WebSocket}; +use serde_json::json; use std::{ collections::VecDeque, fmt::Write, - sync::{LazyLock, RwLock}, + sync::{Arc, LazyLock, RwLock}, }; +use tokio::sync::broadcast; const MAX_LOG_LEN: usize = 4096; @@ -31,7 +35,11 @@ pub fn enable_logging() { pub struct Log { inner: env_logger::Logger, - log: RwLock<(VecDeque, VecDeque)>, + stream: ( + broadcast::Sender>, + broadcast::Sender>, + ), + log: RwLock<(VecDeque>, VecDeque>)>, } pub struct LogLine { @@ -41,14 +49,15 @@ pub struct LogLine { message: String, } -#[get("/admin/log?")] +#[get("/admin/log?", rank = 2)] pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult> { Ok(LayoutPage { title: "Log".into(), + class: Some("admin_log"), content: markup::new! { h1 { "Server Log" } a[href=uri!(r_admin_log(!warnonly))] { @if warnonly { "Show everything" } else { "Show only warnings" }} - code.log { + code.log[id="log"] { @let g = LOGGER.log.read().unwrap(); table { @for e in if warnonly { g.1.iter() } else { g.0.iter() } { tr[class=format!("level-{:?}", e.level).to_ascii_lowercase()] { @@ -63,6 +72,32 @@ pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult", rank = 1)] +pub fn r_admin_log_stream( + _session: AdminSession, + ws: WebSocket, + warnonly: bool, +) -> Stream!['static] { + let mut stream = if warnonly { + LOGGER.stream.1.subscribe() + } else { + LOGGER.stream.0.subscribe() + }; + Stream! { ws => + let _ = ws; + while let Ok(line) = stream.recv().await { + yield Message::Text(json!({ + "time": line.time, + "level_class": format!("level-{:?}", line.level).to_ascii_lowercase(), + "level_html": format_level_string(line.level), + "module": line.module, + "message": vt100_to_html(&line.message), + }).to_string()); + } + } +} + impl Default for Log { fn default() -> Self { Self { @@ -70,6 +105,10 @@ impl Default for Log { .filter_level(log::LevelFilter::Warn) .parse_env("LOG") .build(), + stream: ( + tokio::sync::broadcast::channel(1024).0, + tokio::sync::broadcast::channel(1024).0, + ), log: Default::default(), } } @@ -85,24 +124,22 @@ impl Log { } } fn do_log(&self, record: &log::Record) { - let mut w = self.log.write().unwrap(); let time = Utc::now(); - w.0.push_back(LogLine { + let line = Arc::new(LogLine { time, module: record.module_path_static(), level: record.level(), message: record.args().to_string(), }); + let mut w = self.log.write().unwrap(); + w.0.push_back(line.clone()); + let _ = self.stream.0.send(line.clone()); while w.0.len() > MAX_LOG_LEN { w.0.pop_front(); } if record.level() <= Level::Warn { - w.1.push_back(LogLine { - time, - module: record.module_path_static(), - level: record.level(), - message: record.args().to_string(), - }); + let _ = self.stream.1.send(line.clone()); + w.1.push_back(line); while w.1.len() > MAX_LOG_LEN { w.1.pop_front(); } @@ -150,6 +187,11 @@ fn format_level(level: Level) -> impl markup::Render { }; markup::new! { span[style=format!("color:{c}")] {@s} } } +fn format_level_string(level: Level) -> String { + let mut w = String::new(); + format_level(level).render(&mut w).unwrap(); + w +} #[derive(Default)] pub struct HtmlOut { diff --git a/web/script/log_stream.ts b/web/script/log_stream.ts new file mode 100644 index 0000000..5a6a3ce --- /dev/null +++ b/web/script/log_stream.ts @@ -0,0 +1,43 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2025 metamuffin +*/ +/// +globalThis.addEventListener("DOMContentLoaded", () => { + if (!document.body.classList.contains("admin_log")) return + const log = document.getElementById("log")! + + const warnonly = new URL(globalThis.location.href).searchParams.get("warnonly") == "true" + const ws = new WebSocket(`/admin/log?stream&warnonly=${warnonly}`) + ws.onopen = () => console.log("live log connected"); + ws.onclose = () => console.log("live log disconnected"); + ws.onerror = e => console.log(`live log ws error: ${e}`); + + ws.onmessage = msg => { + const line = JSON.parse(msg.data) + + const td_time = document.createElement("td") + td_time.classList.add("time") + td_time.textContent = line.time + + const td_level = document.createElement("td") + td_level.classList.add("level") + td_level.innerHTML = line.level_html + + const td_module = document.createElement("td") + td_module.classList.add("module") + td_module.textContent = line.module + + const td_message = document.createElement("td") + td_message.innerHTML = line.message + + const tr = document.createElement("tr"); + tr.classList.add(line.level_class) + tr.append(td_time, td_level, td_module, td_message) + + log.children[0].children[0].append(tr) + while (log.children[0].children[0].children.length > 1024) + log.children[0].children[0].children[0].remove() + } +}) diff --git a/web/script/main.ts b/web/script/main.ts index 6d335ee..d7a36cb 100644 --- a/web/script/main.ts +++ b/web/script/main.ts @@ -8,3 +8,4 @@ import "./player/mod.ts" import "./transition.ts" import "./backbutton.ts" import "./dangerbutton.ts" +import "./log_stream.ts" -- cgit v1.2.3-70-g09d2