Skip to main content

ferroui_base/logging/
string_log_sink.rs

1use std::any::Any;
2use std::fmt::{Display, Write as _};
3use std::io::Write as _;
4
5use super::{ILogSink, LogEventLevel};
6use crate::FerroObject;
7
8/// A sink that formats events as text lines and hands them to a callback.
9pub struct StringLogSink {
10    logger: Box<dyn Fn(&str) + Send + Sync>,
11    level: LogEventLevel,
12    areas: Option<Vec<String>>,
13}
14
15impl StringLogSink {
16    /// Creates a sink that passes every formatted event with at least
17    /// `minimum_level` to `logger`. When `areas` is not empty only events of
18    /// those areas are logged; valid values are listed in
19    /// [`LogArea`](super::LogArea).
20    pub fn new(logger: impl Fn(&str) + Send + Sync + 'static, minimum_level: LogEventLevel, areas: &[&str]) -> Self {
21        Self {
22            logger: Box::new(logger),
23            level: minimum_level,
24            areas: if areas.is_empty() { None } else { Some(areas.iter().map(|area| area.to_string()).collect()) },
25        }
26    }
27
28    /// Creates a sink that writes events to the diagnostic trace output of
29    /// the process (standard error).
30    pub fn trace(minimum_level: LogEventLevel, areas: &[&str]) -> Self {
31        Self::new(
32            |line| {
33                // Tracing must never fail the caller.
34                let _ = writeln!(std::io::stderr(), "{line}");
35            },
36            minimum_level,
37            areas,
38        )
39    }
40
41    fn format(area: &str, template: &str, source: Option<&dyn Any>, values: Option<&[&dyn Display]>) -> String {
42        let mut result = String::with_capacity(template.len() + area.len() + 16);
43        let mut chars = template.chars().peekable();
44        let mut i = 0;
45
46        result.push('[');
47        result.push_str(area);
48        // The plain overload separates the area from the message; the
49        // overload with values does not.
50        result.push_str(if values.is_none() { "] " } else { "]" });
51
52        while let Some(c) = chars.next() {
53            if c != '{' {
54                result.push(c);
55            } else if chars.peek() != Some(&'{') {
56                result.push('\'');
57                if let Some(value) = values.and_then(|values| values.get(i)) {
58                    let _ = write!(result, "{value}");
59                }
60                i += 1;
61                result.push('\'');
62                // Skip the placeholder name and the closing brace.
63                for c in chars.by_ref() {
64                    if c == '}' {
65                        break;
66                    }
67                }
68            } else {
69                result.push('{');
70                chars.next();
71            }
72        }
73
74        Self::format_source(source, &mut result);
75        result
76    }
77
78    fn format_source(source: Option<&dyn Any>, result: &mut String) {
79        let Some(source) = source else {
80            return;
81        };
82
83        result.push_str(" (");
84        if let Some(object) = source.downcast_ref::<FerroObject>() {
85            result.push_str(object.get_type().name());
86        } else {
87            result.push_str("Object");
88        }
89        result.push_str(" #");
90        let _ = write!(result, "{}", source as *const dyn Any as *const () as usize);
91        result.push(')');
92    }
93}
94
95impl ILogSink for StringLogSink {
96    fn is_enabled(&self, level: LogEventLevel, area: &str) -> bool {
97        level >= self.level && self.areas.as_ref().is_none_or(|areas| areas.iter().any(|a| a == area))
98    }
99
100    fn log(&self, level: LogEventLevel, area: &str, source: Option<&dyn Any>, message_template: &str) {
101        if self.is_enabled(level, area) {
102            (self.logger)(&Self::format(area, message_template, source, None));
103        }
104    }
105
106    fn log_with_values(
107        &self,
108        level: LogEventLevel,
109        area: &str,
110        source: Option<&dyn Any>,
111        message_template: &str,
112        property_values: &[&dyn Display],
113    ) {
114        if self.is_enabled(level, area) {
115            (self.logger)(&Self::format(area, message_template, source, Some(property_values)));
116        }
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::logging::{LogArea, Logger};
124    use std::cell::RefCell;
125    use std::rc::Rc;
126    use std::sync::{Arc, Mutex};
127
128    fn collecting_sink(level: LogEventLevel, areas: &[&str]) -> (StringLogSink, Arc<Mutex<Vec<String>>>) {
129        let lines = Arc::new(Mutex::new(Vec::new()));
130        let l = lines.clone();
131        (StringLogSink::new(move |line| l.lock().unwrap().push(line.to_string()), level, areas), lines)
132    }
133
134    #[test]
135    fn is_enabled_filters_by_level_and_area() {
136        let (sink, _) = collecting_sink(LogEventLevel::Warning, &[]);
137        assert!(sink.is_enabled(LogEventLevel::Warning, LogArea::BINDING));
138        assert!(sink.is_enabled(LogEventLevel::Fatal, LogArea::LAYOUT));
139        assert!(!sink.is_enabled(LogEventLevel::Information, LogArea::BINDING));
140
141        let (sink, _) = collecting_sink(LogEventLevel::Verbose, &[LogArea::LAYOUT]);
142        assert!(sink.is_enabled(LogEventLevel::Verbose, LogArea::LAYOUT));
143        assert!(!sink.is_enabled(LogEventLevel::Fatal, LogArea::BINDING));
144    }
145
146    #[test]
147    fn formats_message_without_values() {
148        let (sink, lines) = collecting_sink(LogEventLevel::Verbose, &[]);
149        sink.log(LogEventLevel::Warning, LogArea::BINDING, None, "Something {Name} happened {{literal}}");
150        assert_eq!(lines.lock().unwrap().as_slice(), ["[Binding] Something '' happened {literal}}"]);
151    }
152
153    #[test]
154    fn formats_message_with_values() {
155        let (sink, lines) = collecting_sink(LogEventLevel::Verbose, &[]);
156        sink.log_with_values(
157            LogEventLevel::Error,
158            LogArea::LAYOUT,
159            None,
160            "Measure of {Control} took {Time}ms and {Missing}",
161            &[&"Button", &12.5],
162        );
163        assert_eq!(lines.lock().unwrap().as_slice(), ["[Layout]Measure of 'Button' took '12.5'ms and ''"]);
164    }
165
166    #[test]
167    fn formats_source() {
168        let (sink, lines) = collecting_sink(LogEventLevel::Verbose, &[]);
169        let source = 5i32;
170        sink.log(LogEventLevel::Warning, LogArea::CONTROL, Some(&source), "Message");
171        let lines = lines.lock().unwrap();
172        assert!(lines[0].starts_with("[Control] Message (Object #"), "{}", lines[0]);
173        assert!(lines[0].ends_with(')'));
174    }
175
176    #[test]
177    fn disabled_events_are_not_logged() {
178        let (sink, lines) = collecting_sink(LogEventLevel::Error, &[LogArea::LAYOUT]);
179        sink.log(LogEventLevel::Warning, LogArea::LAYOUT, None, "low level");
180        sink.log_with_values(LogEventLevel::Fatal, LogArea::BINDING, None, "other area {X}", &[&1]);
181        assert!(lines.lock().unwrap().is_empty());
182    }
183
184    struct TestSink {
185        events: RefCell<Vec<(LogEventLevel, String, String, Vec<String>)>>,
186    }
187
188    impl ILogSink for TestSink {
189        fn is_enabled(&self, level: LogEventLevel, _area: &str) -> bool {
190            level >= LogEventLevel::Warning
191        }
192
193        fn log(&self, level: LogEventLevel, area: &str, _source: Option<&dyn Any>, message_template: &str) {
194            self.events.borrow_mut().push((level, area.to_string(), message_template.to_string(), Vec::new()));
195        }
196
197        fn log_with_values(
198            &self,
199            level: LogEventLevel,
200            area: &str,
201            _source: Option<&dyn Any>,
202            message_template: &str,
203            property_values: &[&dyn Display],
204        ) {
205            self.events.borrow_mut().push((
206                level,
207                area.to_string(),
208                message_template.to_string(),
209                property_values.iter().map(|v| v.to_string()).collect(),
210            ));
211        }
212    }
213
214    #[test]
215    fn logger_uses_the_thread_sink() {
216        let sink = Rc::new(TestSink { events: RefCell::new(Vec::new()) });
217        let previous = Logger::set_thread_sink(Some(sink.clone()));
218
219        assert!(Logger::is_enabled(LogEventLevel::Warning, LogArea::BINDING));
220        assert!(!Logger::is_enabled(LogEventLevel::Debug, LogArea::BINDING));
221        assert!(Logger::try_get(LogEventLevel::Debug, LogArea::BINDING).is_none());
222
223        let logger = Logger::try_get(LogEventLevel::Error, LogArea::BINDING).unwrap();
224        assert!(logger.is_valid());
225        logger.log(None, "plain");
226        logger.log_with_values(None, "value {A} {B}", &[&1, &"two"]);
227
228        Logger::set_thread_sink(previous);
229        assert!(Logger::thread_sink().is_none());
230
231        let events = sink.events.borrow();
232        assert_eq!(events.len(), 2);
233        assert_eq!(events[0], (LogEventLevel::Error, "Binding".to_string(), "plain".to_string(), vec![]));
234        assert_eq!(events[1].3, vec!["1".to_string(), "two".to_string()]);
235    }
236}