1use std::any::Any;
2use std::fmt::{Display, Write as _};
3use std::io::Write as _;
4
5use super::{ILogSink, LogEventLevel};
6use crate::FerroObject;
7
8pub struct StringLogSink {
10 logger: Box<dyn Fn(&str) + Send + Sync>,
11 level: LogEventLevel,
12 areas: Option<Vec<String>>,
13}
14
15impl StringLogSink {
16 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 pub fn trace(minimum_level: LogEventLevel, areas: &[&str]) -> Self {
31 Self::new(
32 |line| {
33 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 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 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}