Skip to main content

saf_core/logging/subscriber/
layer.rs

1//! `SafLogLayer` — a `tracing` Layer that formats and emits SAF debug log lines.
2//!
3//! Listens for tracing events with target `"saf_debug"`, extracts the structured
4//! fields (`saf_module`, `saf_phase`, `saf_tag`, `saf_narrative`, `saf_kv`),
5//! checks them against the `SafLogFilter`, and writes formatted DSL lines.
6
7use std::io::Write;
8use std::sync::Mutex;
9
10use tracing::Event;
11use tracing::field::{Field, Visit};
12use tracing_subscriber::Layer;
13use tracing_subscriber::layer::Context;
14
15use super::filter::SafLogFilter;
16use crate::logging::formatter::format_saf_log_line;
17
18/// A tracing `Layer` that emits SAF structured debug log lines.
19pub struct SafLogLayer {
20    filter: SafLogFilter,
21    writer: Mutex<Box<dyn Write + Send>>,
22}
23
24impl SafLogLayer {
25    /// Create a new `SafLogLayer` with the given filter and output writer.
26    pub fn new(filter: SafLogFilter, writer: Box<dyn Write + Send>) -> Self {
27        Self {
28            filter,
29            writer: Mutex::new(writer),
30        }
31    }
32}
33
34impl<S: tracing::Subscriber> Layer<S> for SafLogLayer {
35    fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
36        // Only process events with our target
37        if event.metadata().target() != "saf_debug" {
38            return;
39        }
40
41        // Skip if filter is not active
42        if !self.filter.is_active() {
43            return;
44        }
45
46        // Extract fields
47        let mut visitor = SafLogVisitor::default();
48        event.record(&mut visitor);
49
50        let module = visitor.module.as_deref().unwrap_or("");
51        let phase = visitor.phase.as_deref().unwrap_or("");
52        let tag = visitor.tag.as_deref().unwrap_or("");
53
54        // Check filter
55        if !self.filter.matches(module, phase, tag) {
56            return;
57        }
58
59        let narrative = visitor.narrative.as_deref().unwrap_or("");
60        let kv = visitor.kv.as_deref().unwrap_or("");
61
62        let line = format_saf_log_line(module, phase, tag, narrative, kv);
63
64        if let Ok(mut w) = self.writer.lock() {
65            let _ = writeln!(w, "{line}");
66        }
67    }
68}
69
70/// Visitor that extracts SAF-specific fields from tracing events.
71#[derive(Default)]
72struct SafLogVisitor {
73    module: Option<String>,
74    phase: Option<String>,
75    tag: Option<String>,
76    narrative: Option<String>,
77    kv: Option<String>,
78}
79
80impl Visit for SafLogVisitor {
81    fn record_str(&mut self, field: &Field, value: &str) {
82        match field.name() {
83            "saf_module" => self.module = Some(value.to_string()),
84            "saf_phase" => self.phase = Some(value.to_string()),
85            "saf_tag" => self.tag = Some(value.to_string()),
86            "saf_narrative" => self.narrative = Some(value.to_string()),
87            "saf_kv" => self.kv = Some(value.to_string()),
88            _ => {}
89        }
90    }
91
92    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
93        // Fallback for fields recorded as Debug
94        let s = format!("{value:?}");
95        self.record_str(field, &s);
96    }
97}