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 pub use std::collections::HashMap;
28 pub use std::fmt::Formatter;
29 pub use std::fmt::Result as FmtResult;
30 pub use strfmt::strfmt;
32}
33
34use imp::*;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
38pub enum CuLogLevel {
39 Debug = 0,
41 Info = 1,
43 Warning = 2,
45 Error = 3,
47 Critical = 4,
49}
50
51impl CuLogLevel {
52 #[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#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Encode, Decode)]
70pub struct CuLogOrigin {
71 pub culistid: Option<u64>,
73 pub component_id: Option<u32>,
75 pub task_index: Option<u32>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct CuLogEntry {
82 pub time: CuTime,
84
85 pub level: CuLogLevel,
87
88 pub origin: CuLogOrigin,
90
91 pub msg_index: u32,
93
94 pub paramname_indexes: SmallVec<[u32; MAX_LOG_PARAMS_ON_STACK]>,
96
97 pub params: SmallVec<[Value; MAX_LOG_PARAMS_ON_STACK]>,
99}
100
101impl Encode for CuLogEntry {
102 fn encode<E: bincode::enc::Encoder>(
103 &self,
104 encoder: &mut E,
105 ) -> Result<(), bincode::error::EncodeError> {
106 self.time.encode(encoder)?;
107 (self.level as u8).encode(encoder)?;
108 self.origin.encode(encoder)?;
109 self.msg_index.encode(encoder)?;
110
111 (self.paramname_indexes.len() as u64).encode(encoder)?;
112 for &index in &self.paramname_indexes {
113 index.encode(encoder)?;
114 }
115
116 (self.params.len() as u64).encode(encoder)?;
117 for param in &self.params {
118 param.encode(encoder)?;
119 }
120
121 Ok(())
122 }
123}
124
125impl<Context> Decode<Context> for CuLogEntry {
126 fn decode<D: bincode::de::Decoder>(
127 decoder: &mut D,
128 ) -> Result<Self, bincode::error::DecodeError> {
129 let time = CuTime::decode(decoder)?;
130 let level_raw = u8::decode(decoder)?;
131 let level = match level_raw {
132 0 => CuLogLevel::Debug,
133 1 => CuLogLevel::Info,
134 2 => CuLogLevel::Warning,
135 3 => CuLogLevel::Error,
136 4 => CuLogLevel::Critical,
137 _ => CuLogLevel::Debug, };
139 let origin = CuLogOrigin::decode(decoder)?;
140 let msg_index = u32::decode(decoder)?;
141
142 let paramname_len = u64::decode(decoder)? as usize;
143 let mut paramname_indexes = SmallVec::with_capacity(paramname_len);
144 for _ in 0..paramname_len {
145 paramname_indexes.push(u32::decode(decoder)?);
146 }
147
148 let params_len = u64::decode(decoder)? as usize;
149 let mut params = SmallVec::with_capacity(params_len);
150 for _ in 0..params_len {
151 params.push(Value::decode(decoder)?);
152 }
153
154 Ok(CuLogEntry {
155 time,
156 level,
157 origin,
158 msg_index,
159 paramname_indexes,
160 params,
161 })
162 }
163}
164
165impl Display for CuLogEntry {
167 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
168 write!(
169 f,
170 "CuLogEntry {{ level: {:?}, origin: {:?}, msg_index: {}, paramname_indexes: {:?}, params: {:?} }}",
171 self.level, self.origin, self.msg_index, self.paramname_indexes, self.params
172 )
173 }
174}
175
176impl CuLogEntry {
177 pub fn new(msg_index: u32, level: CuLogLevel) -> Self {
179 CuLogEntry {
180 time: 0.into(), level,
183 origin: CuLogOrigin::default(),
184 msg_index,
185 paramname_indexes: SmallVec::new(),
186 params: SmallVec::new(),
187 }
188 }
189
190 pub fn set_origin(
192 &mut self,
193 culistid: Option<u64>,
194 component_id: Option<u32>,
195 task_index: Option<u32>,
196 ) {
197 self.origin = CuLogOrigin {
198 culistid,
199 component_id,
200 task_index,
201 };
202 }
203
204 pub fn add_param(&mut self, paramname_index: u32, param: Value) {
207 self.paramname_indexes.push(paramname_index);
208 self.params.push(param);
209 }
210}
211
212#[inline]
215#[cfg(feature = "std")]
216pub fn format_logline(
217 time: CuTime,
218 level: CuLogLevel,
219 format_str: &str,
220 params: &[String],
221 named_params: &HashMap<String, String>,
222) -> CuResult<String> {
223 if format_str.contains("{}") {
226 let mut formatted = format_str.to_string();
227 for param in params.iter() {
228 if !formatted.contains("{}") {
229 break;
230 }
231 formatted = formatted.replacen("{}", param, 1);
232 }
233 if !named_params.is_empty() {
234 let mut named = named_params.iter().collect::<Vec<_>>();
235 named.sort_by(|a, b| a.0.cmp(b.0));
236 for (name, value) in named {
237 if formatted.contains("{}") {
238 formatted = formatted.replacen("{}", value, 1);
239 }
240 formatted = formatted.replace(&format!("{{{name}}}"), value);
241 }
242 }
243 return Ok(format!("{time} [{level:?}]: {formatted}"));
244 }
245
246 let logline = strfmt(format_str, named_params).map_err(|e| {
248 cu29_traits::CuError::new_with_cause(
249 format!("Failed to format log line: {format_str:?} with variables [{named_params:?}]")
250 .as_str(),
251 e,
252 )
253 })?;
254 Ok(format!("{time} [{level:?}]: {logline}"))
255}
256
257#[cfg(feature = "std")]
260pub fn rebuild_logline(all_interned_strings: &[String], entry: &CuLogEntry) -> CuResult<String> {
261 let format_string = all_interned_strings
262 .get(entry.msg_index as usize)
263 .ok_or_else(|| {
264 cu29_traits::CuError::from(format!(
265 "Invalid message index {} (interned strings length {})",
266 entry.msg_index,
267 all_interned_strings.len()
268 ))
269 })?;
270 if entry.paramname_indexes.len() != entry.params.len() {
271 return Err(cu29_traits::CuError::from(format!(
272 "Mismatched parameter metadata: {} names for {} params",
273 entry.paramname_indexes.len(),
274 entry.params.len()
275 )));
276 }
277
278 let mut anon_params = Vec::with_capacity(entry.params.len());
279 let mut named_params = HashMap::with_capacity(entry.params.len());
280
281 for (i, param) in entry.params.iter().enumerate() {
282 let param_as_string = format!("{param}");
283 if entry.paramname_indexes[i] == 0 {
284 anon_params.push(param_as_string);
286 } else {
287 let name = all_interned_strings
289 .get(entry.paramname_indexes[i] as usize)
290 .ok_or_else(|| {
291 cu29_traits::CuError::from(format!(
292 "Invalid parameter name index {} (interned strings length {})",
293 entry.paramname_indexes[i],
294 all_interned_strings.len()
295 ))
296 })?
297 .clone();
298 named_params.insert(name, param_as_string);
299 }
300 }
301 format_logline(
302 entry.time,
303 entry.level,
304 format_string,
305 &anon_params,
306 &named_params,
307 )
308}
309
310#[cfg(all(feature = "defmt", not(feature = "std")))]
312#[macro_export]
313macro_rules! __cu29_defmt_debug {
314 ($fmt:literal $(, $arg:expr)* $(,)?) => {
315 ::defmt::debug!($fmt $(, $arg)*);
316 }
317}
318#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
319#[macro_export]
320macro_rules! __cu29_defmt_debug {
321 ($($tt:tt)*) => {{}};
322}
323
324#[cfg(all(feature = "defmt", not(feature = "std")))]
325#[macro_export]
326macro_rules! __cu29_defmt_info {
327 ($fmt:literal $(, $arg:expr)* $(,)?) => {
328 ::defmt::info!($fmt $(, $arg)*);
329 }
330}
331#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
332#[macro_export]
333macro_rules! __cu29_defmt_info {
334 ($($tt:tt)*) => {{}};
335}
336
337#[cfg(all(feature = "defmt", not(feature = "std")))]
338#[macro_export]
339macro_rules! __cu29_defmt_warn {
340 ($fmt:literal $(, $arg:expr)* $(,)?) => {
341 ::defmt::warn!($fmt $(, $arg)*);
342 }
343}
344#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
345#[macro_export]
346macro_rules! __cu29_defmt_warn {
347 ($($tt:tt)*) => {{}};
348}
349
350#[cfg(all(feature = "defmt", not(feature = "std")))]
351#[macro_export]
352macro_rules! __cu29_defmt_error {
353 ($fmt:literal $(, $arg:expr)* $(,)?) => {
354 ::defmt::error!($fmt $(, $arg)*);
355 }
356}
357#[cfg(not(all(feature = "defmt", not(feature = "std"))))]
358#[macro_export]
359macro_rules! __cu29_defmt_error {
360 ($($tt:tt)*) => {{}};
361}
362
363#[macro_export]
364macro_rules! defmt_debug {
365 ($($tt:tt)*) => { $crate::__cu29_defmt_debug!($($tt)*) };
366}
367
368#[macro_export]
369macro_rules! defmt_info {
370 ($($tt:tt)*) => { $crate::__cu29_defmt_info!($($tt)*) };
371}
372
373#[macro_export]
374macro_rules! defmt_warn {
375 ($($tt:tt)*) => { $crate::__cu29_defmt_warn!($($tt)*) };
376}
377
378#[macro_export]
379macro_rules! defmt_error {
380 ($($tt:tt)*) => { $crate::__cu29_defmt_error!($($tt)*) };
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386
387 #[test]
388 fn test_log_level_ordering() {
389 assert!(CuLogLevel::Critical > CuLogLevel::Error);
390 assert!(CuLogLevel::Error > CuLogLevel::Warning);
391 assert!(CuLogLevel::Warning > CuLogLevel::Info);
392 assert!(CuLogLevel::Info > CuLogLevel::Debug);
393
394 assert!(CuLogLevel::Debug < CuLogLevel::Info);
395 assert!(CuLogLevel::Info < CuLogLevel::Warning);
396 assert!(CuLogLevel::Warning < CuLogLevel::Error);
397 assert!(CuLogLevel::Error < CuLogLevel::Critical);
398 }
399
400 #[test]
401 fn test_log_level_enabled() {
402 assert!(CuLogLevel::Debug.enabled(CuLogLevel::Debug));
404 assert!(CuLogLevel::Info.enabled(CuLogLevel::Debug));
405 assert!(CuLogLevel::Warning.enabled(CuLogLevel::Debug));
406 assert!(CuLogLevel::Error.enabled(CuLogLevel::Debug));
407 assert!(CuLogLevel::Critical.enabled(CuLogLevel::Debug));
408
409 assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Info));
411 assert!(CuLogLevel::Info.enabled(CuLogLevel::Info));
412 assert!(CuLogLevel::Warning.enabled(CuLogLevel::Info));
413 assert!(CuLogLevel::Error.enabled(CuLogLevel::Info));
414 assert!(CuLogLevel::Critical.enabled(CuLogLevel::Info));
415
416 assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Warning));
418 assert!(!CuLogLevel::Info.enabled(CuLogLevel::Warning));
419 assert!(CuLogLevel::Warning.enabled(CuLogLevel::Warning));
420 assert!(CuLogLevel::Error.enabled(CuLogLevel::Warning));
421 assert!(CuLogLevel::Critical.enabled(CuLogLevel::Warning));
422
423 assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Error));
425 assert!(!CuLogLevel::Info.enabled(CuLogLevel::Error));
426 assert!(!CuLogLevel::Warning.enabled(CuLogLevel::Error));
427 assert!(CuLogLevel::Error.enabled(CuLogLevel::Error));
428 assert!(CuLogLevel::Critical.enabled(CuLogLevel::Error));
429
430 assert!(!CuLogLevel::Debug.enabled(CuLogLevel::Critical));
432 assert!(!CuLogLevel::Info.enabled(CuLogLevel::Critical));
433 assert!(!CuLogLevel::Warning.enabled(CuLogLevel::Critical));
434 assert!(!CuLogLevel::Error.enabled(CuLogLevel::Critical));
435 assert!(CuLogLevel::Critical.enabled(CuLogLevel::Critical));
436 }
437
438 #[test]
439 fn test_cu_log_entry_with_level() {
440 let entry = CuLogEntry::new(42, CuLogLevel::Warning);
441 assert_eq!(entry.level, CuLogLevel::Warning);
442 assert_eq!(entry.origin, CuLogOrigin::default());
443 assert_eq!(entry.msg_index, 42);
444 }
445
446 #[cfg(feature = "std")]
447 #[test]
448 fn test_rebuild_logline_mixes_named_and_positional_placeholders() {
449 let all_interned_strings = vec![
450 "File closed after hash was calculated Hash: {hash}, size: {size};\n{}".to_string(),
451 "hash".to_string(),
452 "size".to_string(),
453 ];
454 let mut entry = CuLogEntry::new(0, CuLogLevel::Debug);
455 entry.add_param(ANONYMOUS, Value::String("event payload".to_string()));
456 entry.add_param(1, Value::String("0x000000000".to_string()));
457 entry.add_param(2, Value::U64(420));
458
459 let line = rebuild_logline(&all_interned_strings, &entry).unwrap();
460
461 assert_eq!(
462 line,
463 "0 ns [Debug]: File closed after hash was calculated Hash: 0x000000000, size: 420;\nevent payload"
464 );
465 }
466}