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)] #[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 if let Ok(mut writer_guard) = mutex.lock() {
133 *writer_guard = Box::new(DummyWriteStream);
135 }
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
159 #[cfg(debug_assertions)]
161 {
162 }
165
166 Ok(())
167}
168
169#[cfg(debug_assertions)]
172pub fn log_debug_mode(
173 entry: &mut CuLogEntry,
174 _format_str: &str, _param_names: &[&str],
176) -> CuResult<()> {
177 log(entry)?;
178
179 #[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 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 let named_params: HashMap<String, String> = named_params
205 .iter()
206 .map(|(k, v)| (k.to_string(), v.clone()))
207 .collect();
208
209 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#[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#[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}