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 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 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(path);
73 LOG.set(ret.clone()).expect("Error initializing logger");
74 ret
75 }
76}
77
78pub 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); 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}