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