1#[cfg(feature = "std")]
38extern crate std;
39
40use core::sync::atomic::{AtomicBool, AtomicU8, Ordering};
41
42use alloc::borrow::Cow;
43use alloc::boxed::Box;
44use alloc::collections::VecDeque;
45use alloc::string::{String, ToString};
46use alloc::sync::Arc;
47use alloc::vec::Vec;
48
49use spin::Mutex;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
56#[repr(u8)]
57pub enum LogLevel {
58 Off = 0,
60 Error = 1,
62 Warn = 2,
64 Fixme = 3,
66 Info = 4,
68 Debug = 5,
70 Log = 6,
72 Trace = 7,
74}
75
76impl LogLevel {
77 pub fn as_str(self) -> &'static str {
79 match self {
80 LogLevel::Off => "OFF",
81 LogLevel::Error => "ERROR",
82 LogLevel::Warn => "WARN",
83 LogLevel::Fixme => "FIXME",
84 LogLevel::Info => "INFO",
85 LogLevel::Debug => "DEBUG",
86 LogLevel::Log => "LOG",
87 LogLevel::Trace => "TRACE",
88 }
89 }
90
91 pub fn parse(s: &str) -> Option<LogLevel> {
94 let s = s.trim();
95 if let Ok(n) = s.parse::<u8>() {
96 return Self::from_u8(n);
97 }
98 Some(match () {
99 _ if s.eq_ignore_ascii_case("off") || s.eq_ignore_ascii_case("none") => LogLevel::Off,
100 _ if s.eq_ignore_ascii_case("error") => LogLevel::Error,
101 _ if s.eq_ignore_ascii_case("warn") || s.eq_ignore_ascii_case("warning") => {
102 LogLevel::Warn
103 }
104 _ if s.eq_ignore_ascii_case("fixme") => LogLevel::Fixme,
105 _ if s.eq_ignore_ascii_case("info") => LogLevel::Info,
106 _ if s.eq_ignore_ascii_case("debug") => LogLevel::Debug,
107 _ if s.eq_ignore_ascii_case("log") => LogLevel::Log,
108 _ if s.eq_ignore_ascii_case("trace") => LogLevel::Trace,
109 _ => return None,
110 })
111 }
112
113 pub fn from_u8(n: u8) -> Option<LogLevel> {
115 Some(match n {
116 0 => LogLevel::Off,
117 1 => LogLevel::Error,
118 2 => LogLevel::Warn,
119 3 => LogLevel::Fixme,
120 4 => LogLevel::Info,
121 5 => LogLevel::Debug,
122 6 => LogLevel::Log,
123 7 => LogLevel::Trace,
124 _ => return None,
125 })
126 }
127}
128
129pub fn short_type_name<T: ?Sized>() -> &'static str {
134 let full = core::any::type_name::<T>();
135 let base = full.split_once('<').map_or(full, |(head, _)| head);
139 match base.rsplit("::").next() {
140 Some(s) if !s.is_empty() => s,
141 _ => base,
142 }
143}
144
145pub trait LogSource {
149 fn log_category(&self) -> &'static str;
151 fn log_instance(&self) -> Option<&str> {
154 None
155 }
156 fn log_category_override(&self) -> Option<&str> {
161 None
162 }
163}
164
165#[derive(Debug, Default, Clone, PartialEq, Eq)]
170pub struct LogName {
171 instance: Option<String>,
172 category: Option<String>,
173}
174
175impl LogName {
176 pub const fn new() -> Self {
177 Self {
178 instance: None,
179 category: None,
180 }
181 }
182
183 pub fn set_instance(&mut self, name: String) {
185 self.instance = Some(name);
186 }
187
188 pub fn set_category(&mut self, category: String) {
190 self.category = Some(category);
191 }
192
193 pub fn instance(&self) -> Option<&str> {
194 self.instance.as_deref()
195 }
196
197 pub fn category(&self) -> Option<&str> {
198 self.category.as_deref()
199 }
200}
201
202#[derive(Debug, Clone, Copy)]
205pub struct Target<'a> {
206 pub category: &'static str,
207 pub instance: Option<&'a str>,
208}
209
210impl<'a> Target<'a> {
211 pub fn named(category: &'static str, instance: &'a str) -> Self {
213 Self {
214 category,
215 instance: Some(instance),
216 }
217 }
218
219 pub fn category(category: &'static str) -> Self {
221 Self {
222 category,
223 instance: None,
224 }
225 }
226}
227
228impl LogSource for Target<'_> {
229 fn log_category(&self) -> &'static str {
230 self.category
231 }
232 fn log_instance(&self) -> Option<&str> {
233 self.instance
234 }
235}
236
237#[derive(Debug, Default)]
242pub struct InstanceNamer {
243 counts: Vec<(&'static str, u32)>,
244}
245
246impl InstanceNamer {
247 pub fn new() -> Self {
248 Self::default()
249 }
250
251 pub fn add(&mut self, category: &'static str, explicit: Option<&str>) -> String {
256 let name = match explicit {
257 Some(n) => String::from(n),
258 None => {
259 let n = match self.counts.iter_mut().find(|(c, _)| *c == category) {
260 Some(e) => {
261 let v = e.1;
262 e.1 += 1;
263 v
264 }
265 None => {
266 self.counts.push((category, 1));
267 0
268 }
269 };
270 alloc::format!("{category}{n}")
271 }
272 };
273 crate::g2g_info!(Target::named(category, &name), "added to pipeline");
274 name
275 }
276}
277
278impl<T: LogSource + ?Sized> LogSource for &T {
282 fn log_category(&self) -> &'static str {
283 (**self).log_category()
284 }
285 fn log_instance(&self) -> Option<&str> {
286 (**self).log_instance()
287 }
288 fn log_category_override(&self) -> Option<&str> {
289 (**self).log_category_override()
290 }
291}
292
293impl<T: LogSource + ?Sized> LogSource for &mut T {
294 fn log_category(&self) -> &'static str {
295 (**self).log_category()
296 }
297 fn log_instance(&self) -> Option<&str> {
298 (**self).log_instance()
299 }
300 fn log_category_override(&self) -> Option<&str> {
301 (**self).log_category_override()
302 }
303}
304
305#[derive(Debug, Clone, PartialEq)]
310pub enum LogValue<'a> {
311 Str(Cow<'a, str>),
312 Int(i64),
313 Uint(u64),
314 Float(f64),
315 Bool(bool),
316}
317
318impl LogValue<'_> {
319 pub fn into_owned(self) -> LogValue<'static> {
322 match self {
323 LogValue::Str(s) => LogValue::Str(Cow::Owned(s.into_owned())),
324 LogValue::Int(v) => LogValue::Int(v),
325 LogValue::Uint(v) => LogValue::Uint(v),
326 LogValue::Float(v) => LogValue::Float(v),
327 LogValue::Bool(v) => LogValue::Bool(v),
328 }
329 }
330}
331
332impl core::fmt::Display for LogValue<'_> {
333 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
334 match self {
335 LogValue::Str(s) => f.write_str(s),
336 LogValue::Int(v) => write!(f, "{v}"),
337 LogValue::Uint(v) => write!(f, "{v}"),
338 LogValue::Float(v) => write!(f, "{v}"),
339 LogValue::Bool(v) => write!(f, "{v}"),
340 }
341 }
342}
343
344impl<'a> From<&'a str> for LogValue<'a> {
345 fn from(v: &'a str) -> Self {
346 LogValue::Str(Cow::Borrowed(v))
347 }
348}
349
350impl From<String> for LogValue<'static> {
351 fn from(v: String) -> Self {
352 LogValue::Str(Cow::Owned(v))
353 }
354}
355
356impl From<bool> for LogValue<'_> {
357 fn from(v: bool) -> Self {
358 LogValue::Bool(v)
359 }
360}
361
362macro_rules! log_value_from {
363 ($variant:ident, $($ty:ty),+) => {$(
364 impl From<$ty> for LogValue<'_> {
365 fn from(v: $ty) -> Self {
366 LogValue::$variant(v.into())
367 }
368 }
369 )+};
370}
371log_value_from!(Int, i8, i16, i32, i64);
372log_value_from!(Uint, u8, u16, u32, u64);
373log_value_from!(Float, f32, f64);
374
375impl From<usize> for LogValue<'_> {
376 fn from(v: usize) -> Self {
377 LogValue::Uint(v as u64)
378 }
379}
380
381#[derive(Debug, Clone, PartialEq)]
383pub struct LogField<'a> {
384 pub key: Cow<'a, str>,
385 pub value: LogValue<'a>,
386}
387
388impl<'a> LogField<'a> {
389 pub fn new(key: impl Into<Cow<'a, str>>, value: impl Into<LogValue<'a>>) -> Self {
390 Self {
391 key: key.into(),
392 value: value.into(),
393 }
394 }
395
396 pub fn into_owned(self) -> LogField<'static> {
398 LogField {
399 key: Cow::Owned(self.key.into_owned()),
400 value: self.value.into_owned(),
401 }
402 }
403}
404
405#[derive(Debug)]
410pub struct LogRecord<'a> {
411 pub level: LogLevel,
412 pub category: &'a str,
413 pub instance: Option<&'a str>,
414 pub timestamp_ns: Option<u64>,
417 pub fields: &'a [LogField<'a>],
418 pub message: core::fmt::Arguments<'a>,
419}
420
421impl LogRecord<'_> {
422 pub fn to_owned_record(&self) -> OwnedLogRecord {
425 OwnedLogRecord {
426 level: self.level,
427 category: self.category.to_string(),
428 instance: self.instance.map(|s| s.to_string()),
429 timestamp_ns: self.timestamp_ns,
430 fields: self
431 .fields
432 .iter()
433 .cloned()
434 .map(LogField::into_owned)
435 .collect(),
436 message: alloc::format!("{}", self.message),
437 }
438 }
439}
440
441#[derive(Debug, Clone, PartialEq)]
443pub struct OwnedLogRecord {
444 pub level: LogLevel,
445 pub category: String,
446 pub instance: Option<String>,
447 pub timestamp_ns: Option<u64>,
448 pub fields: Vec<LogField<'static>>,
449 pub message: String,
450}
451
452impl OwnedLogRecord {
453 pub fn field(&self, key: &str) -> Option<&LogValue<'static>> {
455 self.fields.iter().find(|f| f.key == key).map(|f| &f.value)
456 }
457
458 pub fn emit_to(&self, sink: &dyn LogSink) {
465 sink.emit(&LogRecord {
466 level: self.level,
467 category: &self.category,
468 instance: self.instance.as_deref(),
469 timestamp_ns: self.timestamp_ns,
470 fields: &self.fields,
471 message: format_args!("{}", self.message),
472 });
473 }
474}
475
476pub trait LogSink: Send + Sync {
479 fn emit(&self, record: &LogRecord<'_>);
480}
481
482#[derive(Debug, Clone)]
486pub struct LogConfig {
487 default: LogLevel,
488 overrides: Vec<(String, LogLevel)>,
489}
490
491impl Default for LogConfig {
492 fn default() -> Self {
493 Self::new()
494 }
495}
496
497impl LogConfig {
498 pub const fn new() -> Self {
501 Self {
502 default: LogLevel::Error,
503 overrides: Vec::new(),
504 }
505 }
506
507 pub fn level_for(&self, category: &str) -> LogLevel {
511 for (k, v) in &self.overrides {
512 if k == category {
513 return *v;
514 }
515 }
516 for (k, v) in &self.overrides {
517 if k.contains(['*', '?']) && glob_match(k, category) {
518 return *v;
519 }
520 }
521 self.default
522 }
523
524 pub fn enabled(&self, category: &str, level: LogLevel) -> bool {
526 level != LogLevel::Off && (level as u8) <= (self.level_for(category) as u8)
527 }
528
529 pub fn set_default(&mut self, level: LogLevel) {
531 self.default = level;
532 }
533
534 pub fn set_category(&mut self, category: &str, level: LogLevel) {
536 if let Some(e) = self.overrides.iter_mut().find(|(k, _)| k == category) {
537 e.1 = level;
538 } else {
539 self.overrides.push((category.to_string(), level));
540 }
541 }
542
543 pub fn parse_spec(&mut self, spec: &str) {
547 for part in spec.split(',') {
548 let part = part.trim();
549 if part.is_empty() {
550 continue;
551 }
552 match part.split_once(':') {
553 Some((name, lvl)) => {
554 if let Some(level) = LogLevel::parse(lvl) {
555 if name.trim() == "*" {
556 self.set_default(level);
557 } else {
558 self.set_category(name.trim(), level);
559 }
560 }
561 }
562 None => {
563 if let Some(level) = LogLevel::parse(part) {
564 self.set_default(level);
565 }
566 }
567 }
568 }
569 }
570
571 fn has_overrides(&self) -> bool {
572 !self.overrides.is_empty()
573 }
574}
575
576fn glob_match(pattern: &str, s: &str) -> bool {
580 let (p, t) = (pattern.as_bytes(), s.as_bytes());
581 let (mut pi, mut ti) = (0usize, 0usize);
582 let mut star: Option<(usize, usize)> = None;
583 while ti < t.len() {
584 if pi < p.len() && (p[pi] == b'?' || p[pi] == t[ti]) {
585 pi += 1;
586 ti += 1;
587 } else if pi < p.len() && p[pi] == b'*' {
588 star = Some((pi, ti));
589 pi += 1;
590 } else if let Some((sp, sm)) = star {
591 star = Some((sp, sm + 1));
593 pi = sp + 1;
594 ti = sm + 1;
595 } else {
596 return false;
597 }
598 }
599 while pi < p.len() && p[pi] == b'*' {
600 pi += 1;
601 }
602 pi == p.len()
603}
604
605static DEFAULT_LEVEL: AtomicU8 = AtomicU8::new(LogLevel::Error as u8);
608static HAS_OVERRIDES: AtomicBool = AtomicBool::new(false);
609static CONFIG: Mutex<LogConfig> = Mutex::new(LogConfig::new());
610#[allow(clippy::type_complexity)]
611static SINK: Mutex<Option<Box<dyn LogSink>>> = Mutex::new(None);
612
613fn sync_caches(cfg: &LogConfig) {
614 DEFAULT_LEVEL.store(cfg.default as u8, Ordering::Relaxed);
615 HAS_OVERRIDES.store(cfg.has_overrides(), Ordering::Relaxed);
616}
617
618pub fn enabled(category: &str, level: LogLevel) -> bool {
621 if matches!(level, LogLevel::Off) {
622 return false;
623 }
624 let lvl = level as u8;
625 if HAS_OVERRIDES.load(Ordering::Relaxed) {
626 lvl <= CONFIG.lock().level_for(category) as u8
627 } else {
628 lvl <= DEFAULT_LEVEL.load(Ordering::Relaxed)
629 }
630}
631
632pub fn emit(
635 category: &str,
636 instance: Option<&str>,
637 level: LogLevel,
638 message: core::fmt::Arguments<'_>,
639) {
640 emit_fields(category, instance, level, &[], message);
641}
642
643pub fn emit_fields(
645 category: &str,
646 instance: Option<&str>,
647 level: LogLevel,
648 fields: &[LogField<'_>],
649 message: core::fmt::Arguments<'_>,
650) {
651 if let Some(sink) = SINK.lock().as_deref() {
652 sink.emit(&LogRecord {
653 level,
654 category,
655 instance,
656 timestamp_ns: timestamp_now(),
657 fields,
658 message,
659 });
660 }
661}
662
663pub type TimeSource = fn() -> u64;
666
667static TIME_SOURCE: Mutex<Option<TimeSource>> = Mutex::new(None);
668static HAS_TIME_SOURCE: AtomicBool = AtomicBool::new(false);
670
671pub fn set_time_source(source: TimeSource) {
675 *TIME_SOURCE.lock() = Some(source);
676 HAS_TIME_SOURCE.store(true, Ordering::Relaxed);
677}
678
679pub fn timestamp_now() -> Option<u64> {
681 if !HAS_TIME_SOURCE.load(Ordering::Relaxed) {
682 return None;
683 }
684 let source = *TIME_SOURCE.lock();
685 source.map(|f| f())
686}
687
688#[cfg(feature = "std")]
690pub fn unix_time_source() -> u64 {
691 std::time::SystemTime::now()
692 .duration_since(std::time::UNIX_EPOCH)
693 .map_or(0, |d| u64::try_from(d.as_nanos()).unwrap_or(u64::MAX))
694}
695
696pub fn set_sink(sink: Box<dyn LogSink>) {
698 *SINK.lock() = Some(sink);
699}
700
701pub fn set_default_level(level: LogLevel) {
703 let mut cfg = CONFIG.lock();
704 cfg.set_default(level);
705 sync_caches(&cfg);
706}
707
708pub fn set_category_level(category: &str, level: LogLevel) {
710 let mut cfg = CONFIG.lock();
711 cfg.set_category(category, level);
712 sync_caches(&cfg);
713}
714
715pub fn configure(spec: &str) {
718 let mut cfg = CONFIG.lock();
719 cfg.parse_spec(spec);
720 sync_caches(&cfg);
721}
722
723pub fn reset() {
726 let mut cfg = CONFIG.lock();
727 *cfg = LogConfig::new();
728 sync_caches(&cfg);
729 *SINK.lock() = None;
730 *TIME_SOURCE.lock() = None;
731 HAS_TIME_SOURCE.store(false, Ordering::Relaxed);
732}
733
734#[derive(Debug, Clone)]
754pub struct RingSink {
755 inner: Arc<Mutex<Ring>>,
756}
757
758#[derive(Debug)]
759struct Ring {
760 capacity: usize,
761 records: VecDeque<OwnedLogRecord>,
762 dropped: u64,
763}
764
765impl RingSink {
766 pub fn new(capacity: usize) -> Self {
768 let capacity = capacity.max(1);
769 Self {
770 inner: Arc::new(Mutex::new(Ring {
771 capacity,
772 records: VecDeque::with_capacity(capacity),
773 dropped: 0,
774 })),
775 }
776 }
777
778 pub fn snapshot(&self) -> Vec<OwnedLogRecord> {
780 self.inner.lock().records.iter().cloned().collect()
781 }
782
783 pub fn drain(&self) -> Vec<OwnedLogRecord> {
785 self.inner.lock().records.drain(..).collect()
786 }
787
788 pub fn len(&self) -> usize {
790 self.inner.lock().records.len()
791 }
792
793 pub fn is_empty(&self) -> bool {
794 self.len() == 0
795 }
796
797 pub fn capacity(&self) -> usize {
799 self.inner.lock().capacity
800 }
801
802 pub fn overwritten(&self) -> u64 {
805 self.inner.lock().dropped
806 }
807}
808
809impl LogSink for RingSink {
810 fn emit(&self, record: &LogRecord<'_>) {
811 let mut ring = self.inner.lock();
812 if ring.records.len() == ring.capacity {
813 ring.records.pop_front();
814 ring.dropped += 1;
815 }
816 ring.records.push_back(record.to_owned_record());
817 }
818}
819
820pub const CAPS_CATEGORY: &str = "caps";
825
826pub const RUNTIME_CATEGORY: &str = "runtime";
830
831pub fn report_element_failure(name: Option<&str>, err: &crate::G2gError) {
837 let Some(name) = name.filter(|n| !n.is_empty()) else {
838 return;
839 };
840 crate::g2g_error!(
841 Target::category(RUNTIME_CATEGORY),
842 "pipeline error in {name}: {err:?}"
843 );
844}
845
846#[cfg(feature = "std")]
851pub fn io_err(e: std::io::Error) -> crate::G2gError {
852 crate::G2gError::Hardware(crate::error::HardwareError::Io(
853 e.raw_os_error().unwrap_or(0),
854 ))
855}
856
857#[cfg(feature = "std")]
861pub fn path_io_err<P: AsRef<std::path::Path>>(
862 category: &'static str,
863 verb: &str,
864 path: P,
865 e: std::io::Error,
866) -> crate::G2gError {
867 crate::g2g_error!(
868 Target::category(category),
869 "cannot {verb} {}: {e}",
870 path.as_ref().display()
871 );
872 io_err(e)
873}
874
875#[cfg(feature = "std")]
885pub fn init_from_env() {
886 set_sink(Box::new(StderrSink));
891 set_time_source(unix_time_source);
892 if let Ok(spec) = std::env::var("G2G_DEBUG") {
893 configure(&spec);
894 }
895 if let Ok(v) = std::env::var("G2G_CAPS_TRACE") {
896 let v = v.trim();
897 let enable = !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false");
898 if enable {
899 let level = match v.to_ascii_lowercase().as_str() {
901 "1" | "true" | "on" | "yes" => LogLevel::Debug,
902 other => LogLevel::parse(other)
903 .filter(|l| *l != LogLevel::Off)
904 .unwrap_or(LogLevel::Debug),
905 };
906 set_category_level(CAPS_CATEGORY, level);
907 }
908 }
909}
910
911#[cfg(feature = "std")]
915#[derive(Debug, Default)]
916pub struct StderrSink;
917
918#[cfg(feature = "std")]
919impl LogSink for StderrSink {
920 fn emit(&self, r: &LogRecord<'_>) {
921 use core::fmt::Write;
922 let mut tail = String::new();
923 for f in r.fields {
924 let _ = write!(tail, " {}={}", f.key, f.value);
925 }
926 match r.instance {
927 Some(i) => {
928 std::eprintln!(
929 "{:<5} {:<16} <{}> {}{}",
930 r.level.as_str(),
931 r.category,
932 i,
933 r.message,
934 tail
935 )
936 }
937 None => std::eprintln!(
938 "{:<5} {:<16} {}{}",
939 r.level.as_str(),
940 r.category,
941 r.message,
942 tail
943 ),
944 }
945 }
946}
947
948#[cfg(feature = "tracing")]
965#[derive(Debug, Default)]
966pub struct TracingSink;
967
968#[cfg(feature = "tracing")]
969impl LogSink for TracingSink {
970 fn emit(&self, r: &LogRecord<'_>) {
971 let category = r.category;
972 let instance = r.instance.unwrap_or("");
973 let g2g_level = r.level.as_str();
974 let message = r.message;
975 match r.level {
979 LogLevel::Error => tracing::event!(
980 target: "g2g", tracing::Level::ERROR,
981 category, instance, g2g_level, "{message}"
982 ),
983 LogLevel::Warn | LogLevel::Fixme => tracing::event!(
984 target: "g2g", tracing::Level::WARN,
985 category, instance, g2g_level, "{message}"
986 ),
987 LogLevel::Info => tracing::event!(
988 target: "g2g", tracing::Level::INFO,
989 category, instance, g2g_level, "{message}"
990 ),
991 LogLevel::Debug => tracing::event!(
992 target: "g2g", tracing::Level::DEBUG,
993 category, instance, g2g_level, "{message}"
994 ),
995 LogLevel::Log | LogLevel::Trace => tracing::event!(
996 target: "g2g", tracing::Level::TRACE,
997 category, instance, g2g_level, "{message}"
998 ),
999 LogLevel::Off => {}
1001 }
1002 }
1003}
1004
1005#[cfg(feature = "tracing")]
1012pub fn init_tracing() {
1013 set_sink(Box::new(TracingSink));
1014 set_default_level(LogLevel::Trace);
1015}
1016
1017#[doc(hidden)]
1022pub fn __log<S: LogSource + ?Sized>(src: &S, level: LogLevel, args: core::fmt::Arguments<'_>) {
1023 __log_fields(src, level, &[], args)
1024}
1025
1026#[doc(hidden)]
1028pub fn __log_fields<S: LogSource + ?Sized>(
1029 src: &S,
1030 level: LogLevel,
1031 fields: &[LogField<'_>],
1032 args: core::fmt::Arguments<'_>,
1033) {
1034 let category = match src.log_category_override() {
1037 Some(c) => c,
1038 None => src.log_category(),
1039 };
1040 if enabled(category, level) {
1041 emit_fields(category, src.log_instance(), level, fields, args);
1042 }
1043}
1044
1045#[macro_export]
1048macro_rules! g2g_log_at {
1049 ($level:expr, $src:expr, $($arg:tt)+) => {
1050 $crate::log::__log(&$src, $level, ::core::format_args!($($arg)+))
1051 };
1052}
1053
1054#[macro_export]
1060macro_rules! g2g_log_fields {
1061 ($level:expr, $src:expr, [$($k:expr => $v:expr),* $(,)?], $($arg:tt)+) => {
1062 $crate::log::__log_fields(
1063 &$src,
1064 $level,
1065 &[$($crate::log::LogField::new($k, $v)),*],
1066 ::core::format_args!($($arg)+),
1067 )
1068 };
1069}
1070
1071#[macro_export]
1073macro_rules! g2g_error {
1074 ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Error, $src, $($arg)+) };
1075}
1076#[macro_export]
1078macro_rules! g2g_warn {
1079 ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Warn, $src, $($arg)+) };
1080}
1081#[macro_export]
1083macro_rules! g2g_fixme {
1084 ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Fixme, $src, $($arg)+) };
1085}
1086#[macro_export]
1088macro_rules! g2g_info {
1089 ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Info, $src, $($arg)+) };
1090}
1091#[macro_export]
1093macro_rules! g2g_debug {
1094 ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Debug, $src, $($arg)+) };
1095}
1096#[macro_export]
1098macro_rules! g2g_log {
1099 ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Log, $src, $($arg)+) };
1100}
1101#[macro_export]
1103macro_rules! g2g_trace {
1104 ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Trace, $src, $($arg)+) };
1105}
1106
1107#[cfg(test)]
1108mod tests {
1109 use super::*;
1110 use alloc::format;
1111 use alloc::sync::Arc;
1112
1113 #[test]
1117 fn a_buffered_record_replays_into_another_sink() {
1118 let ring = RingSink::new(4);
1119 ring.emit(&LogRecord {
1120 level: LogLevel::Error,
1121 category: "FileSink",
1122 instance: Some("FileSink0"),
1123 timestamp_ns: None,
1124 fields: &[],
1125 message: format_args!("reads host memory but got a Cuda frame"),
1126 });
1127
1128 let replayed = RingSink::new(4);
1129 for record in ring.snapshot() {
1130 record.emit_to(&replayed);
1131 }
1132
1133 let out = replayed.snapshot();
1134 assert_eq!(out.len(), 1);
1135 assert_eq!(out[0].level, LogLevel::Error);
1136 assert_eq!(out[0].category, "FileSink");
1137 assert_eq!(out[0].instance.as_deref(), Some("FileSink0"));
1138 assert_eq!(out[0].message, "reads host memory but got a Cuda frame");
1139 }
1140
1141 #[test]
1142 fn short_type_name_strips_generics_and_path() {
1143 struct Inner;
1144 struct Outer<T>(core::marker::PhantomData<T>);
1145 assert_eq!(short_type_name::<Inner>(), "Inner");
1146 assert_eq!(short_type_name::<Outer<Inner>>(), "Outer");
1148 }
1149
1150 #[test]
1151 fn level_parse_accepts_names_and_numbers() {
1152 assert_eq!(LogLevel::parse("debug"), Some(LogLevel::Debug));
1153 assert_eq!(LogLevel::parse("WARNING"), Some(LogLevel::Warn));
1154 assert_eq!(LogLevel::parse("5"), Some(LogLevel::Debug));
1155 assert_eq!(LogLevel::parse("off"), Some(LogLevel::Off));
1156 assert_eq!(LogLevel::parse("nope"), None);
1157 assert_eq!(LogLevel::parse("9"), None);
1158 }
1159
1160 #[test]
1161 fn config_filters_by_category_and_default() {
1162 let mut cfg = LogConfig::new(); assert!(cfg.enabled("opusenc", LogLevel::Error));
1164 assert!(
1165 !cfg.enabled("opusenc", LogLevel::Debug),
1166 "default Error hides Debug"
1167 );
1168
1169 cfg.set_default(LogLevel::Warn);
1170 cfg.set_category("opusenc", LogLevel::Trace);
1171 assert!(cfg.enabled("opusenc", LogLevel::Trace));
1173 assert!(cfg.enabled("opusenc", LogLevel::Debug));
1174 assert!(
1175 !cfg.enabled("videoscale", LogLevel::Info),
1176 "non-overridden uses default Warn"
1177 );
1178 assert!(cfg.enabled("videoscale", LogLevel::Warn));
1179 cfg.set_category("muted", LogLevel::Off);
1181 assert!(!cfg.enabled("muted", LogLevel::Error));
1182 }
1183
1184 #[test]
1185 fn parse_spec_sets_default_and_overrides() {
1186 let mut cfg = LogConfig::new();
1187 cfg.parse_spec("*:warning,opusenc:debug, videoscale:5");
1188 assert_eq!(cfg.level_for("opusenc"), LogLevel::Debug);
1189 assert_eq!(cfg.level_for("videoscale"), LogLevel::Debug);
1190 assert_eq!(cfg.level_for("anything-else"), LogLevel::Warn);
1191 let mut c2 = LogConfig::new();
1193 c2.parse_spec("info");
1194 assert_eq!(c2.level_for("x"), LogLevel::Info);
1195 }
1196
1197 #[test]
1198 fn glob_overrides_match_categories() {
1199 let mut cfg = LogConfig::new();
1200 cfg.parse_spec("*:warning,*sink*:5,opus?nc:debug,waylandsink:error");
1201 assert_eq!(cfg.level_for("filesink"), LogLevel::Debug);
1203 assert_eq!(cfg.level_for("sinkpad"), LogLevel::Debug);
1204 assert_eq!(cfg.level_for("opusenc"), LogLevel::Debug);
1206 assert_eq!(cfg.level_for("opusnc"), LogLevel::Warn);
1207 assert_eq!(cfg.level_for("waylandsink"), LogLevel::Error);
1209 assert_eq!(cfg.level_for("videoscale"), LogLevel::Warn);
1211 }
1212
1213 #[test]
1214 fn glob_match_handles_edges() {
1215 assert!(glob_match("*", "anything"));
1216 assert!(glob_match("*", ""));
1217 assert!(glob_match("a*b*c", "a-long-b-run-c"));
1218 assert!(!glob_match("a*b*c", "a-long-b-run"));
1219 assert!(glob_match("??", "ab"));
1220 assert!(!glob_match("??", "a"));
1221 assert!(!glob_match("abc", "abd"));
1222 }
1223
1224 type CapturedRecord = (LogLevel, String, Option<String>, String);
1226 struct CaptureSink(Arc<Mutex<Vec<CapturedRecord>>>);
1228 impl LogSink for CaptureSink {
1229 fn emit(&self, r: &LogRecord<'_>) {
1230 self.0.lock().push((
1231 r.level,
1232 r.category.to_string(),
1233 r.instance.map(|s| s.to_string()),
1234 format!("{}", r.message),
1235 ));
1236 }
1237 }
1238
1239 static GLOBAL_GUARD: Mutex<()> = Mutex::new(());
1241
1242 #[test]
1243 fn macros_respect_global_filtering_and_route_to_sink() {
1244 let _g = GLOBAL_GUARD.lock();
1245 reset();
1246 let captured = Arc::new(Mutex::new(Vec::new()));
1247 set_sink(Box::new(CaptureSink(captured.clone())));
1248 configure("*:warning,opusenc:debug");
1249
1250 let enc = Target::named("opusenc", "opusenc0");
1251 let scale = Target::named("videoscale", "videoscale0");
1252
1253 g2g_debug!(enc, "encoded {} bytes", 42);
1255 g2g_debug!(scale, "scaled a frame");
1257 g2g_warn!(scale, "odd dimension");
1259
1260 let recs = captured.lock();
1261 assert_eq!(recs.len(), 2, "got: {recs:?}");
1262 assert_eq!(recs[0].0, LogLevel::Debug);
1263 assert_eq!(recs[0].1, "opusenc");
1264 assert_eq!(recs[0].2.as_deref(), Some("opusenc0"));
1265 assert_eq!(recs[0].3, "encoded 42 bytes");
1266 assert_eq!(recs[1].0, LogLevel::Warn);
1267 assert_eq!(recs[1].1, "videoscale");
1268 drop(recs);
1269 reset();
1270 }
1271
1272 struct FakeElement {
1276 name: LogName,
1277 }
1278 impl LogSource for FakeElement {
1279 fn log_category(&self) -> &'static str {
1280 "VideoFlip"
1281 }
1282 fn log_instance(&self) -> Option<&str> {
1283 self.name.instance()
1284 }
1285 fn log_category_override(&self) -> Option<&str> {
1286 self.name.category()
1287 }
1288 }
1289
1290 #[test]
1291 fn category_override_replaces_the_type_category_for_filtering() {
1292 let _g = GLOBAL_GUARD.lock();
1293 reset();
1294 let captured = Arc::new(Mutex::new(Vec::new()));
1295 set_sink(Box::new(CaptureSink(captured.clone())));
1296 configure("*:off,flip-a:debug,*-glob:info");
1299
1300 let mut plain = FakeElement {
1301 name: LogName::new(),
1302 };
1303 plain.name.set_instance(String::from("VideoFlip0"));
1304 let mut renamed = FakeElement {
1305 name: LogName::new(),
1306 };
1307 renamed.name.set_instance(String::from("VideoFlip1"));
1308 renamed.name.set_category(String::from("flip-a"));
1309 let mut globbed = FakeElement {
1310 name: LogName::new(),
1311 };
1312 globbed.name.set_category(String::from("via-glob"));
1313
1314 g2g_debug!(plain, "type category is off");
1315 g2g_debug!(renamed, "override is at debug");
1316 g2g_info!(globbed, "override matches the glob");
1317
1318 let recs = captured.lock();
1319 assert_eq!(recs.len(), 2, "got: {recs:?}");
1320 assert_eq!(recs[0].1, "flip-a");
1322 assert_eq!(recs[0].2.as_deref(), Some("VideoFlip1"));
1323 assert_eq!(recs[1].1, "via-glob");
1324 drop(recs);
1325 reset();
1326 }
1327
1328 #[test]
1329 fn structured_fields_and_timestamp_reach_the_sink() {
1330 let _g = GLOBAL_GUARD.lock();
1331 reset();
1332 let owned: Arc<Mutex<Vec<OwnedLogRecord>>> = Arc::new(Mutex::new(Vec::new()));
1333 struct OwningSink(Arc<Mutex<Vec<OwnedLogRecord>>>);
1334 impl LogSink for OwningSink {
1335 fn emit(&self, r: &LogRecord<'_>) {
1336 self.0.lock().push(r.to_owned_record());
1337 }
1338 }
1339 set_sink(Box::new(OwningSink(owned.clone())));
1340 set_time_source(|| 42);
1341 configure("*:debug");
1342
1343 let width = 1920u32;
1344 g2g_log_fields!(
1345 LogLevel::Info,
1346 Target::named("videoscale", "videoscale0"),
1347 ["width" => width, "height" => 1080u32, "format" => "NV12", "scaled" => true, "ratio" => 1.5f64],
1348 "configured {width}"
1349 );
1350
1351 let recs = owned.lock();
1352 assert_eq!(recs.len(), 1);
1353 let r = &recs[0];
1354 assert_eq!(r.field("width"), Some(&LogValue::Uint(1920)));
1357 assert_eq!(r.field("height"), Some(&LogValue::Uint(1080)));
1358 assert_eq!(
1359 r.field("format"),
1360 Some(&LogValue::Str(Cow::Borrowed("NV12")))
1361 );
1362 assert_eq!(r.field("scaled"), Some(&LogValue::Bool(true)));
1363 assert_eq!(r.field("ratio"), Some(&LogValue::Float(1.5)));
1364 assert_eq!(r.field("missing"), None);
1365 assert_eq!(r.timestamp_ns, Some(42));
1366 assert_eq!(r.message, "configured 1920");
1367 assert_eq!(r.instance.as_deref(), Some("videoscale0"));
1368 drop(recs);
1369 reset();
1370 }
1371
1372 #[test]
1373 fn ring_sink_keeps_the_newest_records_and_drains() {
1374 let _g = GLOBAL_GUARD.lock();
1375 reset();
1376 let ring = RingSink::new(3);
1377 set_sink(Box::new(ring.clone()));
1378 configure("*:debug");
1379
1380 for i in 0..5 {
1381 g2g_info!(Target::category("demo"), "record {i}");
1382 }
1383
1384 assert_eq!(ring.len(), 3, "bounded at capacity");
1385 assert_eq!(ring.capacity(), 3);
1386 assert_eq!(ring.overwritten(), 2, "two oldest were overwritten");
1387 let snap = ring.snapshot();
1388 let messages: Vec<&str> = snap.iter().map(|r| r.message.as_str()).collect();
1389 assert_eq!(messages, ["record 2", "record 3", "record 4"]);
1390 assert_eq!(ring.len(), 3);
1392 let drained = ring.drain();
1393 assert_eq!(drained.len(), 3);
1394 assert!(ring.is_empty());
1395 assert_eq!(drained[0].timestamp_ns, None);
1397
1398 g2g_info!(Target::category("demo"), "after drain");
1400 assert_eq!(ring.snapshot()[0].message, "after drain");
1401 reset();
1402 }
1403
1404 #[test]
1405 fn no_sink_drops_records_without_panic() {
1406 let _g = GLOBAL_GUARD.lock();
1407 reset();
1408 configure("*:trace");
1409 g2g_error!(Target::category("x"), "no sink, {}", "dropped");
1411 reset();
1412 }
1413
1414 #[cfg(feature = "tracing")]
1418 #[test]
1419 fn tracing_sink_forwards_records_to_subscriber() {
1420 use core::fmt::Write;
1421 use tracing::field::{Field, Visit};
1422
1423 #[derive(Default)]
1425 struct Capture {
1426 events: Mutex<Vec<String>>,
1427 }
1428 struct Recorder<'a>(&'a mut String);
1429 impl Visit for Recorder<'_> {
1430 fn record_debug(&mut self, field: &Field, value: &dyn core::fmt::Debug) {
1431 let _ = write!(self.0, "{}={:?} ", field.name(), value);
1432 }
1433 fn record_str(&mut self, field: &Field, value: &str) {
1434 let _ = write!(self.0, "{}={} ", field.name(), value);
1435 }
1436 }
1437 impl tracing::Subscriber for Capture {
1438 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
1439 true
1440 }
1441 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
1442 tracing::span::Id::from_u64(1)
1443 }
1444 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
1445 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
1446 fn event(&self, event: &tracing::Event<'_>) {
1447 let meta = event.metadata();
1448 let mut line = String::new();
1449 let _ = write!(line, "{} {} ", meta.level(), meta.target());
1450 event.record(&mut Recorder(&mut line));
1451 self.events.lock().push(line);
1452 }
1453 fn enter(&self, _: &tracing::span::Id) {}
1454 fn exit(&self, _: &tracing::span::Id) {}
1455 }
1456
1457 let _g = GLOBAL_GUARD.lock();
1458 reset();
1459 init_tracing();
1460
1461 let capture = Arc::new(Capture::default());
1462 tracing::subscriber::with_default(capture.clone(), || {
1463 let enc = Target::named("opusenc", "opusenc0");
1464 g2g_info!(enc, "encoded {} bytes", 42);
1465 g2g_fixme!(Target::category("videoscale"), "todo: odd dims");
1466 });
1467
1468 let events = capture.events.lock();
1469 assert_eq!(events.len(), 2, "got: {events:?}");
1470 assert!(events[0].contains("INFO"), "{}", events[0]);
1472 assert!(events[0].contains("category=opusenc"), "{}", events[0]);
1473 assert!(events[0].contains("instance=opusenc0"), "{}", events[0]);
1474 assert!(events[0].contains("encoded 42 bytes"), "{}", events[0]);
1475 assert!(events[1].contains("WARN"), "{}", events[1]);
1477 assert!(events[1].contains("g2g_level=FIXME"), "{}", events[1]);
1478 drop(events);
1479 reset();
1480 }
1481}