1use bincode::config::Configuration;
2use bincode::enc::write::Writer;
3use bincode::enc::Encode;
4use bincode::enc::{Encoder, EncoderImpl};
5use bincode::error::EncodeError;
6use cu29_clock::RobotClock;
7use cu29_log::CuLogEntry;
8#[allow(unused_imports)]
9use cu29_log::CuLogLevel;
10use cu29_traits::{CuResult, WriteStream};
11use log::Log;
12
13#[cfg(debug_assertions)]
14use {cu29_log::format_logline, std::collections::HashMap, std::sync::RwLock};
15
16use std::fmt::{Debug, Formatter};
17use std::fs::File;
18use std::io::{BufWriter, Write};
19use std::path::PathBuf;
20use std::sync::{Mutex, OnceLock};
21
22#[derive(Debug)]
23struct DummyWriteStream;
24
25impl WriteStream<CuLogEntry> for DummyWriteStream {
26 fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
27 eprintln!("Pending logs got cut: {obj:?}");
28 Ok(())
29 }
30}
31type LogWriter = Box<dyn WriteStream<CuLogEntry>>;
32type WriterPair = (Mutex<LogWriter>, RobotClock);
33
34static WRITER: OnceLock<WriterPair> = OnceLock::new();
35
36#[cfg(debug_assertions)]
37pub static EXTRA_TEXT_LOGGER: RwLock<Option<Box<dyn Log + 'static>>> = RwLock::new(None);
38
39pub struct NullLog;
40impl Log for NullLog {
41 fn enabled(&self, _metadata: &log::Metadata) -> bool {
42 false
43 }
44
45 fn log(&self, _record: &log::Record) {}
46 fn flush(&self) {}
47}
48
49pub struct LoggerRuntime {}
51
52impl LoggerRuntime {
53 pub fn init(
56 clock: RobotClock,
57 destination: impl WriteStream<CuLogEntry> + 'static,
58 #[allow(unused_variables)] extra_text_logger: Option<impl Log + 'static>,
59 ) -> Self {
60 let runtime = LoggerRuntime {};
61
62 if let Some((writer, _)) = WRITER.get() {
65 let mut writer_guard = writer.lock().unwrap();
66 *writer_guard = Box::new(destination);
67 } else {
68 WRITER
69 .set((Mutex::new(Box::new(destination)), clock))
70 .unwrap();
71 }
72 #[cfg(debug_assertions)]
73 if let Some(logger) = extra_text_logger {
74 *EXTRA_TEXT_LOGGER.write().unwrap() = Some(Box::new(logger) as Box<dyn Log>);
75 }
76
77 runtime
78 }
79
80 pub fn flush(&self) {
81 if let Some((writer, _clock)) = WRITER.get() {
82 if let Ok(mut writer) = writer.lock() {
83 if let Err(err) = writer.flush() {
84 eprintln!("cu29_log: Failed to flush writer: {err}");
85 }
86 } else {
87 eprintln!("cu29_log: Failed to lock writer.");
88 }
89 } else {
90 eprintln!("cu29_log: Logger not initialized.");
91 }
92 }
93}
94
95impl Drop for LoggerRuntime {
96 fn drop(&mut self) {
97 self.flush();
98 if let Some((mutex, _clock)) = WRITER.get() {
99 if let Ok(mut writer_guard) = mutex.lock() {
100 *writer_guard = Box::new(DummyWriteStream);
102 }
103 }
104 }
105}
106
107#[inline(always)]
110pub fn log(entry: &mut CuLogEntry) -> CuResult<()> {
111 let d = WRITER.get().map(|(writer, clock)| (writer, clock));
112 if d.is_none() {
113 return Err("Logger not initialized.".into());
114 }
115 let (writer, clock) = d.unwrap();
116 entry.time = clock.now();
117 if let Err(err) = writer.lock().unwrap().log(entry) {
118 eprintln!("Failed to log data: {err}");
119 }
120 #[cfg(debug_assertions)]
122 {
123 }
126
127 Ok(())
128}
129
130#[cfg(debug_assertions)]
133pub fn log_debug_mode(
134 entry: &mut CuLogEntry,
135 format_str: &str, param_names: &[&str],
137) -> CuResult<()> {
138 log(entry)?;
139
140 let guarded_logger = EXTRA_TEXT_LOGGER.read().unwrap();
141 if guarded_logger.is_none() {
142 return Ok(());
143 }
144 if let Some(logger) = guarded_logger.as_ref() {
145 let fstr = format_str.to_string();
146 let params: Vec<String> = entry.params.iter().map(|v| v.to_string()).collect();
148 let named_params: Vec<(&str, String)> = param_names
149 .iter()
150 .zip(params.iter())
151 .map(|(name, value)| (*name, value.clone()))
152 .collect();
153 let named_params: HashMap<String, String> = named_params
155 .iter()
156 .map(|(k, v)| (k.to_string(), v.clone()))
157 .collect();
158
159 let log_level = match entry.level {
164 CuLogLevel::Debug => log::Level::Debug,
165 CuLogLevel::Info => log::Level::Info,
166 CuLogLevel::Warning => log::Level::Warn,
167 CuLogLevel::Error => log::Level::Error,
168 CuLogLevel::Critical => log::Level::Error,
169 };
170
171 let logline = format_logline(
172 entry.time,
173 entry.level,
174 &fstr,
175 params.as_slice(),
176 &named_params,
177 )?;
178 logger.log(
179 &log::Record::builder()
180 .args(format_args!("{logline}"))
181 .level(log_level)
182 .target("cu29_log")
183 .module_path_static(Some("cu29_log"))
184 .file_static(Some("cu29_log"))
185 .line(Some(0))
186 .build(),
187 );
188 }
189 Ok(())
190}
191
192pub struct OwningIoWriter<W: Write> {
194 writer: BufWriter<W>,
195 bytes_written: usize,
196}
197
198impl<W: Write> OwningIoWriter<W> {
199 pub fn new(writer: W) -> Self {
200 Self {
201 writer: BufWriter::new(writer),
202 bytes_written: 0,
203 }
204 }
205
206 pub fn bytes_written(&self) -> usize {
207 self.bytes_written
208 }
209
210 pub fn flush(&mut self) -> Result<(), EncodeError> {
211 self.writer.flush().map_err(|inner| EncodeError::Io {
212 inner,
213 index: self.bytes_written,
214 })
215 }
216}
217
218impl<W: Write> Writer for OwningIoWriter<W> {
219 #[inline(always)]
220 fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
221 self.writer
222 .write_all(bytes)
223 .map_err(|inner| EncodeError::Io {
224 inner,
225 index: self.bytes_written,
226 })?;
227 self.bytes_written += bytes.len();
228 Ok(())
229 }
230}
231
232pub struct SimpleFileWriter {
234 path: PathBuf,
235 encoder: EncoderImpl<OwningIoWriter<File>, Configuration>,
236}
237
238impl SimpleFileWriter {
239 pub fn new(path: &PathBuf) -> CuResult<Self> {
240 let file = std::fs::OpenOptions::new()
241 .create(true)
242 .truncate(true)
243 .write(true)
244 .open(path)
245 .map_err(|e| format!("Failed to open file: {e:?}"))?;
246
247 let writer = OwningIoWriter::new(file);
248 let encoder = EncoderImpl::new(writer, bincode::config::standard());
249
250 Ok(SimpleFileWriter {
251 path: path.clone(),
252 encoder,
253 })
254 }
255}
256
257impl Debug for SimpleFileWriter {
258 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
259 write!(f, "SimpleFileWriter for path {:?}", self.path)
260 }
261}
262
263impl WriteStream<CuLogEntry> for SimpleFileWriter {
264 #[inline(always)]
265 fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
266 obj.encode(&mut self.encoder)
267 .map_err(|e| format!("Failed to write to file: {e:?}"))?;
268 Ok(())
269 }
270
271 fn flush(&mut self) -> CuResult<()> {
272 self.encoder
273 .writer()
274 .flush()
275 .map_err(|e| format!("Failed to flush file: {e:?}"))?;
276 Ok(())
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use crate::CuLogEntry;
283 use bincode::config::standard;
284 use cu29_log::CuLogLevel;
285 use cu29_value::Value;
286 use smallvec::smallvec;
287
288 #[test]
289 fn test_encode_decode_structured_log() {
290 let log_entry = CuLogEntry {
291 time: 0.into(),
292 level: CuLogLevel::Info,
293 msg_index: 1,
294 paramname_indexes: smallvec![2, 3],
295 params: smallvec![Value::String("test".to_string())],
296 };
297 let encoded = bincode::encode_to_vec(&log_entry, standard()).unwrap();
298 let decoded_tuple: (CuLogEntry, usize) =
299 bincode::decode_from_slice(&encoded, standard()).unwrap();
300 assert_eq!(log_entry, decoded_tuple.0);
301 }
302}