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;
8use cu29_traits::{CuResult, WriteStream};
9use log::Log;
10
11#[cfg(debug_assertions)]
12use {cu29_log::format_logline, std::collections::HashMap, std::sync::RwLock};
13
14use std::fmt::{Debug, Formatter};
15use std::fs::File;
16use std::io::{BufWriter, Write};
17use std::path::PathBuf;
18use std::sync::{Mutex, OnceLock};
19
20#[derive(Debug)]
21struct DummyWriteStream;
22
23impl WriteStream<CuLogEntry> for DummyWriteStream {
24    fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
25        eprintln!("Pending logs got cut: {obj:?}");
26        Ok(())
27    }
28}
29type LogWriter = Box<dyn WriteStream<CuLogEntry>>;
30type WriterPair = (Mutex<LogWriter>, RobotClock);
31
32static WRITER: OnceLock<WriterPair> = OnceLock::new();
33
34#[cfg(debug_assertions)]
35pub static EXTRA_TEXT_LOGGER: RwLock<Option<Box<dyn Log + 'static>>> = RwLock::new(None);
36
37pub struct NullLog;
38impl Log for NullLog {
39    fn enabled(&self, _metadata: &log::Metadata) -> bool {
40        false
41    }
42
43    fn log(&self, _record: &log::Record) {}
44    fn flush(&self) {}
45}
46
47/// The lifetime of this struct is the lifetime of the logger.
48pub struct LoggerRuntime {}
49
50impl LoggerRuntime {
51    /// destination is the binary stream in which we will log the structured log.
52    /// `extra_text_logger` is the logger that will log the text logs in real time. This is slow and only for debug builds.
53    pub fn init(
54        clock: RobotClock,
55        destination: impl WriteStream<CuLogEntry> + 'static,
56        #[allow(unused_variables)] extra_text_logger: Option<impl Log + 'static>,
57    ) -> Self {
58        let runtime = LoggerRuntime {};
59
60        // If WRITER is already initialized, update the inner value.
61        // This should only be useful for unit testing.
62        if let Some((writer, _)) = WRITER.get() {
63            let mut writer_guard = writer.lock().unwrap();
64            *writer_guard = Box::new(destination);
65        } else {
66            WRITER
67                .set((Mutex::new(Box::new(destination)), clock))
68                .unwrap();
69        }
70        #[cfg(debug_assertions)]
71        if let Some(logger) = extra_text_logger {
72            *EXTRA_TEXT_LOGGER.write().unwrap() = Some(Box::new(logger) as Box<dyn Log>);
73        }
74
75        runtime
76    }
77
78    pub fn flush(&self) {
79        if let Some((writer, _clock)) = WRITER.get() {
80            if let Ok(mut writer) = writer.lock() {
81                if let Err(err) = writer.flush() {
82                    eprintln!("cu29_log: Failed to flush writer: {err}");
83                }
84            } else {
85                eprintln!("cu29_log: Failed to lock writer.");
86            }
87        } else {
88            eprintln!("cu29_log: Logger not initialized.");
89        }
90    }
91}
92
93impl Drop for LoggerRuntime {
94    fn drop(&mut self) {
95        self.flush();
96        if let Some((mutex, _clock)) = WRITER.get() {
97            if let Ok(mut writer_guard) = mutex.lock() {
98                // Replace the current WriteStream with a DummyWriteStream
99                *writer_guard = Box::new(DummyWriteStream);
100            }
101        }
102    }
103}
104
105/// Function called from generated code to log data.
106/// It moves entry by design, it will be absorbed in the queue.
107#[inline(always)]
108pub fn log(entry: &mut CuLogEntry) -> CuResult<()> {
109    let d = WRITER.get().map(|(writer, clock)| (writer, clock));
110    if d.is_none() {
111        return Err("Logger not initialized.".into());
112    }
113    let (writer, clock) = d.unwrap();
114    entry.time = clock.now();
115    if let Err(err) = writer.lock().unwrap().log(entry) {
116        eprintln!("Failed to log data: {err}");
117    }
118    // This is only for debug builds with standard textual logging implemented.
119    #[cfg(debug_assertions)]
120    {
121        // This scope is important :).
122        // if we have not passed a text logger in debug mode, it is ok just move along.
123    }
124
125    Ok(())
126}
127
128/// This version of log is only compiled in debug mode
129/// This allows a normal logging framework to be bridged.
130#[cfg(debug_assertions)]
131pub fn log_debug_mode(
132    entry: &mut CuLogEntry,
133    format_str: &str, // this is the missing info at runtime.
134    param_names: &[&str],
135) -> CuResult<()> {
136    log(entry)?;
137
138    let guarded_logger = EXTRA_TEXT_LOGGER.read().unwrap();
139    if guarded_logger.is_none() {
140        return Ok(());
141    }
142    if let Some(logger) = guarded_logger.as_ref() {
143        let fstr = format_str.to_string();
144        // transform the slice into a hashmap
145        let params: Vec<String> = entry.params.iter().map(|v| v.to_string()).collect();
146        let named_params: Vec<(&str, String)> = param_names
147            .iter()
148            .zip(params.iter())
149            .map(|(name, value)| (*name, value.clone()))
150            .collect();
151        // build hashmap of string, string from named_paramgs
152        let named_params: HashMap<String, String> = named_params
153            .iter()
154            .map(|(k, v)| (k.to_string(), v.clone()))
155            .collect();
156        let logline = format_logline(entry.time, &fstr, params.as_slice(), &named_params)?;
157        logger.log(
158            &log::Record::builder()
159                .args(format_args!("{logline}"))
160                .level(log::Level::Info)
161                .target("cu29_log")
162                .module_path_static(Some("cu29_log"))
163                .file_static(Some("cu29_log"))
164                .line(Some(0))
165                .build(),
166        );
167    }
168    Ok(())
169}
170
171// This is an adaptation of the Iowriter from bincode.
172pub struct OwningIoWriter<W: Write> {
173    writer: BufWriter<W>,
174    bytes_written: usize,
175}
176
177impl<W: Write> OwningIoWriter<W> {
178    pub fn new(writer: W) -> Self {
179        Self {
180            writer: BufWriter::new(writer),
181            bytes_written: 0,
182        }
183    }
184
185    pub fn bytes_written(&self) -> usize {
186        self.bytes_written
187    }
188
189    pub fn flush(&mut self) -> Result<(), EncodeError> {
190        self.writer.flush().map_err(|inner| EncodeError::Io {
191            inner,
192            index: self.bytes_written,
193        })
194    }
195}
196
197impl<W: Write> Writer for OwningIoWriter<W> {
198    #[inline(always)]
199    fn write(&mut self, bytes: &[u8]) -> Result<(), EncodeError> {
200        self.writer
201            .write_all(bytes)
202            .map_err(|inner| EncodeError::Io {
203                inner,
204                index: self.bytes_written,
205            })?;
206        self.bytes_written += bytes.len();
207        Ok(())
208    }
209}
210
211/// This allows this crate to be used outside of Copper (ie. decoupling it from the unifiedlog.
212pub struct SimpleFileWriter {
213    path: PathBuf,
214    encoder: EncoderImpl<OwningIoWriter<File>, Configuration>,
215}
216
217impl SimpleFileWriter {
218    pub fn new(path: &PathBuf) -> CuResult<Self> {
219        let file = std::fs::OpenOptions::new()
220            .create(true)
221            .truncate(true)
222            .write(true)
223            .open(path)
224            .map_err(|e| format!("Failed to open file: {e:?}"))?;
225
226        let writer = OwningIoWriter::new(file);
227        let encoder = EncoderImpl::new(writer, bincode::config::standard());
228
229        Ok(SimpleFileWriter {
230            path: path.clone(),
231            encoder,
232        })
233    }
234}
235
236impl Debug for SimpleFileWriter {
237    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
238        write!(f, "SimpleFileWriter for path {:?}", self.path)
239    }
240}
241
242impl WriteStream<CuLogEntry> for SimpleFileWriter {
243    #[inline(always)]
244    fn log(&mut self, obj: &CuLogEntry) -> CuResult<()> {
245        obj.encode(&mut self.encoder)
246            .map_err(|e| format!("Failed to write to file: {e:?}"))?;
247        Ok(())
248    }
249
250    fn flush(&mut self) -> CuResult<()> {
251        self.encoder
252            .writer()
253            .flush()
254            .map_err(|e| format!("Failed to flush file: {e:?}"))?;
255        Ok(())
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use crate::CuLogEntry;
262    use bincode::config::standard;
263    use cu29_value::Value;
264    use smallvec::smallvec;
265
266    #[test]
267    fn test_encode_decode_structured_log() {
268        let log_entry = CuLogEntry {
269            time: 0.into(),
270            msg_index: 1,
271            paramname_indexes: smallvec![2, 3],
272            params: smallvec![Value::String("test".to_string())],
273        };
274        let encoded = bincode::encode_to_vec(&log_entry, standard()).unwrap();
275        let decoded_tuple: (CuLogEntry, usize) =
276            bincode::decode_from_slice(&encoded, standard()).unwrap();
277        assert_eq!(log_entry, decoded_tuple.0);
278    }
279}