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
47pub struct LoggerRuntime {}
49
50impl LoggerRuntime {
51 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 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 *writer_guard = Box::new(DummyWriteStream);
100 }
101 }
102 }
103}
104
105#[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 #[cfg(debug_assertions)]
120 {
121 }
124
125 Ok(())
126}
127
128#[cfg(debug_assertions)]
131pub fn log_debug_mode(
132 entry: &mut CuLogEntry,
133 format_str: &str, 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 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 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
171pub 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
211pub 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}