flams_system/
logging.rs

1use std::{fmt::Display ,path::PathBuf};
2use flams_utils::{change_listener::{ChangeListener, ChangeSender}, logs::LogFileLine, time::Timestamp, triomphe::Arc, vecmap::VecMap};
3use tracing::span::Id;
4use tracing_subscriber::{layer::Context, Layer};
5
6#[derive(Clone,Debug)]
7pub struct LogStore(Arc<LogStoreI>);
8
9static LOG : std::sync::OnceLock<LogStore> = std::sync::OnceLock::new();
10
11impl LogStore {
12  ///#### Panics
13  pub fn initialize() {
14    use tracing_subscriber::layer::SubscriberExt;
15    if LOG.get().is_some() {
16      return
17    }
18    let logger = Self::new();
19    let level = if crate::settings::Settings::get().debug { tracing::Level::DEBUG } else { tracing::Level::INFO };
20    let subscriber = tracing_subscriber::registry()
21      .with(logger.with_filter(tracing::level_filters::LevelFilter::from(level)))
22      .with(tracing_error::ErrorLayer::default());
23    tracing::subscriber::set_global_default(subscriber).expect(
24      "Error initializing tracing subscriber"
25    );
26  }
27
28  #[allow(clippy::new_without_default)]
29  ///#### Panics
30  pub fn new() -> Self {
31    assert!(LOG.get().is_none(),"Logger already initialized");
32    let settings = crate::settings::Settings::get();
33    let logdir = &settings.log_dir;
34    let filename = chrono::Local::now().format("%Y-%m-%d-%H.%M.%S.log").to_string();
35    let path = logdir.join(&filename);
36    let ret = Self::new_i(/*guard,file_layer,*/path);
37    LOG.set(ret.clone()).expect("Error initializing logger");
38    ret
39  }
40}
41
42/// ### Panics
43pub fn logger() -> &'static LogStore {
44  LOG.get().expect("log should be initialized")
45}
46
47#[derive(Clone)]
48enum Msg {
49    Line(LogFileLine),
50    Kill
51}
52
53#[derive(Debug)]
54struct LogStoreI {
55  notifier:ChangeListener<LogFileLine>,
56  sender:crossbeam_channel::Sender<Msg>,
57  log_file:PathBuf,
58}
59
60impl Drop for LogStoreI {
61    fn drop(&mut self) {
62        let _ = self.sender.send(Msg::Kill);
63    }
64}
65
66impl LogStore {
67  #[allow(clippy::let_underscore_future)]
68  fn new_i<P:Into<PathBuf>>(log_file:P) -> Self {
69    let (sender,recv) = crossbeam_channel::unbounded();
70    let cs = ChangeSender::new(1024);
71      let store = Arc::new(LogStoreI {
72          notifier: cs.listener(),
73          sender,
74          log_file: log_file.into()
75      });
76      let file = store.log_file.clone();
77      let _ = tokio::task::spawn_blocking(move || {
78        if let Some(p) = file.parent() {
79            let _ = std::fs::create_dir_all(p);
80        };
81        let f = std::fs::File::create(file).expect("Failed to create log file");
82        let mut f = std::io::BufWriter::new(f);//std::fs::File::create_buffered(file).expect("Failed to create log file");
83        loop {
84          match recv.recv() {
85            Err(_) | Ok(Msg::Kill) => break,
86            Ok(Msg::Line(msg)) => {
87                let _ = serde_json::to_writer(&mut f, &msg);
88                let _ = std::io::Write::write(&mut f, b"\n");
89                cs.send(msg);
90            }
91          }
92        }
93      });
94      Self(store)
95  }
96  #[must_use]
97  pub fn listener(&self) -> ChangeListener<LogFileLine> {
98      self.0.notifier.clone()
99  }
100  #[must_use]
101  pub fn log_file(&self) -> &std::path::Path { &self.0.log_file }
102}
103
104impl<S: tracing::Subscriber> Layer<S> for LogStore {
105  fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) {
106    let mut visitor = StringVisitor::default();
107    event.record(&mut visitor);
108    let args = visitor.0;
109    let message = visitor.1;
110    let timestamp = Timestamp::now();
111    let parent = if event.is_root() { None }
112    else {
113        event.parent().map_or_else(
114            || ctx.current_span().id().map(tracing::Id::into_non_zero_u64),
115            |i| Some(i.into_non_zero_u64())
116        )
117    };
118    let target: Option<String> = {
119        let tg = event.metadata().target();
120        if tg.starts_with("flams") { None } else { Some(tg.into()) }
121    };
122    let msg = LogFileLine::Message {
123        message,
124        timestamp,
125        target,
126        level: (*event.metadata().level()).into(),
127        args,
128        span:parent,
129    };
130    let _ = self.0.sender.send(Msg::Line(msg));
131  }
132
133  fn on_new_span(&self, md: &tracing::span::Attributes<'_>, thisid: &Id, ctx: Context<'_, S>) {
134    let mut visitor = StringVisitor::default();
135    md.record(&mut visitor);
136    let args = visitor.0;
137    let name = md.metadata().name().to_string();
138    let target: Option<String> = {
139        let tg = md.metadata().target();
140        if tg.starts_with("flams") { None } else { Some(tg.into()) }
141    };
142    let parent = if md.is_root() { None }
143    else {
144        md.parent().map_or_else(
145            || ctx.current_span().id().map(tracing::Id::into_non_zero_u64),
146            |i| Some(i.into_non_zero_u64())
147        )
148    };
149    let id = thisid.into_non_zero_u64();
150    let level = (*md.metadata().level()).into();
151    let _ = self.0.sender.send(Msg::Line(LogFileLine::SpanOpen { name, id, timestamp:Timestamp::now(), target, level, args, parent }));
152  }
153  
154  fn on_close(&self, id: Id, _ctx: Context<'_, S>) {
155    let _ = self.0.sender.send(Msg::Line(LogFileLine::SpanClose { id: id.into_non_zero_u64(), timestamp:Timestamp::now() }));
156  }
157}
158
159#[derive(Default)]
160struct StringVisitor(VecMap<String, String>,String);
161impl Display for StringVisitor {
162  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163    use std::fmt::Write;
164      if self.0.is_empty() {return Ok(())}
165      f.write_char('{')?;
166      let mut had = false;
167      for (k,v) in self.0.iter() {
168          if had { f.write_str(", ")?;}
169          f.write_str(k)?;
170          f.write_char('=')?;
171          f.write_str(v)?;
172          had = true;
173      }
174      f.write_char('}')
175  }
176}
177
178impl tracing::field::Visit for StringVisitor {
179  fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
180    if field.name() == "message" { self.1 = format!("{value:?}");}
181    else {
182      self.0
183          .insert(field.name().to_string(), format!("{value:?}"));
184    }
185  } 
186}