Streaming log page with colors
Using the ansi-to-html crate.
This commit is contained in:
parent
6713a7a440
commit
786f3a194f
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -31,6 +31,16 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ansi-to-html"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7bd918cc0ff933f0e6cf48a8f74584818ea43e07d1fba1f9251bb3df2a37ca2"
|
||||||
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.71"
|
version = "1.0.71"
|
||||||
@ -252,6 +262,7 @@ dependencies = [
|
|||||||
name = "crawlnicle"
|
name = "crawlnicle"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ansi-to-html",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argh",
|
"argh",
|
||||||
"axum",
|
"axum",
|
||||||
|
@ -11,6 +11,7 @@ path = "src/lib.rs"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ansi-to-html = "0.1"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
argh = "0.1"
|
argh = "0.1"
|
||||||
axum = "0.6"
|
axum = "0.6"
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
use std::str::from_utf8;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use ansi_to_html::convert_escaped;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::{
|
use axum::response::{
|
||||||
sse::{Event, Sse},
|
sse::{Event, Sse},
|
||||||
Response,
|
Response,
|
||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use maud::html;
|
use maud::{html, PreEscaped};
|
||||||
use tokio::sync::watch::Receiver;
|
use tokio::sync::watch::Receiver;
|
||||||
use tokio_stream::wrappers::WatchStream;
|
use tokio_stream::wrappers::WatchStream;
|
||||||
use tokio_stream::Stream;
|
use tokio_stream::Stream;
|
||||||
@ -21,7 +23,7 @@ pub async fn get(layout: Layout) -> Result<Response> {
|
|||||||
let mem_buf = MEM_LOG.lock().unwrap();
|
let mem_buf = MEM_LOG.lock().unwrap();
|
||||||
Ok(layout.render(html! {
|
Ok(layout.render(html! {
|
||||||
turbo-stream-source src="/log/stream" {}
|
turbo-stream-source src="/log/stream" {}
|
||||||
pre id="log" { (std::str::from_utf8(mem_buf.as_slices().0).unwrap()) }
|
pre id="log" { (PreEscaped(convert_escaped(from_utf8(mem_buf.as_slices().0).unwrap()).unwrap())) }
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,13 +32,16 @@ pub async fn stream(
|
|||||||
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
||||||
let log_stream = WatchStream::new(log_receiver);
|
let log_stream = WatchStream::new(log_receiver);
|
||||||
let log_stream = log_stream.map(|line| {
|
let log_stream = log_stream.map(|line| {
|
||||||
Ok(Event::default().data(html! {
|
Ok(Event::default().data(
|
||||||
turbo-stream action="append" target="log" {
|
html! {
|
||||||
template {
|
turbo-stream action="append" target="log" {
|
||||||
(std::str::from_utf8(&line).unwrap())
|
template {
|
||||||
|
(PreEscaped(convert_escaped(from_utf8(&line).unwrap()).unwrap()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.into_string()))
|
.into_string(),
|
||||||
|
))
|
||||||
});
|
});
|
||||||
Sse::new(log_stream).keep_alive(
|
Sse::new(log_stream).keep_alive(
|
||||||
axum::response::sse::KeepAlive::new()
|
axum::response::sse::KeepAlive::new()
|
||||||
|
25
src/log.rs
25
src/log.rs
@ -1,5 +1,5 @@
|
|||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::{io::Write, collections::VecDeque};
|
use std::{collections::VecDeque, io::Write};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
@ -7,7 +7,7 @@ use once_cell::sync::Lazy;
|
|||||||
use tokio::sync::watch::Sender;
|
use tokio::sync::watch::Sender;
|
||||||
use tracing_appender::non_blocking::WorkerGuard;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
use tracing_subscriber::{fmt::format, EnvFilter};
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
|
||||||
@ -33,11 +33,7 @@ struct LimitedInMemoryBuffer {
|
|||||||
|
|
||||||
impl LimitedInMemoryBuffer {
|
impl LimitedInMemoryBuffer {
|
||||||
fn new(buf: &'static Mutex<VecDeque<u8>>, sender: Sender<Bytes>, max: usize) -> Self {
|
fn new(buf: &'static Mutex<VecDeque<u8>>, sender: Sender<Bytes>, max: usize) -> Self {
|
||||||
Self {
|
Self { buf, sender, max }
|
||||||
buf,
|
|
||||||
sender,
|
|
||||||
max,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Truncate the buffer to max bytes plus bytes before the closest newline starting from the
|
/// Truncate the buffer to max bytes plus bytes before the closest newline starting from the
|
||||||
@ -75,21 +71,18 @@ impl Write for LimitedInMemoryBuffer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_tracing(config: &Config, log_sender: Sender<Bytes>) -> Result<(WorkerGuard, WorkerGuard)> {
|
pub fn init_tracing(
|
||||||
|
config: &Config,
|
||||||
|
log_sender: Sender<Bytes>,
|
||||||
|
) -> Result<(WorkerGuard, WorkerGuard)> {
|
||||||
let fmt_layer = tracing_subscriber::fmt::layer();
|
let fmt_layer = tracing_subscriber::fmt::layer();
|
||||||
let filter_layer = EnvFilter::from_default_env();
|
let filter_layer = EnvFilter::from_default_env();
|
||||||
let file_appender = tracing_appender::rolling::hourly("./logs", "log");
|
let file_appender = tracing_appender::rolling::hourly("./logs", "log");
|
||||||
let (file_writer, file_writer_guard) = tracing_appender::non_blocking(file_appender);
|
let (file_writer, file_writer_guard) = tracing_appender::non_blocking(file_appender);
|
||||||
let mem_writer = LimitedInMemoryBuffer::new(&MEM_LOG, log_sender, config.max_mem_log_size);
|
let mem_writer = LimitedInMemoryBuffer::new(&MEM_LOG, log_sender, config.max_mem_log_size);
|
||||||
let (mem_writer, mem_writer_guard) = tracing_appender::non_blocking(mem_writer);
|
let (mem_writer, mem_writer_guard) = tracing_appender::non_blocking(mem_writer);
|
||||||
let file_writer_layer = tracing_subscriber::fmt::layer()
|
let file_writer_layer = tracing_subscriber::fmt::layer().with_writer(file_writer);
|
||||||
.with_writer(file_writer)
|
let mem_writer_layer = tracing_subscriber::fmt::layer().with_writer(mem_writer);
|
||||||
.with_ansi(false)
|
|
||||||
.fmt_fields(format::PrettyFields::new().with_ansi(false));
|
|
||||||
let mem_writer_layer = tracing_subscriber::fmt::layer()
|
|
||||||
.with_writer(mem_writer)
|
|
||||||
.with_ansi(false)
|
|
||||||
.fmt_fields(format::PrettyFields::new().with_ansi(false));
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(filter_layer)
|
.with(filter_layer)
|
||||||
.with(fmt_layer)
|
.with(fmt_layer)
|
||||||
|
@ -17,8 +17,8 @@ use tracing::debug;
|
|||||||
|
|
||||||
use lib::config::Config;
|
use lib::config::Config;
|
||||||
use lib::handlers;
|
use lib::handlers;
|
||||||
use lib::state::AppState;
|
|
||||||
use lib::log::init_tracing;
|
use lib::log::init_tracing;
|
||||||
|
use lib::state::AppState;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
@ -49,7 +49,11 @@ async fn main() -> Result<()> {
|
|||||||
.route("/entry/:id", get(handlers::entry::get))
|
.route("/entry/:id", get(handlers::entry::get))
|
||||||
.route("/log", get(handlers::log::get))
|
.route("/log", get(handlers::log::get))
|
||||||
.route("/log/stream", get(handlers::log::stream))
|
.route("/log/stream", get(handlers::log::stream))
|
||||||
.with_state(AppState { pool, config, log_receiver })
|
.with_state(AppState {
|
||||||
|
pool,
|
||||||
|
config,
|
||||||
|
log_receiver,
|
||||||
|
})
|
||||||
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()));
|
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()));
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
use maud::{html, Markup};
|
use maud::{html, Markup};
|
||||||
|
|
||||||
pub fn header() -> Markup {
|
pub fn header(title: &str) -> Markup {
|
||||||
html! {
|
html! {
|
||||||
header {
|
header {
|
||||||
nav {
|
nav {
|
||||||
h1 { a href="/" data-turbo-frame="main" { "crawlnicle" } }
|
h1 { a href="/" data-turbo-frame="main" { (title) } }
|
||||||
ul {
|
ul {
|
||||||
li { a href="/feeds" data-turbo-frame="main" { "feeds" } }
|
li { a href="/feeds" data-turbo-frame="main" { "feeds" } }
|
||||||
li { a href="/log" data-turbo-frame="main" { "log" } }
|
li { a href="/log" data-turbo-frame="main" { "log" } }
|
||||||
|
@ -38,13 +38,13 @@ impl Layout {
|
|||||||
html lang="en" {
|
html lang="en" {
|
||||||
head {
|
head {
|
||||||
meta charset="utf-8";
|
meta charset="utf-8";
|
||||||
title { "crawlnicle" }
|
title { (self.title) }
|
||||||
script type="module" {
|
script type="module" {
|
||||||
r#"import * as Turbo from 'https://cdn.skypack.dev/@hotwired/turbo';"#
|
r#"import * as Turbo from 'https://cdn.skypack.dev/@hotwired/turbo';"#
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
(header())
|
(header(&self.title))
|
||||||
turbo-frame id="main" data-turbo-action="advance" {
|
turbo-frame id="main" data-turbo-action="advance" {
|
||||||
(template)
|
(template)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user