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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14pub enum CuLogLevel {
15 Debug = 0,
17 Info = 1,
19 Warning = 2,
21 Error = 3,
23 Critical = 4,
25}
26
27impl CuLogLevel {
28 #[inline]
34 pub const fn enabled(self, max_level: CuLogLevel) -> bool {
35 self as u8 >= max_level as u8
36 }
37}
38
39const 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#[derive(Debug, Serialize, Deserialize, PartialEq)]
49pub struct CuLogEntry {
50 pub time: CuTime,
52
53 pub level: CuLogLevel,
55
56 pub msg_index: u32,
58
59 pub paramname_indexes: SmallVec<[u32; MAX_LOG_PARAMS_ON_STACK]>,
61
62 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, };
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
127impl 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 pub fn new(msg_index: u32, level: CuLogLevel) -> Self {
141 CuLogEntry {
142 time: 0.into(), level,
145 msg_index,
146 paramname_indexes: SmallVec::new(),
147 params: SmallVec::new(),
148 }
149 }
150
151 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#[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
188pub 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 anon_params.push(param_as_string);
200 } else {
201 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
223pub 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 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 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 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 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 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}