cu29_log_runtime/
lib.rs

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
49/// The lifetime of this struct is the lifetime of the logger.
50pub struct LoggerRuntime {}
51
52impl LoggerRuntime {
53    /// destination is the binary stream in which we will log the structured log.
54    /// `extra_text_logger` is the logger that will log the text logs in real time. This is slow and only for debug builds.
55    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 WRITER is already initialized, update the inner value.
63        // This should only be useful for unit testing.
64        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                // Replace the current WriteStream with a DummyWriteStream
101                *writer_guard = Box::new(DummyWriteStream);
102            }
103        }
104    }
105}
106
107/// Function called from generated code to log data.
108/// It moves entry by design, it will be absorbed in the queue.
109#[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    // This is only for debug builds with standard textual logging implemented.
121    #[cfg(debug_assertions)]
122    {
123        // This scope is important :).
124        // if we have not passed a text logger in debug mode, it is ok just move along.
125    }
126
127    Ok(())
128}
129
130/// This version of log is only compiled in debug mode
131/// This allows a normal logging framework to be bridged.
132#[cfg(debug_assertions)]
133pub fn log_debug_mode(
134    entry: &mut CuLogEntry,
135    format_str: &str, // this is the missing info at runtime.
136    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        // transform the slice into a hashmap
147        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        // build hashmap of string, string from named_paramgs
154        let named_params: HashMap<String, String> = named_params
155            .iter()
156            .map(|(k, v)| (k.to_string(), v.clone()))
157            .collect();
158
159        // Convert Copper log level to the standard log level
160        // Note: CuLogLevel::Critical is mapped to log::Level::Error because the `log` crate
161        // does not have a `Critical` level. `Error` is the highest severity level available
162        // in the `log` crate, making it the closest equivalent.
163        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
192// This is an adaptation of the Iowriter from bincode.
193pub 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
232/// This allows this crate to be used outside of Copper (ie. decoupling it from the unifiedlog.
233pub 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}