Streaming log page with colors

Using the ansi-to-html crate.
This commit is contained in:
Tyler Hallada 2023-06-06 21:14:29 -04:00
parent 6713a7a440
commit 786f3a194f
7 changed files with 50 additions and 36 deletions

11
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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(
html! {
turbo-stream action="append" target="log" { turbo-stream action="append" target="log" {
template { template {
(std::str::from_utf8(&line).unwrap()) (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()

View File

@ -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)

View File

@ -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)]

View File

@ -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" } }

View File

@ -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)
} }