cu29_log_runtime/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#[cfg(not(feature = "std"))]
3extern crate alloc;
4
5use cu29_clock::RobotClock;
6use cu29_log::CuLogEntry;
7#[allow(unused_imports)]
8use cu29_log::CuLogLevel;
9use cu29_traits::{CuResult, WriteStream};
10use log::Log;
11
12#[cfg(not(feature = "std"))]
13mod imp {
14    pub use alloc::boxed::Box;
15    pub use spin::Mutex;
16    pub use spin::once::Once as OnceLock;
17}
18
19#[cfg(feature = "std")]
20mod imp {
21    pub use bincode::config::Configuration;
22    pub use bincode::enc::Encode;
23    pub use bincode::enc::Encoder;
24    pub use bincode::enc::EncoderImpl;
25    pub use bincode::enc::write::Writer;
26    pub use bincode::error::EncodeError;
27    pub use std::fmt::{Debug, Formatter};
28    pub use std::fs::File;
29    pub use std::io::{BufWriter, Write};
30    pub use std::path::PathBuf;
31    pub use std::sync::{Mutex, OnceLock};
32
33    #[cfg(debug_assertions)]
34    pub use {cu29_log::format_logline, std::collections::HashMap, std::sync::RwLock};
35}
36
37use imp::*;
38
39#[allow(dead_code)] // for no_std
40#[derive(Debug)]
41struct DummyWriteStream;
42
43impl WriteStream<CuLogEntry> for DummyWriteStream {
44    #[allow(unused_variables)] // for no_std
45    fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
46        #[cfg(feature = "std")]
47        eprintln!("Pending logs got cut: {obj:?}");
48        Ok(())
49    }
50}
51type LogWriter = Box<dyn WriteStream<CuLogEntry> + Send + 'static>;
52type WriterPair = (Mutex<LogWriter>, RobotClock);
53
54static WRITER: OnceLock<WriterPair> = OnceLock::new();
55
56#[cfg(debug_assertions)]
57#[cfg(feature = "std")]
58pub static EXTRA_TEXT_LOGGER: RwLock<Option<Box<dyn Log + 'static>>> = RwLock::new(None);
59
60pub struct NullLog;
61impl Log for NullLog {
62    fn enabled(&self, _metadata: &log::Metadata) -> bool {
63        false
64    }
65
66    fn log(&self, _record: &log::Record) {}
67    fn flush(&self) {}
68}
69
70/// The lifetime of this struct is the lifetime of the logger.
71pub struct LoggerRuntime {}
72
73impl LoggerRuntime {
74    /// destination is the binary stream in which we will log the structured log.
75    /// `extra_text_logger` is the logger that will log the text logs in real time. This is slow and only for debug builds.
76    pub fn init(
77        clock: RobotClock,
78        destination: impl WriteStream<CuLogEntry> + 'static,
79        #[allow(unused_variables)] extra_text_logger: Option<impl Log + 'static>,
80    ) -> Self {
81        let runtime = LoggerRuntime {};
82
83        // If WRITER is already initialized, update the inner value.
84        // This should only be useful for unit testing.
85        if let Some((writer, _)) = WRITER.get() {
86            #[cfg(not(feature = "std"))]
87            let mut writer_guard = writer.lock();
88            #[cfg(feature = "std")]
89            let mut writer_guard = writer.lock().unwrap();
90            *writer_guard = Box::new(destination);
91        } else {
92            #[cfg(not(feature = "std"))]
93            WRITER.call_once(|| (Mutex::new(Box::new(destination)), clock));
94            #[cfg(feature = "std")]
95            WRITER
96                .set((Mutex::new(Box::new(destination)), clock))
97                .unwrap();
98        }
99        #[cfg(debug_assertions)]
100        #[cfg(feature = "std")]
101        if let Some(logger) = extra_text_logger {
102            let mut extra_text_logger = EXTRA_TEXT_LOGGER.write().unwrap();
103            *extra_text_logger = Some(Box::new(logger) as Box<dyn Log>);
104        }
105
106        runtime
107    }
108
109    pub fn flush(&self) {
110        // no op in no_std TODO(gbin): check if it will be needed in no_std at some point.
111        #[cfg(feature = "std")]
112        if let Some((writer, _clock)) = WRITER.get() {
113            if let Ok(mut writer) = writer.lock() {
114                if let Err(err) = writer.flush() {
115                    eprintln!("cu29_log: Failed to flush writer: {err}");
116                }
117            } else {
118                eprintln!("cu29_log: Failed to lock writer.");
119            }
120        } else {
121            eprintln!("cu29_log: Logger not initialized.");
122        }
123    }
124}
125
126impl Drop for LoggerRuntime {
127    fn drop(&mut self) {
128        self.flush();
129        // Assume on no-std that there is no buffering. TODO(gbin): check if this hold true.
130        #[cfg(feature = "std")]
131        if let Some((mutex, _clock)) = WRITER.get()
132            && let Ok(mut writer_guard) = mutex.lock()
133        {
134            // Replace the current WriteStream with a DummyWriteStream
135            *writer_guard = Box::new(DummyWriteStream);
136        }
137    }
138}
139
140/// Function called from generated code to log data.
141/// It moves entry by design, it will be absorbed in the queue.
142#[inline(always)]
143pub fn log(entry: &mut CuLogEntry) -> CuResult<()> {
144    let d = WRITER.get().map(|(writer, clock)| (writer, clock));
145    if d.is_none() {
146        return Err("Logger not initialized.".into());
147    }
148    let (writer, clock) = d.unwrap();
149    entry.time = clock.now();
150
151    #[cfg(not(feature = "std"))]
152    writer.lock().log(entry)?;
153
154    #[cfg(feature = "std")]
155    if let Err(err) = writer.lock().unwrap().log(entry) {
156        eprintln!("Failed to log data: {err}");
157    }
158    Ok(())
159}
160
161/// This version of log is only compiled in debug mode
162/// This allows a normal logging framework to be bridged.
163#[cfg(debug_assertions)]
164pub fn log_debug_mode(
165    entry: &mut CuLogEntry,
166    _format_str: &str, // this is the missing info at runtime.
167    _param_names: &[&str],
168) -> CuResult<()> {
169    log(entry)?;
170
171    // and the bridging is only available in std.
172    #[cfg(feature = "std")]
173    extra_log(entry, _format_str, _param_names)?;
174
175    Ok(())
176}
177
178#[cfg(debug_assertions)]
179#[cfg(feature = "std")]
180fn extra_log(entry: &mut CuLogEntry, format_str: &str, param_names: &[&str]) -> CuResult<()> {
181    let guarded_logger = EXTRA_TEXT_LOGGER.read().unwrap();
182    if guarded_logger.is_none() {
183        return Ok(());
184    }
185
186    if let Some(logger) = guarded_logger.as_ref() {
187        let fstr = format_str.to_string();
188        // transform the slice into a hashmap
189        let params: Vec<String> = entry.params.iter().map(|v| v.to_string()).collect();
190        let named_params: Vec<(&str, String)> = param_names
191            .iter()
192            .zip(params.iter())
193            .map(|(name, value)| (*name, value.clone()))
194            .collect();
195        // build hashmap of string, string from named_paramgs
196        let named_params: HashMap<String, String> = named_params
197            .iter()
198            .map(|(k, v)| (k.to_string(), v.clone()))
199            .collect();
200
201        // Convert Copper log level to the standard log level
202        // Note: CuLogLevel::Critical is mapped to log::Level::Error because the `log` crate
203        // does not have a `Critical` level. `Error` is the highest severity level available
204        // in the `log` crate, making it the closest equivalent.
205        let log_level = match entry.level {
206            CuLogLevel::Debug => log::Level::Debug,
207            CuLogLevel::Info => log::Level::Info,
208            CuLogLevel::Warning => log::Level::Warn,
209            CuLogLevel::Error => log::Level::Error,
210            CuLogLevel::Critical => log::Level::Error,
211        };
212
213        let logline = format_logline(
214            entry.time,
215            entry.level,
216            &fstr,
217            params.as_slice(),
218            &named_params,
219        )?;
220        logger.log(
221            &log::Record::builder()
222                .args(format_args!("{logline}"))
223                .level(log_level)
224                .target("cu29_log")
225                .module_path_static(Some("cu29_log"))
226                .file_static(Some("cu29_log"))
227                .line(Some(0))
228                .build(),
229        );
230    }
231    Ok(())
232}
233// This is an adaptation of the Iowriter from bincode.
234
235#[cfg(feature = "std")]
236pub struct OwningIoWriter<W: Write> {
237    writer: BufWriter<W>,
238    bytes_written: usize,
239}
240
241#[cfg(feature = "std")]
242impl<W: Write> OwningIoWriter<W> {
243    pub fn new(writer: W) -> Self {
244        Self {
245            writer: BufWriter::new(writer),
246            bytes_written: 0,
247        }
248    }
249
250    pub fn bytes_written(&self) -> usize {
251        self.bytes_written
252    }
253
254    pub fn flush(&mut self) -> Result<(), EncodeError> {
255        self.writer.flush().map_err(|inner| EncodeError::Io {
256            inner,
257            index: self.bytes_written,
258        })
259    }
260}
261
262#[cfg(feature = "std")]
263impl<W: Write> Writer for OwningIoWriter<W> {
264    #[inline(always)]
265    fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
266        self.writer
267            .write_all(bytes)
268            .map_err(|inner| EncodeError::Io {
269                inner,
270                index: self.bytes_written,
271            })?;
272        self.bytes_written += bytes.len();
273        Ok(())
274    }
275}
276
277/// This allows this crate to be used outside of Copper (ie. decoupling it from the unifiedlog.
278#[cfg(feature = "std")]
279pub struct SimpleFileWriter {
280    path: PathBuf,
281    encoder: EncoderImpl<OwningIoWriter<File>, Configuration>,
282}
283
284#[cfg(feature = "std")]
285impl SimpleFileWriter {
286    pub fn new(path: &PathBuf) -> CuResult<Self> {
287        let file = std::fs::OpenOptions::new()
288            .create(true)
289            .truncate(true)
290            .write(true)
291            .open(path)
292            .map_err(|e| format!("Failed to open file: {e:?}"))?;
293
294        let writer = OwningIoWriter::new(file);
295        let encoder = EncoderImpl::new(writer, bincode::config::standard());
296
297        Ok(SimpleFileWriter {
298            path: path.clone(),
299            encoder,
300        })
301    }
302}
303
304#[cfg(feature = "std")]
305impl Debug for SimpleFileWriter {
306    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
307        write!(f, "SimpleFileWriter for path {:?}", self.path)
308    }
309}
310
311#[cfg(feature = "std")]
312impl WriteStream<CuLogEntry> for SimpleFileWriter {
313    #[inline(always)]
314    fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
315        obj.encode(&mut self.encoder)
316            .map_err(|e| format!("Failed to write to file: {e:?}"))?;
317        Ok(())
318    }
319
320    fn flush(&mut self) -> CuResult<()> {
321        self.encoder
322            .writer()
323            .flush()
324            .map_err(|e| format!("Failed to flush file: {e:?}"))?;
325        Ok(())
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use crate::CuLogEntry;
332    use bincode::config::standard;
333    use cu29_log::CuLogLevel;
334    use cu29_value::Value;
335    use smallvec::smallvec;
336
337    #[cfg(not(feature = "std"))]
338    use alloc::string::ToString;
339
340    #[test]
341    fn test_encode_decode_structured_log() {
342        let log_entry = CuLogEntry {
343            time: 0.into(),
344            level: CuLogLevel::Info,
345            msg_index: 1,
346            paramname_indexes: smallvec![2, 3],
347            params: smallvec![Value::String("test".to_string())],
348        };
349        let encoded = bincode::encode_to_vec(&log_entry, standard()).unwrap();
350        let decoded_tuple: (CuLogEntry, usize) =
351            bincode::decode_from_slice(&encoded, standard()).unwrap();
352        assert_eq!(log_entry, decoded_tuple.0);
353    }
354}