Skip to main content

cu29_log/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#[cfg(not(feature = "std"))]
3extern crate alloc;
4extern crate core;
5
6use bincode::{Decode, Encode};
7use cu29_clock::CuTime;
8use cu29_value::Value;
9use serde::{Deserialize, Serialize};
10use smallvec::SmallVec;
11
12#[cfg(not(feature = "std"))]
13mod imp {
14    pub use core::fmt::Display;
15    pub use core::fmt::Formatter;
16    pub use core::fmt::Result as FmtResult;
17}
18
19#[cfg(feature = "defmt")]
20extern crate defmt;
21
22#[cfg(feature = "std")]
23mod imp {
24    pub use core::fmt::Display;
25    pub use cu29_traits::CuResult;
26    // strfmt forces hashmap from std
27    pub use std::collections::HashMap;
28    pub use std::fmt::Formatter;
29    pub use std::fmt::Result as FmtResult;
30    // This is a blocker for no_std, so no live logging in no_std for now.
31    pub use strfmt::strfmt;
32}
33
34use imp::*;
35
36/// Log levels for Copper.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
38pub enum CuLogLevel {
39    /// Detailed information useful during development
40    Debug = 0,
41    /// General information about system operation
42    Info = 1,
43    /// Indication of potential issues that don't prevent normal operation
44    Warning = 2,
45    /// Issues that might disrupt normal operation but don't cause system failure
46    Error = 3,
47    /// Critical errors requiring immediate attention, usually resulting in system failure
48    Critical = 4,
49}
50
51impl CuLogLevel {
52    /// Returns true if this log level is enabled for the given max level
53    ///
54    /// The log level is enabled if it is greater than or equal to the max level.
55    /// For example, if max_level is Info, then Info, Warning, Error and Critical are enabled,
56    /// but Debug is not.
57    #[inline]
58    pub const fn enabled(self, max_level: CuLogLevel) -> bool {
59        self as u8 >= max_level as u8
60    }
61}
62
63#[allow(dead_code)]
64pub const ANONYMOUS: u32 = 0;
65
66pub const MAX_LOG_PARAMS_ON_STACK: usize = 10;
67
68/// This is the basic structure for a log entry in Copper.
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub struct CuLogEntry {
71    // Approximate time when the log entry was created.
72    pub time: CuTime,
73
74    // Log level of this entry
75    pub level: CuLogLevel,
76
77    // interned index of the message
78    pub msg_index: u32,
79
80    // interned indexes of the parameter names
81    pub paramname_indexes: SmallVec<[u32; MAX_LOG_PARAMS_ON_STACK]>,
82
83    // Serializable values for the parameters (Values are acting like an Any Value).
84    pub params: SmallVec<[Value; MAX_LOG_PARAMS_ON_STACK]>,
85}
86
87impl Encode for CuLogEntry {
88    fn encode<E: bincode::enc::Encoder>(
89        &self,
90        encoder: &mut E,
91    ) -> Result<(), bincode::error::EncodeError> {
92        self.time.encode(encoder)?;
93        (self.level as u8).encode(encoder)?;
94        self.msg_index.encode(encoder)?;
95
96        (self.paramname_indexes.len() as u64).encode(encoder)?;
97        for &index in &self.paramname_indexes {
98            index.encode(encoder)?;
99        }
100
101        (self.params.len() as u64).encode(encoder)?;
102        for param in &self.params {
103            param.encode(encoder)?;
104        }
105
106        Ok(())
107    }
108}
109
110impl<Context> Decode<Context> for CuLogEntry {
111    fn decode<D: bincode::de::Decoder>(
112        decoder: &mut D,
113    ) -> Result<Self, bincode::error::DecodeError> {
114        let time = CuTime::decode(decoder)?;
115        let level_raw = u8::decode(decoder)?;
116        let level = match level_raw {
117            0 => CuLogLevel::Debug,
118            1 => CuLogLevel::Info,
119            2 => CuLogLevel::Warning,
120            3 => CuLogLevel::Error,
121            4 => CuLogLevel::Critical,
122            _ => CuLogLevel::Debug, // Default to Debug for compatibility with older logs
123        };
124        let msg_index = u32::decode(decoder)?;
125
126        let paramname_len = u64::decode(decoder)? as usize;
127        let mut paramname_indexes = SmallVec::with_capacity(paramname_len);
128        for _ in 0..paramname_len {
129            paramname_indexes.push(u32::decode(decoder)?);
130        }
131
132        let params_len = u64::decode(decoder)? as usize;
133        let mut params = SmallVec::with_capacity(params_len);
134        for _ in 0..params_len {
135            params.push(Value::decode(decoder)?);
136        }
137
138        Ok(CuLogEntry {
139            time,
140            level,
141            msg_index,
142            paramname_indexes,
143            params,
144        })
145    }
146}
147
148// This is for internal debug purposes.
149impl Display for CuLogEntry {
150    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
151        write!(
152            f,
153            "CuLogEntry {{ level: {:?}, msg_index: {}, paramname_indexes: {:?}, params: {:?} }}",
154            self.level, self.msg_index, self.paramname_indexes, self.params
155        )
156    }
157}
158
159impl CuLogEntry {
160    /// msg_index is the interned index of the message.
161    pub fn new(msg_index: u32, level: CuLogLevel) -> Self {
162        CuLogEntry {
163            time: 0.into(), // We have no clock at that point it is called from random places
164            // the clock will be set at actual log time from clock source provided
165            level,
166            msg_index,
167            paramname_indexes: SmallVec::new(),
168            params: SmallVec::new(),
169        }
170    }
171
172    /// Add a parameter to the log entry.
173    /// paramname_index is the interned index of the parameter name.
174    pub fn add_param(&mut self, paramname_index: u32, param: Value) {
175        self.paramname_indexes.push(paramname_index);
176        self.params.push(param);
177    }
178}
179
180/// Text log line formatter.
181/// Only available on std. TODO(gbin): Maybe reconsider that at some point
182#[inline]
183#[cfg(feature = "std")]
184pub fn format_logline(
185    time: CuTime,
186    level: CuLogLevel,
187    format_str: &str,
188    params: &[String],
189    named_params: &HashMap<String, String>,
190) -> CuResult<String> {
191    // If the format string uses positional placeholders (`{}`), fill them in order using the
192    // anonymous params first, then any named params (sorted for determinism) if needed.
193    if format_str.contains("{}") {
194        let mut formatted = format_str.to_string();
195        for param in params.iter() {
196            if !formatted.contains("{}") {
197                break;
198            }
199            formatted = formatted.replacen("{}", param, 1);
200        }
201        if formatted.contains("{}") && !named_params.is_empty() {
202            let mut named = named_params.iter().collect::<Vec<_>>();
203            named.sort_by(|a, b| a.0.cmp(b.0));
204            for (_, value) in named {
205                if !formatted.contains("{}") {
206                    break;
207                }
208                formatted = formatted.replacen("{}", value, 1);
209            }
210        }
211        return Ok(format!("{time} [{level:?}]: {formatted}"));
212    }
213
214    // Otherwise rely on named formatting.
215    let logline = strfmt(format_str, named_params).map_err(|e| {
216        cu29_traits::CuError::new_with_cause(
217            format!("Failed to format log line: {format_str:?} with variables [{named_params:?}]")
218                .as_str(),
219            e,
220        )
221    })?;
222    Ok(format!("{time} [{level:?}]: {logline}"))
223}
224
225/// Rebuild a log line from the interned strings and the CuLogEntry.
226/// This basically translates the world of copper logs to text logs.
227#[cfg(feature = "std")]
228pub fn rebuild_logline(all_interned_strings: &[String], entry: &CuLogEntry) -> CuResult<String> {
229    let format_string = all_interned_strings
230        .get(entry.msg_index as usize)
231        .ok_or_else(|| {
232            cu29_traits::CuError::from(format!(
233                "Invalid message index {} (interned strings length {})",
234                entry.msg_index,
235                all_interned_strings.len()
236            ))
237        })?;
238    if entry.paramname_indexes.len() != entry.params.len() {
239        return Err(cu29_traits::CuError::from(format!(
240            "Mismatched parameter metadata: {} names for {} params",
241            entry.paramname_indexes.len(),
242            entry.params.len()
243        )));
244    }
245
246    let mut anon_params = Vec::with_capacity(entry.params.len());
247    let mut named_params = HashMap::with_capacity(entry.params.len());
248
249    for (i, param) in entry.params.iter().enumerate() {
250        let param_as_string = format!("{param}");
251        if entry.paramname_indexes[i] == 0 {
252            // Anonymous parameter
253            anon_params.push(param_as_string);
254        } else {
255            // Named parameter
256            let name = all_interned_strings
257                .get(entry.paramname_indexes[i] as usize)
258                .ok_or_else(|| {
259                    cu29_traits::CuError::from(format!(
260                        "Invalid parameter name index {} (interned strings length {})",
261                        entry.paramname_indexes[i],
262                        all_interned_strings.len()
263                    ))
264                })?
265                .clone();
266            named_params.insert(name, param_as_string);
267        }
268    }
269    format_logline(
270        entry.time,
271        entry.level,
272        format_string,
273        &anon_params,
274        &named_params,
275    )
276}
277
278// ---- defmt shims, selected at cu29-log compile time ----
279#[cfg(all(feature = "defmt", not(feature = "std")))]
280#[macro_export]
281macro_rules! __cu29_defmt_debug {
282    ($fmt:literal $(, $arg:expr)* $(,)?) => {
283        ::defmt::debug!($fmt $(, $arg)*);
284    }
285}
286#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
287#[macro_export]
288macro_rules! __cu29_defmt_debug {
289    ($($tt:tt)*) => {{}};
290}
291
292#[cfg(all(feature = "defmt", not(feature = "std")))]
293#[macro_export]
294macro_rules! __cu29_defmt_info {
295    ($fmt:literal $(, $arg:expr)* $(,)?) => {
296        ::defmt::info!($fmt $(, $arg)*);
297    }
298}
299#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
300#[macro_export]
301macro_rules! __cu29_defmt_info {
302    ($($tt:tt)*) => {{}};
303}
304
305#[cfg(all(feature = "defmt", not(feature = "std")))]
306#[macro_export]
307macro_rules! __cu29_defmt_warn {
308    ($fmt:literal $(, $arg:expr)* $(,)?) => {
309        ::defmt::warn!($fmt $(, $arg)*);
310    }
311}
312#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
313#[macro_export]
314macro_rules! __cu29_defmt_warn {
315    ($($tt:tt)*) => {{}};
316}
317
318#[cfg(all(feature = "defmt", not(feature = "std")))]
319#[macro_export]
320macro_rules! __cu29_defmt_error {
321    ($fmt:literal $(, $arg:expr)* $(,)?) => {
322        ::defmt::error!($fmt $(, $arg)*);
323    }
324}
325#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
326#[macro_export]
327macro_rules! __cu29_defmt_error {
328    ($($tt:tt)*) => {{}};
329}
330
331#[macro_export]
332macro_rules! defmt_debug {
333    ($($tt:tt)*) => { $crate::__cu29_defmt_debug!($($tt)*) };
334}
335
336#[macro_export]
337macro_rules! defmt_info {
338    ($($tt:tt)*) => { $crate::__cu29_defmt_info!($($tt)*) };
339}
340
341#[macro_export]
342macro_rules! defmt_warn {
343    ($($tt:tt)*) => { $crate::__cu29_defmt_warn!($($tt)*) };
344}
345
346#[macro_export]
347macro_rules! defmt_error {
348    ($($tt:tt)*) => { $crate::__cu29_defmt_error!($($tt)*) };
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_log_level_ordering() {
357        assert!(CuLogLevel::Critical > CuLogLevel::Error);
358        assert!(CuLogLevel::Error > CuLogLevel::Warning);
359        assert!(CuLogLevel::Warning > CuLogLevel::Info);
360        assert!(CuLogLevel::Info > CuLogLevel::Debug);
361
362        assert!(CuLogLevel::Debug < CuLogLevel::Info);
363        assert!(CuLogLevel::Info < CuLogLevel::Warning);
364        assert!(CuLogLevel::Warning < CuLogLevel::Error);
365        assert!(CuLogLevel::Error < CuLogLevel::Critical);
366    }
367
368    #[test]
369    fn test_log_level_enabled() {
370        // When min level is Debug (0), all logs are enabled
371        assert!(CuLogLevel::Debug.enabled(CuLogLevel::Debug));
372        assert!(CuLogLevel::Info.enabled(CuLogLevel::Debug));
373        assert!(CuLogLevel::Warning.enabled(CuLogLevel::Debug));
374        assert!(CuLogLevel::Error.enabled(CuLogLevel::Debug));
375        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Debug));
376
377        // When min level is Info (1), only Info and above are enabled
378        assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Info));
379        assert!(CuLogLevel::Info.enabled(CuLogLevel::Info));
380        assert!(CuLogLevel::Warning.enabled(CuLogLevel::Info));
381        assert!(CuLogLevel::Error.enabled(CuLogLevel::Info));
382        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Info));
383
384        // When min level is Warning (2), only Warning and above are enabled
385        assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Warning));
386        assert!(!CuLogLevel::Info.enabled(CuLogLevel::Warning));
387        assert!(CuLogLevel::Warning.enabled(CuLogLevel::Warning));
388        assert!(CuLogLevel::Error.enabled(CuLogLevel::Warning));
389        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Warning));
390
391        // When min level is Error (3), only Error and above are enabled
392        assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Error));
393        assert!(!CuLogLevel::Info.enabled(CuLogLevel::Error));
394        assert!(!CuLogLevel::Warning.enabled(CuLogLevel::Error));
395        assert!(CuLogLevel::Error.enabled(CuLogLevel::Error));
396        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Error));
397
398        // When min level is Critical (4), only Critical is enabled
399        assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Critical));
400        assert!(!CuLogLevel::Info.enabled(CuLogLevel::Critical));
401        assert!(!CuLogLevel::Warning.enabled(CuLogLevel::Critical));
402        assert!(!CuLogLevel::Error.enabled(CuLogLevel::Critical));
403        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Critical));
404    }
405
406    #[test]
407    fn test_cu_log_entry_with_level() {
408        let entry = CuLogEntry::new(42, CuLogLevel::Warning);
409        assert_eq!(entry.level, CuLogLevel::Warning);
410        assert_eq!(entry.msg_index, 42);
411    }
412}