tracing-live 0.0.3

Live http log viewer for the tracing crate
Documentation
use crossbeam_channel::{unbounded, Sender};
use serde_json::Value as JsonValue;
use std::net::SocketAddr;
use std::sync::{Arc, Mutex};
use std::thread;
use tiny_http::{Response, Server};
use tracing::Subscriber;
use tracing_subscriber::layer::Layer;
use tracing_subscriber::registry::LookupSpan;

pub struct Log {
    sender: Sender<JsonValue>,
}

impl Log {
    pub fn builder() -> LogBuilder { LogBuilder::new() }
}

pub struct LogBuilder {
    host: String,
}

impl LogBuilder {
    fn new() -> Self { LogBuilder { host: "127.0.0.1:8362".to_string() } }

    pub fn with_host(mut self, host: &str) -> Self {
        self.host = host.to_string();
        self
    }

    pub fn build(self) -> Log {
        let (sender, receiver) = unbounded();
        let addr: SocketAddr = self.host.parse().expect("Invalid host address");

        thread::spawn(move || {
            let server = Server::http(addr).expect("Failed to start server");
            let receiver = Arc::new(receiver);
            let cache = Arc::new(Mutex::new(Vec::new()));

            for request in server.incoming_requests() {
                let receiver = receiver.clone();
                let cache = cache.clone();
                thread::spawn(move || {
                    let mut cache = cache.lock().unwrap();
                    cache.extend(receiver.try_iter());
                    let logs = cache.clone();
                    // Here we're directly serializing the Vec<JsonValue>
                    let json = serde_json::to_string(&logs).unwrap();
                    let response = Response::from_string(json).with_header(tiny_http::Header::from_bytes(&b"Content-Type"[..], &b"application/json"[..]).unwrap());
                    request.respond(response).expect("Failed to send response");
                });
            }
        });

        Log { sender }
    }
}

impl<S> Layer<S> for Log
where
    S: Subscriber + for<'a> LookupSpan<'a>,
{
    fn on_event(&self, event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
        let mut visitor = JsonVisitor::default();
        event.record(&mut visitor);
        self.sender.send(visitor.fields).expect("Failed to send log entry");
    }
}

#[derive(Default)]
struct JsonVisitor {
    fields: JsonValue,
}

impl tracing::field::Visit for JsonVisitor {
    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { self.fields[field.name()] = JsonValue::String(format!("{:?}", value)); }

    fn record_str(&mut self, field: &tracing::field::Field, value: &str) { self.fields[field.name()] = JsonValue::String(value.to_string()); }

    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { self.fields[field.name()] = JsonValue::Number(value.into()); }

    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { self.fields[field.name()] = JsonValue::Number(value.into()); }

    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { self.fields[field.name()] = JsonValue::Bool(value); }
}

#[cfg(test)]
mod tests {
    use super::*;
    use reqwest::blocking::Client;
    use std::{thread, time::Duration};
    use tracing_subscriber::prelude::*;

    #[test]
    fn test_weblog() {
        let log_port = 8363;
        tracing_subscriber::registry().with(Log::builder().with_host(&format!("127.0.0.1:{}", log_port)).build()).init();

        thread::sleep(Duration::from_secs(1));

        tracing::info!(message = "Test log message 1", index = 1);
        tracing::warn!(message = "Test log message 2", index = 2);
        tracing::error!(message = "Test log message 3", index = 3);

        thread::sleep(Duration::from_millis(500));

        let client = Client::new();
        let response = client.get(&format!("http://127.0.0.1:{}", log_port)).send().expect("Failed to send request");

        assert!(response.status().is_success());

        let body: Vec<JsonValue> = response.json().expect("Failed to parse JSON");

        assert!(body.iter().any(|log| log["message"] == "Test log message 1" && log["index"] == 1));
        assert!(body.iter().any(|log| log["message"] == "Test log message 2" && log["index"] == 2));
        assert!(body.iter().any(|log| log["message"] == "Test log message 3" && log["index"] == 3));

        // Test caching
        tracing::info!(message = "Test log message 4", index = 4);
        thread::sleep(Duration::from_millis(500));

        let response = client.get(&format!("http://127.0.0.1:{}", log_port)).send().expect("Failed to send request");

        let body: Vec<JsonValue> = response.json().expect("Failed to parse JSON");

        assert!(body.iter().any(|log| log["message"] == "Test log message 1" && log["index"] == 1));
        assert!(body.iter().any(|log| log["message"] == "Test log message 2" && log["index"] == 2));
        assert!(body.iter().any(|log| log["message"] == "Test log message 3" && log["index"] == 3));
        assert!(body.iter().any(|log| log["message"] == "Test log message 4" && log["index"] == 4));
    }
}