aboutsummaryrefslogtreecommitdiff
path: root/server/src/routes/ui/admin
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/routes/ui/admin')
-rw-r--r--server/src/routes/ui/admin/log.rs66
1 files changed, 54 insertions, 12 deletions
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<LogLine>, VecDeque<LogLine>)>,
+ stream: (
+ broadcast::Sender<Arc<LogLine>>,
+ broadcast::Sender<Arc<LogLine>>,
+ ),
+ log: RwLock<(VecDeque<Arc<LogLine>>, VecDeque<Arc<LogLine>>)>,
}
pub struct LogLine {
@@ -41,14 +49,15 @@ pub struct LogLine {
message: String,
}
-#[get("/admin/log?<warnonly>")]
+#[get("/admin/log?<warnonly>", rank = 2)]
pub fn r_admin_log<'a>(_session: AdminSession, warnonly: bool) -> MyResult<DynLayoutPage<'a>> {
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<DynLa
..Default::default()
})
}
+
+#[get("/admin/log?stream&<warnonly>", 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 {