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/// The name of the directory where the log index is stored.
13const INDEX_DIR_NAME: &str = "cu29_log_index";
14
15#[allow(dead_code)]
16pub const ANONYMOUS: u32 = 0;
17
18pub const MAX_LOG_PARAMS_ON_STACK: usize = 10;
19
20/// This is the basic structure for a log entry in Copper.
21#[derive(Debug, Serialize, Deserialize, PartialEq)]
22pub struct CuLogEntry {
23    // Approximate time when the log entry was created.
24    pub time: CuTime,
25
26    // interned index of the message
27    pub msg_index: u32,
28
29    // interned indexes of the parameter names
30    pub paramname_indexes: SmallVec<[u32; MAX_LOG_PARAMS_ON_STACK]>,
31
32    // Serializable values for the parameters (Values are acting like an Any Value).
33    pub params: SmallVec<[Value; MAX_LOG_PARAMS_ON_STACK]>,
34}
35
36impl Encode for CuLogEntry {
37    fn encode<E: bincode::enc::Encoder>(
38        &self,
39        encoder: &mut E,
40    ) -> Result<(), bincode::error::EncodeError> {
41        self.time.encode(encoder)?;
42        self.msg_index.encode(encoder)?;
43
44        (self.paramname_indexes.len() as u64).encode(encoder)?;
45        for &index in &self.paramname_indexes {
46            index.encode(encoder)?;
47        }
48
49        (self.params.len() as u64).encode(encoder)?;
50        for param in &self.params {
51            param.encode(encoder)?;
52        }
53
54        Ok(())
55    }
56}
57
58impl<Context> Decode<Context> for CuLogEntry {
59    fn decode<D: bincode::de::Decoder>(
60        decoder: &mut D,
61    ) -> Result<Self, bincode::error::DecodeError> {
62        let time = CuTime::decode(decoder)?;
63        let msg_index = u32::decode(decoder)?;
64
65        let paramname_len = u64::decode(decoder)? as usize;
66        let mut paramname_indexes = SmallVec::with_capacity(paramname_len);
67        for _ in 0..paramname_len {
68            paramname_indexes.push(u32::decode(decoder)?);
69        }
70
71        let params_len = u64::decode(decoder)? as usize;
72        let mut params = SmallVec::with_capacity(params_len);
73        for _ in 0..params_len {
74            params.push(Value::decode(decoder)?);
75        }
76
77        Ok(CuLogEntry {
78            time,
79            msg_index,
80            paramname_indexes,
81            params,
82        })
83    }
84}
85
86// This is for internal debug purposes.
87impl Display for CuLogEntry {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(
90            f,
91            "CuLogEntry {{ msg_index: {}, paramname_indexes: {:?}, params: {:?} }}",
92            self.msg_index, self.paramname_indexes, self.params
93        )
94    }
95}
96
97impl CuLogEntry {
98    /// msg_index is the interned index of the message.
99    pub fn new(msg_index: u32) -> Self {
100        CuLogEntry {
101            time: 0.into(), // We have no clock at that point it is called from random places
102            // the clock will be set at actual log time from clock source provided
103            msg_index,
104            paramname_indexes: SmallVec::new(),
105            params: SmallVec::new(),
106        }
107    }
108
109    /// Add a parameter to the log entry.
110    /// paramname_index is the interned index of the parameter name.
111    pub fn add_param(&mut self, paramname_index: u32, param: Value) {
112        self.paramname_indexes.push(paramname_index);
113        self.params.push(param);
114    }
115}
116
117/// Text log line formatter.
118#[inline]
119pub fn format_logline(
120    time: CuTime,
121    format_str: &str,
122    params: &[String],
123    named_params: &HashMap<String, String>,
124) -> CuResult<String> {
125    let mut format_str = format_str.to_string();
126
127    for param in params.iter() {
128        format_str = format_str.replacen("{}", param, 1);
129    }
130
131    if named_params.is_empty() {
132        return Ok(format_str);
133    }
134
135    let logline = strfmt(&format_str, named_params).map_err(|e| {
136        CuError::new_with_cause(
137            format!("Failed to format log line: {format_str:?} with variables [{named_params:?}]")
138                .as_str(),
139            e,
140        )
141    })?;
142    Ok(format!("{time}: {logline}"))
143}
144
145/// Rebuild a log line from the interned strings and the CuLogEntry.
146/// This basically translates the world of copper logs to text logs.
147pub fn rebuild_logline(all_interned_strings: &[String], entry: &CuLogEntry) -> CuResult<String> {
148    let format_string = &all_interned_strings[entry.msg_index as usize];
149    let mut anon_params: Vec<String> = Vec::new();
150    let mut named_params = HashMap::new();
151
152    for (i, param) in entry.params.iter().enumerate() {
153        let param_as_string = format!("{param}");
154        if entry.paramname_indexes[i] == 0 {
155            // Anonymous parameter
156            anon_params.push(param_as_string);
157        } else {
158            // Named parameter
159            let name = all_interned_strings[entry.paramname_indexes[i] as usize].clone();
160            named_params.insert(name, param_as_string);
161        }
162    }
163    format_logline(entry.time, format_string, &anon_params, &named_params)
164}
165
166fn parent_n_times(path: &Path, n: usize) -> Option<PathBuf> {
167    let mut result = Some(path.to_path_buf());
168    for _ in 0..n {
169        result = result?.parent().map(PathBuf::from);
170    }
171    result
172}
173
174/// Convenience function to returns the default path for the log index directory.
175pub fn default_log_index_dir() -> PathBuf {
176    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.");
177    let outdir_path = Path::new(&outdir);
178
179    parent_n_times(outdir_path, 3).unwrap().join(INDEX_DIR_NAME)
180}