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)] #[derive(Debug)]
41struct DummyWriteStream;
42
43impl WriteStream<CuLogEntry> for DummyWriteStream {
44 #[allow(unused_variables)] 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
70pub struct LoggerRuntime {}
72
73impl LoggerRuntime {
74 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 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 #[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 #[cfg(feature = "std")]
131 if let Some((mutex, _clock)) = WRITER.get()
132 && let Ok(mut writer_guard) = mutex.lock()
133 {
134 *writer_guard = Box::new(DummyWriteStream);
136 }
137 }
138}
139
140#[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#[cfg(debug_assertions)]
164pub fn log_debug_mode(
165 entry: &mut CuLogEntry,
166 _format_str: &str, _param_names: &[&str],
168) -> CuResult<()> {
169 log(entry)?;
170
171 #[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 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 let named_params: HashMap<String, String> = named_params
197 .iter()
198 .map(|(k, v)| (k.to_string(), v.clone()))
199 .collect();
200
201 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#[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#[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}