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::once::Once as OnceLock;
16    pub use spin::Mutex;
17}
18
19#[cfg(feature = "std")]
20mod imp {
21    pub use bincode::config::Configuration;
22    pub use bincode::enc::write::Writer;
23    pub use bincode::enc::Encode;
24    pub use bincode::enc::Encoder;
25    pub use bincode::enc::EncoderImpl;
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            if let Ok(mut writer_guard) = mutex.lock() {
133                // Replace the current WriteStream with a DummyWriteStream
134                *writer_guard = Box::new(DummyWriteStream);
135            }
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
159    // This is only for debug builds with standard textual logging implemented.
160    #[cfg(debug_assertions)]
161    {
162        // This scope is important :).
163        // if we have not passed a text logger in debug mode, it is ok just move along.
164    }
165
166    Ok(())
167}
168
169/// This version of log is only compiled in debug mode
170/// This allows a normal logging framework to be bridged.
171#[cfg(debug_assertions)]
172pub fn log_debug_mode(
173    entry: &mut CuLogEntry,
174    _format_str: &str, // this is the missing info at runtime.
175    _param_names: &[&str],
176) -> CuResult<()> {
177    log(entry)?;
178
179    // and the bridging is only available in std.
180    #[cfg(feature = "std")]
181    extra_log(entry, _format_str, _param_names)?;
182
183    Ok(())
184}
185
186#[cfg(debug_assertions)]
187#[cfg(feature = "std")]
188fn extra_log(entry: &mut CuLogEntry, format_str: &str, param_names: &[&str]) -> CuResult<()> {
189    let guarded_logger = EXTRA_TEXT_LOGGER.read().unwrap();
190    if guarded_logger.is_none() {
191        return Ok(());
192    }
193
194    if let Some(logger) = guarded_logger.as_ref() {
195        let fstr = format_str.to_string();
196        // transform the slice into a hashmap
197        let params: Vec<String> = entry.params.iter().map(|v| v.to_string()).collect();
198        let named_params: Vec<(&str, String)> = param_names
199            .iter()
200            .zip(params.iter())
201            .map(|(name, value)| (*name, value.clone()))
202            .collect();
203        // build hashmap of string, string from named_paramgs
204        let named_params: HashMap<String, String> = named_params
205            .iter()
206            .map(|(k, v)| (k.to_string(), v.clone()))
207            .collect();
208
209        // Convert Copper log level to the standard log level
210        // Note: CuLogLevel::Critical is mapped to log::Level::Error because the `log` crate
211        // does not have a `Critical` level. `Error` is the highest severity level available
212        // in the `log` crate, making it the closest equivalent.
213        let log_level = match entry.level {
214            CuLogLevel::Debug => log::Level::Debug,
215            CuLogLevel::Info => log::Level::Info,
216            CuLogLevel::Warning => log::Level::Warn,
217            CuLogLevel::Error => log::Level::Error,
218            CuLogLevel::Critical => log::Level::Error,
219        };
220
221        let logline = format_logline(
222            entry.time,
223            entry.level,
224            &fstr,
225            params.as_slice(),
226            &named_params,
227        )?;
228        logger.log(
229            &log::Record::builder()
230                .args(format_args!("{logline}"))
231                .level(log_level)
232                .target("cu29_log")
233                .module_path_static(Some("cu29_log"))
234                .file_static(Some("cu29_log"))
235                .line(Some(0))
236                .build(),
237        );
238    }
239    Ok(())
240}
241// This is an adaptation of the Iowriter from bincode.
242
243#[cfg(feature = "std")]
244pub struct OwningIoWriter<W: Write> {
245    writer: BufWriter<W>,
246    bytes_written: usize,
247}
248
249#[cfg(feature = "std")]
250impl<W: Write> OwningIoWriter<W> {
251    pub fn new(writer: W) -> Self {
252        Self {
253            writer: BufWriter::new(writer),
254            bytes_written: 0,
255        }
256    }
257
258    pub fn bytes_written(&self) -> usize {
259        self.bytes_written
260    }
261
262    pub fn flush(&mut self) -> Result<(), EncodeError> {
263        self.writer.flush().map_err(|inner| EncodeError::Io {
264            inner,
265            index: self.bytes_written,
266        })
267    }
268}
269
270#[cfg(feature = "std")]
271impl<W: Write> Writer for OwningIoWriter<W> {
272    #[inline(always)]
273    fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
274        self.writer
275            .write_all(bytes)
276            .map_err(|inner| EncodeError::Io {
277                inner,
278                index: self.bytes_written,
279            })?;
280        self.bytes_written += bytes.len();
281        Ok(())
282    }
283}
284
285/// This allows this crate to be used outside of Copper (ie. decoupling it from the unifiedlog.
286#[cfg(feature = "std")]
287pub struct SimpleFileWriter {
288    path: PathBuf,
289    encoder: EncoderImpl<OwningIoWriter<File>, Configuration>,
290}
291
292#[cfg(feature = "std")]
293impl SimpleFileWriter {
294    pub fn new(path: &PathBuf) -> CuResult<Self> {
295        let file = std::fs::OpenOptions::new()
296            .create(true)
297            .truncate(true)
298            .write(true)
299            .open(path)
300            .map_err(|e| format!("Failed to open file: {e:?}"))?;
301
302        let writer = OwningIoWriter::new(file);
303        let encoder = EncoderImpl::new(writer, bincode::config::standard());
304
305        Ok(SimpleFileWriter {
306            path: path.clone(),
307            encoder,
308        })
309    }
310}
311
312#[cfg(feature = "std")]
313impl Debug for SimpleFileWriter {
314    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
315        write!(f, "SimpleFileWriter for path {:?}", self.path)
316    }
317}
318
319#[cfg(feature = "std")]
320impl WriteStream<CuLogEntry> for SimpleFileWriter {
321    #[inline(always)]
322    fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
323        obj.encode(&mut self.encoder)
324            .map_err(|e| format!("Failed to write to file: {e:?}"))?;
325        Ok(())
326    }
327
328    fn flush(&mut self) -> CuResult<()> {
329        self.encoder
330            .writer()
331            .flush()
332            .map_err(|e| format!("Failed to flush file: {e:?}"))?;
333        Ok(())
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use crate::CuLogEntry;
340    use bincode::config::standard;
341    use cu29_log::CuLogLevel;
342    use cu29_value::Value;
343    use smallvec::smallvec;
344
345    #[cfg(not(feature = "std"))]
346    use alloc::string::ToString;
347
348    #[test]
349    fn test_encode_decode_structured_log() {
350        let log_entry = CuLogEntry {
351            time: 0.into(),
352            level: CuLogLevel::Info,
353            msg_index: 1,
354            paramname_indexes: smallvec![2, 3],
355            params: smallvec![Value::String("test".to_string())],
356        };
357        let encoded = bincode::encode_to_vec(&log_entry, standard()).unwrap();
358        let decoded_tuple: (CuLogEntry, usize) =
359            bincode::decode_from_slice(&encoded, standard()).unwrap();
360        assert_eq!(log_entry, decoded_tuple.0);
361    }
362}