Skip to main content

g2g_core/
log.rs

1//! Element-granular logging facade (M179), the `GST_DEBUG` analog.
2//!
3//! A hand-rolled `no_std` logging layer: levels, per-category thresholds, and a
4//! pluggable sink, so an element emits a record only when its category is enabled
5//! and the record is routed wherever the host installed a sink (stderr on `std`,
6//! a UART / RTT writer on an RTOS). It pulls no external logging crate, matching
7//! the `no_std + alloc` baseline.
8//!
9//! **Categories and instances.** A log record carries a `category` (the element
10//! *type*, e.g. `"opusenc"`, the GStreamer `GST_DEBUG_CATEGORY` analog) and an
11//! optional `instance` name (the element *instance*, e.g. `"opusenc0"`, the
12//! `<object>` in a GStreamer log line). Filtering is per category; the instance
13//! is for disambiguation in the output. An element exposes both by implementing
14//! [`LogSource`]; the runner logs about an element via a [`Target`].
15//!
16//! **Filtering.** [`configure`] parses a `GST_DEBUG`-style spec
17//! (`"*:warning,opusenc:debug"`): `*:LEVEL` (or a bare `LEVEL`) sets the default
18//! threshold, `name:LEVEL` overrides one category, and a `name` with `*` / `?`
19//! wildcards overrides every matching category (`*sink*:5`; an exact override
20//! wins over a glob). A message at `level` is emitted
21//! when `level <= threshold`. The common no-override case is checked against an
22//! atomic without locking, so a disabled `g2g_trace!` in a hot loop is cheap.
23//!
24//! **Macros.** [`g2g_error!`] / [`g2g_warn!`] / [`g2g_fixme!`] / [`g2g_info!`] /
25//! [`g2g_debug!`] / [`g2g_log!`] / [`g2g_trace!`] take a [`LogSource`] then a
26//! `format_args!` message; they check the threshold *before* formatting.
27//! [`g2g_log_fields!`] adds structured [`LogField`]s a sink can render or ship
28//! without re-parsing the message.
29//!
30//! **Timestamps.** Core has no clock, so a record's `timestamp_ns` is filled
31//! from a host-installed [`set_time_source`] (on `std`, [`init_from_env`]
32//! installs the UNIX-epoch one) and is `None` otherwise.
33//!
34//! **Sinks.** [`StderrSink`] (`std`), [`TracingSink`] (`tracing` feature), and
35//! [`RingSink`], a bounded in-memory flight recorder for postmortem dumps.
36
37#[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/// Severity of a log record, ordered most-severe (`Error`) to least (`Trace`),
52/// mirroring GStreamer's debug levels (minus `MEMDUMP`). `Off` disables a
53/// category. The discriminants match GStreamer's numeric levels so a
54/// `G2G_DEBUG=opusenc:5` numeric spec reads the same.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
56#[repr(u8)]
57pub enum LogLevel {
58    /// No logging for this category.
59    Off = 0,
60    /// A fatal or recoverable error.
61    Error = 1,
62    /// A warning: something unexpected but handled.
63    Warn = 2,
64    /// A known-incomplete code path (GStreamer's `FIXME`).
65    Fixme = 3,
66    /// High-level informational lifecycle messages.
67    Info = 4,
68    /// Detailed debugging messages.
69    Debug = 5,
70    /// Very frequent messages (per-buffer scope).
71    Log = 6,
72    /// The most verbose (per-byte / per-iteration) tracing.
73    Trace = 7,
74}
75
76impl LogLevel {
77    /// The uppercase label used in a log line and accepted by [`parse`](Self::parse).
78    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    /// Parse a level from a name (case-insensitive, `WARNING` also accepted) or a
92    /// `0..=7` number, as used in a `G2G_DEBUG` spec. `None` if unrecognized.
93    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    /// The level for a numeric value `0..=7`, else `None`.
114    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
129/// The short type name of `T` (the last `::` segment of
130/// [`core::any::type_name`]), used as the default log category for an element so
131/// every element type gets a filtering key for free (e.g. `"OpusEnc"`). Still a
132/// `&'static str` (a slice into the static type name).
133pub fn short_type_name<T: ?Sized>() -> &'static str {
134    let full = core::any::type_name::<T>();
135    // Strip generic parameters first (`Foo<Bar>` -> `Foo`); otherwise the last
136    // `::` segment is the parameter's path tail (e.g. `SystemClock>`), not the
137    // element type's own name.
138    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
145/// A thing that can be logged about: its [`category`](Self::log_category) (type)
146/// and optional [`instance`](Self::log_instance) name. Elements implement this so
147/// the logging macros pick up both from `self`; the runner uses [`Target`].
148pub trait LogSource {
149    /// The element type's category, e.g. `"opusenc"`, the filtering key.
150    fn log_category(&self) -> &'static str;
151    /// The element instance name, e.g. `"opusenc0"`, for the log line. Default
152    /// none (filtering is by category regardless).
153    fn log_instance(&self) -> Option<&str> {
154        None
155    }
156    /// A per-instance category override (M845), replacing the type category for
157    /// *both* filtering and output, so `G2G_DEBUG=my-cat:debug` (and globs) key
158    /// off the override. Default none: the type name is the category. An
159    /// element stores one in a [`LogName`] and returns it here.
160    fn log_category_override(&self) -> Option<&str> {
161        None
162    }
163}
164
165/// The per-instance log identity an element stores when it logs about itself:
166/// the runner-assigned instance name plus an optional category override.
167/// Elements hold one, feed it from `set_instance_name` / `set_log_category`, and
168/// return its two accessors from their [`LogSource`] impl.
169#[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    /// Store the runner-assigned instance name.
184    pub fn set_instance(&mut self, name: String) {
185        self.instance = Some(name);
186    }
187
188    /// Override the log category for this instance.
189    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/// A standalone [`LogSource`] for logging about a named element from outside it
203/// (the runner naming `<category>N`), or for an ad-hoc log site.
204#[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    /// A target with a category and an instance name.
212    pub fn named(category: &'static str, instance: &'a str) -> Self {
213        Self {
214            category,
215            instance: Some(instance),
216        }
217    }
218
219    /// A target with only a category (no instance name).
220    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/// Per-category instance counter, shared by every runner so an element is named
238/// and logged the same way whichever one drives it (M842). Hands out
239/// `<category>N` names (the GStreamer `videotestsrc0` convention) and emits the
240/// "added to pipeline" lifecycle line.
241#[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    /// Name an element instance and log its addition, returning the name.
252    /// `explicit` is a launch line's `name=`: it is taken verbatim and, as in
253    /// gst-launch, does not consume a number, so auto-named siblings of the same
254    /// category keep counting from 0.
255    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
278// Forward through references so the logging macros accept `self` (a `&Self` or
279// `&mut Self` inside a method) and `&target` uniformly: the macro passes `&$src`
280// and type inference picks the right blanket.
281impl<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/// A structured value on a log record (M845): the scalar kinds a sink can
306/// render or ship (JSON, a tracing field) without knowing the log site. `Str`
307/// borrows at the site, so a record a sink drops allocates nothing; the owned
308/// form comes from [`into_owned`](Self::into_owned).
309#[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    /// Detach from the log site so the value can outlive it (what [`RingSink`]
320    /// stores).
321    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/// One structured key/value on a log record.
382#[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    /// Detach from the log site (see [`LogValue::into_owned`]).
397    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/// One log record handed to a [`LogSink`]. The message is `format_args!` so a
406/// sink that drops the record (or buffers selectively) pays no formatting cost;
407/// `fields` carries the same information structured, so a sink renders or ships
408/// it without re-parsing the message.
409#[derive(Debug)]
410pub struct LogRecord<'a> {
411    pub level: LogLevel,
412    pub category: &'a str,
413    pub instance: Option<&'a str>,
414    /// Nanoseconds from the installed [`set_time_source`], `None` when the host
415    /// installed none (the `no_std` default: core reads no clock itself).
416    pub timestamp_ns: Option<u64>,
417    pub fields: &'a [LogField<'a>],
418    pub message: core::fmt::Arguments<'a>,
419}
420
421impl LogRecord<'_> {
422    /// Copy the record (message formatted, fields and names owned) so it can be
423    /// buffered past the log site, as [`RingSink`] does.
424    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/// An owned [`LogRecord`], what a buffering sink stores and hands back.
442#[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    /// The value of one structured field by key, `None` if absent.
454    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    /// Hand a buffered record to `sink`, the reverse of
459    /// [`to_owned_record`](LogRecord::to_owned_record). For replaying what a
460    /// buffering sink captured once a real destination exists: the TUI diverts
461    /// logging into a ring for its log pane, and the pane goes away with the
462    /// alternate screen, so anything explaining a failure has to be played back
463    /// onto stderr afterwards or it is lost.
464    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
476/// A destination for log records. The host installs one via [`set_sink`]; without
477/// one, records are dropped. `Send + Sync` so it lives in a global behind a lock.
478pub trait LogSink: Send + Sync {
479    fn emit(&self, record: &LogRecord<'_>);
480}
481
482/// The mutable filter configuration: a default threshold plus per-category
483/// overrides. Pure (no globals), so it is unit-testable in isolation; the process
484/// global is a thin wrapper over one of these.
485#[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    /// A config defaulting every category to `Error` (errors always surface; the
499    /// host raises the level to see more).
500    pub const fn new() -> Self {
501        Self {
502            default: LogLevel::Error,
503            overrides: Vec::new(),
504        }
505    }
506
507    /// The effective threshold for `category`: an exact override, else the first
508    /// matching glob override (`*` / `?` wildcards, e.g. `G2G_DEBUG=*sink*:5`),
509    /// else the default. Exact wins over glob regardless of spec order.
510    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    /// Whether a `level` message in `category` should be emitted.
525    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    /// Set the default threshold (the `*:LEVEL` of a spec).
530    pub fn set_default(&mut self, level: LogLevel) {
531        self.default = level;
532    }
533
534    /// Override (or add) one category's threshold.
535    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    /// Apply a `GST_DEBUG`-style spec: comma-separated `name:LEVEL` entries, with
544    /// `*:LEVEL` or a bare `LEVEL` setting the default. Unparseable entries are
545    /// skipped.
546    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
576/// Minimal glob over ASCII category names: `*` matches any run (including
577/// empty), `?` exactly one byte. Iterative with single-star backtracking, so a
578/// pathological pattern cannot recurse.
579fn 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            // Backtrack: let the last `*` consume one more byte.
592            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
605// Process-global filter + sink. `DEFAULT_LEVEL` / `HAS_OVERRIDES` mirror `CONFIG`
606// so the common no-override `enabled` check reads an atomic without locking.
607static 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
618/// Whether a `level` message in `category` is enabled by the global config. The
619/// macros call this before formatting; a hot disabled site costs one atomic load.
620pub 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
632/// Emit a record to the installed sink (no-op if none). Called by the macros
633/// after the [`enabled`] check; a direct caller should gate on [`enabled`] too.
634pub 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
643/// [`emit`] with structured fields attached (M845).
644pub 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
663/// A nanosecond timestamp source the host installs (see [`set_time_source`]).
664/// Any epoch, as long as it is consistent: a sink reports it verbatim.
665pub type TimeSource = fn() -> u64;
666
667static TIME_SOURCE: Mutex<Option<TimeSource>> = Mutex::new(None);
668// Mirrors `TIME_SOURCE.is_some()` so the unset case skips the lock.
669static HAS_TIME_SOURCE: AtomicBool = AtomicBool::new(false);
670
671/// Install the clock that stamps records. Core reads no clock of its own (the
672/// `no_std` baseline has none), so without this every record's `timestamp_ns`
673/// is `None`. On `std`, [`init_from_env`] installs [`unix_time_source`].
674pub fn set_time_source(source: TimeSource) {
675    *TIME_SOURCE.lock() = Some(source);
676    HAS_TIME_SOURCE.store(true, Ordering::Relaxed);
677}
678
679/// The current timestamp from the installed source, `None` if none.
680pub 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/// Nanoseconds since the UNIX epoch, the `std` [`TimeSource`].
689#[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
696/// Install (replace) the global log sink. Without one, records are dropped.
697pub fn set_sink(sink: Box<dyn LogSink>) {
698    *SINK.lock() = Some(sink);
699}
700
701/// Set the global default threshold (applies to categories with no override).
702pub fn set_default_level(level: LogLevel) {
703    let mut cfg = CONFIG.lock();
704    cfg.set_default(level);
705    sync_caches(&cfg);
706}
707
708/// Override one category's global threshold.
709pub 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
715/// Apply a `GST_DEBUG`-style spec to the global config (see
716/// [`LogConfig::parse_spec`]).
717pub fn configure(spec: &str) {
718    let mut cfg = CONFIG.lock();
719    cfg.parse_spec(spec);
720    sync_caches(&cfg);
721}
722
723/// Reset the global config to defaults and remove the sink and time source
724/// (for tests).
725pub 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/// A bounded in-memory [`LogSink`] (M845), the flight recorder: it keeps the
735/// most recent `capacity` records and overwrites the oldest, so a postmortem
736/// dump on a target with no live log stream (an RTOS board, a crashed field
737/// unit) still shows what led up to the fault. `no_std + alloc`: records are
738/// stored owned (see [`OwnedLogRecord`]), so the buffer's memory is bounded by
739/// capacity times record size, not by run length.
740///
741/// Cloning shares one buffer: install a clone as the sink and keep the original
742/// to [`snapshot`](Self::snapshot) or [`drain`](Self::drain) it.
743///
744/// ```
745/// # use g2g_core::log::{self, LogLevel, RingSink, Target};
746/// # use g2g_core::g2g_error;
747/// let ring = RingSink::new(64);
748/// log::set_sink(Box::new(ring.clone()));
749/// log::set_default_level(LogLevel::Warn);
750/// g2g_error!(Target::category("demo"), "boom");
751/// assert_eq!(ring.drain().len(), 1);
752/// ```
753#[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    /// A recorder holding at most `capacity` records (clamped to at least one).
767    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    /// Copy the buffered records, oldest first, leaving them in place.
779    pub fn snapshot(&self) -> Vec<OwnedLogRecord> {
780        self.inner.lock().records.iter().cloned().collect()
781    }
782
783    /// Take the buffered records, oldest first, emptying the buffer.
784    pub fn drain(&self) -> Vec<OwnedLogRecord> {
785        self.inner.lock().records.drain(..).collect()
786    }
787
788    /// Records currently buffered.
789    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    /// The buffer's fixed capacity.
798    pub fn capacity(&self) -> usize {
799        self.inner.lock().capacity
800    }
801
802    /// How many records the recorder has overwritten since it was created: a
803    /// non-zero count means the dump is a tail, not the whole run.
804    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
820/// The reserved log category the caps-negotiation explainer emits under
821/// (DESIGN.md 4.20a). Not an element type: it names the solver's narration, so
822/// `G2G_DEBUG=caps:debug` (or the `G2G_CAPS_TRACE` shortcut) turns it on
823/// independent of element logging.
824pub const CAPS_CATEGORY: &str = "caps";
825
826/// The reserved log category the runners emit under. Not an element type: it
827/// names the runner's own narration, notably which element instance raised the
828/// error that ended a run.
829pub const RUNTIME_CATEGORY: &str = "runtime";
830
831/// Name the element instance whose arm returned `err`, at error level (so it
832/// prints without `G2G_DEBUG`). The runners call this at the one point they pick
833/// a run's reported error, since [`G2gError`](crate::G2gError) itself carries no
834/// element identity. An unnamed arm (a plain broadcast tee, the coordinator)
835/// stays quiet: the caller already prints the error.
836pub 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/// Map a filesystem / device I/O failure onto [`G2gError`](crate::G2gError),
847/// which carries an errno rather than the `io::Error` itself. The single mapping
848/// every path-opening element and the flight-recorder dump share; prefer
849/// [`path_io_err`], which also says which path failed.
850#[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/// [`io_err`] plus an error log naming the file and the OS message. The errno in
858/// `Hardware(Io)` alone does not say which path failed or what went wrong, so
859/// every element that opens a path reports through here.
860#[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/// Install the stderr sink and apply logging from the environment. The sink is
876/// always installed, so ERROR-level diagnostics print by default; the
877/// `G2G_DEBUG` environment variable (a `GST_DEBUG`-style spec) tunes thresholds
878/// up from the default Error level. Also honors `G2G_CAPS_TRACE`
879/// as a shortcut for the caps explainer: a boolean-ish value (`1` / `true` / `on`
880/// / `yes`) raises the [`CAPS_CATEGORY`] to `Debug`, or a level name / number
881/// (`debug`, `trace`, `7`) sets that verbosity, installing the stderr sink if
882/// `G2G_DEBUG` did not. Call once at startup; the `g2g-launch` / `g2g-inspect`
883/// binaries and apps invoke it.
884#[cfg(feature = "std")]
885pub fn init_from_env() {
886    // Always install the stderr sink so ERROR-level diagnostics (notably the
887    // caps-negotiation narration, which already runs on every failed solve) are
888    // visible by default without opting in. The default threshold is Error
889    // (LogConfig::new), so a normal run stays quiet; G2G_DEBUG only tunes it up.
890    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            // A bare on-switch means Debug; a level name / number tunes it.
900            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/// A [`LogSink`] that writes one line per record to stderr, in the shape
912/// `LEVEL category <instance> message [k=v ...]` (the `<instance>` omitted when
913/// unnamed, the `k=v` tail only when the record carries structured fields).
914#[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/// A [`LogSink`] that forwards each record to the [`tracing`] crate, so a host
949/// running a `tracing` subscriber (fmt, journald, OTLP / Jaeger, tokio-console)
950/// receives g2g's logs in its existing observability pipeline. The g2g element
951/// *category* and *instance* are emitted as `tracing` fields under a fixed
952/// `g2g` target, and the message is forwarded lazily (it is only formatted if
953/// the subscriber enables the event).
954///
955/// **Level mapping.** `tracing` has five levels to g2g's seven, so two pairs
956/// collapse: `Fixme` maps to `WARN` and `Log` maps to `TRACE`. The original g2g
957/// level is preserved verbatim in the `g2g_level` field, so nothing is lost,
958/// the subscriber can still distinguish `FIXME` from `WARN`.
959///
960/// **Filtering.** With this sink installed, let the `tracing` subscriber own
961/// filtering (e.g. `RUST_LOG=g2g=debug`) rather than g2g's per-category
962/// thresholds, by raising g2g's default to pass everything through.
963/// [`init_tracing`] does exactly that.
964#[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        // `tracing::event!` needs a const level and target, so dispatch per
976        // level. The message is passed as `format_args!`, so tracing formats it
977        // only when the event is enabled by the subscriber.
978        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            // `emit` is only reached for an enabled (non-`Off`) record.
1000            LogLevel::Off => {}
1001        }
1002    }
1003}
1004
1005/// Route g2g's logging into the `tracing` ecosystem: install [`TracingSink`] and
1006/// raise the g2g default threshold to `Trace` so g2g stops filtering and the
1007/// installed `tracing` subscriber owns verbosity (e.g. `RUST_LOG=g2g=debug`).
1008/// Call once at startup, after setting up your subscriber. Records flow to
1009/// `tracing` under the `g2g` target with `category` / `instance` / `g2g_level`
1010/// fields.
1011#[cfg(feature = "tracing")]
1012pub fn init_tracing() {
1013    set_sink(Box::new(TracingSink));
1014    set_default_level(LogLevel::Trace);
1015}
1016
1017/// Implementation hook for the logging macros: check the category threshold and,
1018/// when enabled, format and emit. Generic over `&S` so the macro can pass `&$src`
1019/// whether `$src` is `self` (a `&`/`&mut Self`) or a [`Target`] value (the
1020/// reference forwarding impls cover the extra indirection). Not called directly.
1021#[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/// [`__log`] with structured fields. Not called directly.
1027#[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    // A per-instance override replaces the type category for filtering too, so
1035    // a G2G_DEBUG entry (or glob) written against the override matches.
1036    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/// Log at `level` about a [`LogSource`], checking the category threshold before
1046/// formatting the message. Prefer the level-specific macros.
1047#[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/// Log at `level` with structured fields plus the formatted message, so a sink
1055/// can render or ship the values without re-parsing the line:
1056/// `g2g_log_fields!(LogLevel::Info, self, ["width" => w, "height" => h],
1057/// "configured {w}x{h}")`. Field values are anything convertible into a
1058/// [`LogValue`] (strings, integers, floats, bools).
1059#[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/// `ERROR`-level log about a [`LogSource`].
1072#[macro_export]
1073macro_rules! g2g_error {
1074    ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Error, $src, $($arg)+) };
1075}
1076/// `WARN`-level log about a [`LogSource`].
1077#[macro_export]
1078macro_rules! g2g_warn {
1079    ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Warn, $src, $($arg)+) };
1080}
1081/// `FIXME`-level log about a [`LogSource`].
1082#[macro_export]
1083macro_rules! g2g_fixme {
1084    ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Fixme, $src, $($arg)+) };
1085}
1086/// `INFO`-level log about a [`LogSource`].
1087#[macro_export]
1088macro_rules! g2g_info {
1089    ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Info, $src, $($arg)+) };
1090}
1091/// `DEBUG`-level log about a [`LogSource`].
1092#[macro_export]
1093macro_rules! g2g_debug {
1094    ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Debug, $src, $($arg)+) };
1095}
1096/// `LOG`-level (per-buffer) log about a [`LogSource`].
1097#[macro_export]
1098macro_rules! g2g_log {
1099    ($src:expr, $($arg:tt)+) => { $crate::g2g_log_at!($crate::log::LogLevel::Log, $src, $($arg)+) };
1100}
1101/// `TRACE`-level (most verbose) log about a [`LogSource`].
1102#[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    /// What the TUI leans on at teardown: records buffered while logging was
1114    /// diverted into a ring can be handed to a real sink afterwards, or the only
1115    /// account of a failure dies with the screen that displayed it.
1116    #[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        // A generic element keys on its own name, not the parameter's path tail.
1147        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(); // default Error
1163        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        // The override lets opusenc through at Trace; others stay at Warn.
1172        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        // Off is never enabled.
1180        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        // A bare level sets the default.
1192        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        // Glob hits every matching category...
1202        assert_eq!(cfg.level_for("filesink"), LogLevel::Debug);
1203        assert_eq!(cfg.level_for("sinkpad"), LogLevel::Debug);
1204        // ...`?` matches exactly one byte...
1205        assert_eq!(cfg.level_for("opusenc"), LogLevel::Debug);
1206        assert_eq!(cfg.level_for("opusnc"), LogLevel::Warn);
1207        // ...an exact override wins over a matching glob, in either spec order...
1208        assert_eq!(cfg.level_for("waylandsink"), LogLevel::Error);
1209        // ...and non-matches keep the default.
1210        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    /// One captured log record (level, category, instance, formatted message).
1225    type CapturedRecord = (LogLevel, String, Option<String>, String);
1226    /// A capturing sink for the global-path test.
1227    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    // Serializes the few tests that touch the process-global config / sink.
1240    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        // opusenc is at DEBUG: a debug line is captured with the instance name.
1254        g2g_debug!(enc, "encoded {} bytes", 42);
1255        // videoscale is at the WARNING default: a debug line is filtered out.
1256        g2g_debug!(scale, "scaled a frame");
1257        // A warning on videoscale passes.
1258        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    /// An element-shaped source: a fixed type category plus a settable
1273    /// per-instance name and category override (what [`LogName`] gives an
1274    /// element).
1275    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        // The type category is off; only the override (and a glob covering it)
1297        // is enabled.
1298        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        // The override is the category the sink sees, not just the filter key.
1321        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        // The fields survive as typed values, so a sink renders or ships them
1355        // without re-parsing the message.
1356        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        // A snapshot leaves the buffer intact; a drain empties it.
1391        assert_eq!(ring.len(), 3);
1392        let drained = ring.drain();
1393        assert_eq!(drained.len(), 3);
1394        assert!(ring.is_empty());
1395        // Records carry no timestamp when the host installed no time source.
1396        assert_eq!(drained[0].timestamp_ns, None);
1397
1398        // The recorder keeps working after a drain.
1399        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        // No sink installed: emitting must be a harmless no-op.
1410        g2g_error!(Target::category("x"), "no sink, {}", "dropped");
1411        reset();
1412    }
1413
1414    /// The `tracing` bridge forwards g2g records to the active `tracing`
1415    /// subscriber, carrying category / instance / original level as fields, with
1416    /// `Fixme` collapsing to `WARN` but preserved verbatim in `g2g_level`.
1417    #[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        // A subscriber that captures each event as a flat "level target k=v ..." line.
1424        #[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        // INFO event carries category, instance, and the forwarded message.
1471        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        // FIXME collapses to WARN at the tracing level but is kept in g2g_level.
1476        assert!(events[1].contains("WARN"), "{}", events[1]);
1477        assert!(events[1].contains("g2g_level=FIXME"), "{}", events[1]);
1478        drop(events);
1479        reset();
1480    }
1481}