1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
use crossbeam_channel::{unbounded, Sender};
use serde_json::json;
use std::net::SocketAddr;
use std::sync::Arc;
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<String>,
}

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

            for request in server.incoming_requests() {
                let receiver = receiver.clone();
                thread::spawn(move || {
                    let logs: Vec<String> = receiver.try_iter().collect();
                    let json = json!({ "logs": logs });
                    let response = Response::from_string(json.to_string()).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);
        let log_entry = serde_json::to_string(&visitor.fields).unwrap();
        self.sender.send(log_entry).expect("Failed to send log entry");
    }
}

#[derive(Default)]
struct JsonVisitor {
    fields: serde_json::Map<String, serde_json::Value>,
}

impl tracing::field::Visit for JsonVisitor {
    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { self.fields.insert(field.name().to_string(), json!(format!("{:?}", value))); }

    fn record_str(&mut self, field: &tracing::field::Field, value: &str) { self.fields.insert(field.name().to_string(), json!(value)); }

    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { self.fields.insert(field.name().to_string(), json!(value)); }

    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { self.fields.insert(field.name().to_string(), json!(value)); }

    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { self.fields.insert(field.name().to_string(), json!(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!("Test log message 1");
        tracing::warn!("Test log message 2");
        tracing::error!("Test log message 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: serde_json::Value = response.json().expect("Failed to parse JSON");
        let logs = body["logs"].as_array().expect("Logs should be an array");

        assert_eq!(logs.len(), 3);

        assert!(logs.iter().any(|log| log.to_string().contains("Test log message 1")));
        assert!(logs.iter().any(|log| log.to_string().contains("Test log message 2")));
        assert!(logs.iter().any(|log| log.to_string().contains("Test log message 3")));
    }
}