flams_system/
logging.rs

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