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 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 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(path);
37 LOG.set(ret.clone()).expect("Error initializing logger");
38 ret
39 }
40}
41
42pub 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);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}