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