cu29_log/
lib.rs

1use bincode::{Decode, Encode};
2use cu29_clock::CuTime;
3use cu29_traits::{CuError, CuResult};
4use cu29_value::Value;
5use serde::{Deserialize, Serialize};
6use smallvec::SmallVec;
7use std::collections::HashMap;
8use std::fmt::Display;
9use strfmt::strfmt;
10
11/// Log levels for Copper.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
13pub enum CuLogLevel {
14    /// Detailed information useful during development
15    Debug = 0,
16    /// General information about system operation
17    Info = 1,
18    /// Indication of potential issues that don't prevent normal operation
19    Warning = 2,
20    /// Issues that might disrupt normal operation but don't cause system failure
21    Error = 3,
22    /// Critical errors requiring immediate attention, usually resulting in system failure
23    Critical = 4,
24}
25
26impl CuLogLevel {
27    /// Returns true if this log level is enabled for the given max level
28    ///
29    /// The log level is enabled if it is greater than or equal to the max level.
30    /// For example, if max_level is Info, then Info, Warning, Error and Critical are enabled,
31    /// but Debug is not.
32    #[inline]
33    pub const fn enabled(self, max_level: CuLogLevel) -> bool {
34        self as u8 >= max_level as u8
35    }
36}
37
38#[allow(dead_code)]
39pub const ANONYMOUS: u32 = 0;
40
41pub const MAX_LOG_PARAMS_ON_STACK: usize = 10;
42
43/// This is the basic structure for a log entry in Copper.
44#[derive(Debug, Serialize, Deserialize, PartialEq)]
45pub struct CuLogEntry {
46    // Approximate time when the log entry was created.
47    pub time: CuTime,
48
49    // Log level of this entry
50    pub level: CuLogLevel,
51
52    // interned index of the message
53    pub msg_index: u32,
54
55    // interned indexes of the parameter names
56    pub paramname_indexes: SmallVec<[u32; MAX_LOG_PARAMS_ON_STACK]>,
57
58    // Serializable values for the parameters (Values are acting like an Any Value).
59    pub params: SmallVec<[Value; MAX_LOG_PARAMS_ON_STACK]>,
60}
61
62impl Encode for CuLogEntry {
63    fn encode<E: bincode::enc::Encoder>(
64        &self,
65        encoder: &mut E,
66    ) -> Result<(), bincode::error::EncodeError> {
67        self.time.encode(encoder)?;
68        (self.level as u8).encode(encoder)?;
69        self.msg_index.encode(encoder)?;
70
71        (self.paramname_indexes.len() as u64).encode(encoder)?;
72        for &index in &self.paramname_indexes {
73            index.encode(encoder)?;
74        }
75
76        (self.params.len() as u64).encode(encoder)?;
77        for param in &self.params {
78            param.encode(encoder)?;
79        }
80
81        Ok(())
82    }
83}
84
85impl<Context> Decode<Context> for CuLogEntry {
86    fn decode<D: bincode::de::Decoder>(
87        decoder: &mut D,
88    ) -> Result<Self, bincode::error::DecodeError> {
89        let time = CuTime::decode(decoder)?;
90        let level_raw = u8::decode(decoder)?;
91        let level = match level_raw {
92            0 => CuLogLevel::Debug,
93            1 => CuLogLevel::Info,
94            2 => CuLogLevel::Warning,
95            3 => CuLogLevel::Error,
96            4 => CuLogLevel::Critical,
97            _ => CuLogLevel::Debug, // Default to Debug for compatibility with older logs
98        };
99        let msg_index = u32::decode(decoder)?;
100
101        let paramname_len = u64::decode(decoder)? as usize;
102        let mut paramname_indexes = SmallVec::with_capacity(paramname_len);
103        for _ in 0..paramname_len {
104            paramname_indexes.push(u32::decode(decoder)?);
105        }
106
107        let params_len = u64::decode(decoder)? as usize;
108        let mut params = SmallVec::with_capacity(params_len);
109        for _ in 0..params_len {
110            params.push(Value::decode(decoder)?);
111        }
112
113        Ok(CuLogEntry {
114            time,
115            level,
116            msg_index,
117            paramname_indexes,
118            params,
119        })
120    }
121}
122
123// This is for internal debug purposes.
124impl Display for CuLogEntry {
125    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126        write!(
127            f,
128            "CuLogEntry {{ level: {:?}, msg_index: {}, paramname_indexes: {:?}, params: {:?} }}",
129            self.level, self.msg_index, self.paramname_indexes, self.params
130        )
131    }
132}
133
134impl CuLogEntry {
135    /// msg_index is the interned index of the message.
136    pub fn new(msg_index: u32, level: CuLogLevel) -> Self {
137        CuLogEntry {
138            time: 0.into(), // We have no clock at that point it is called from random places
139            // the clock will be set at actual log time from clock source provided
140            level,
141            msg_index,
142            paramname_indexes: SmallVec::new(),
143            params: SmallVec::new(),
144        }
145    }
146
147    /// Add a parameter to the log entry.
148    /// paramname_index is the interned index of the parameter name.
149    pub fn add_param(&mut self, paramname_index: u32, param: Value) {
150        self.paramname_indexes.push(paramname_index);
151        self.params.push(param);
152    }
153}
154
155/// Text log line formatter.
156#[inline]
157pub fn format_logline(
158    time: CuTime,
159    level: CuLogLevel,
160    format_str: &str,
161    params: &[String],
162    named_params: &HashMap<String, String>,
163) -> CuResult<String> {
164    let mut format_str = format_str.to_string();
165
166    for param in params.iter() {
167        format_str = format_str.replacen("{}", param, 1);
168    }
169
170    if named_params.is_empty() {
171        return Ok(format_str);
172    }
173
174    let logline = strfmt(&format_str, named_params).map_err(|e| {
175        CuError::new_with_cause(
176            format!("Failed to format log line: {format_str:?} with variables [{named_params:?}]")
177                .as_str(),
178            e,
179        )
180    })?;
181    Ok(format!("{time} [{level:?}]: {logline}"))
182}
183
184/// Rebuild a log line from the interned strings and the CuLogEntry.
185/// This basically translates the world of copper logs to text logs.
186pub fn rebuild_logline(all_interned_strings: &[String], entry: &CuLogEntry) -> CuResult<String> {
187    let format_string = &all_interned_strings[entry.msg_index as usize];
188    let mut anon_params: Vec<String> = Vec::new();
189    let mut named_params = HashMap::new();
190
191    for (i, param) in entry.params.iter().enumerate() {
192        let param_as_string = format!("{param}");
193        if entry.paramname_indexes[i] == 0 {
194            // Anonymous parameter
195            anon_params.push(param_as_string);
196        } else {
197            // Named parameter
198            let name = all_interned_strings[entry.paramname_indexes[i] as usize].clone();
199            named_params.insert(name, param_as_string);
200        }
201    }
202    format_logline(
203        entry.time,
204        entry.level,
205        format_string,
206        &anon_params,
207        &named_params,
208    )
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn test_log_level_ordering() {
217        assert!(CuLogLevel::Critical > CuLogLevel::Error);
218        assert!(CuLogLevel::Error > CuLogLevel::Warning);
219        assert!(CuLogLevel::Warning > CuLogLevel::Info);
220        assert!(CuLogLevel::Info > CuLogLevel::Debug);
221
222        assert!(CuLogLevel::Debug < CuLogLevel::Info);
223        assert!(CuLogLevel::Info < CuLogLevel::Warning);
224        assert!(CuLogLevel::Warning < CuLogLevel::Error);
225        assert!(CuLogLevel::Error < CuLogLevel::Critical);
226    }
227
228    #[test]
229    fn test_log_level_enabled() {
230        // When min level is Debug (0), all logs are enabled
231        assert!(CuLogLevel::Debug.enabled(CuLogLevel::Debug));
232        assert!(CuLogLevel::Info.enabled(CuLogLevel::Debug));
233        assert!(CuLogLevel::Warning.enabled(CuLogLevel::Debug));
234        assert!(CuLogLevel::Error.enabled(CuLogLevel::Debug));
235        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Debug));
236
237        // When min level is Info (1), only Info and above are enabled
238        assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Info));
239        assert!(CuLogLevel::Info.enabled(CuLogLevel::Info));
240        assert!(CuLogLevel::Warning.enabled(CuLogLevel::Info));
241        assert!(CuLogLevel::Error.enabled(CuLogLevel::Info));
242        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Info));
243
244        // When min level is Warning (2), only Warning and above are enabled
245        assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Warning));
246        assert!(!CuLogLevel::Info.enabled(CuLogLevel::Warning));
247        assert!(CuLogLevel::Warning.enabled(CuLogLevel::Warning));
248        assert!(CuLogLevel::Error.enabled(CuLogLevel::Warning));
249        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Warning));
250
251        // When min level is Error (3), only Error and above are enabled
252        assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Error));
253        assert!(!CuLogLevel::Info.enabled(CuLogLevel::Error));
254        assert!(!CuLogLevel::Warning.enabled(CuLogLevel::Error));
255        assert!(CuLogLevel::Error.enabled(CuLogLevel::Error));
256        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Error));
257
258        // When min level is Critical (4), only Critical is enabled
259        assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Critical));
260        assert!(!CuLogLevel::Info.enabled(CuLogLevel::Critical));
261        assert!(!CuLogLevel::Warning.enabled(CuLogLevel::Critical));
262        assert!(!CuLogLevel::Error.enabled(CuLogLevel::Critical));
263        assert!(CuLogLevel::Critical.enabled(CuLogLevel::Critical));
264    }
265
266    #[test]
267    fn test_cu_log_entry_with_level() {
268        let entry = CuLogEntry::new(42, CuLogLevel::Warning);
269        assert_eq!(entry.level, CuLogLevel::Warning);
270        assert_eq!(entry.msg_index, 42);
271    }
272}