redox_log/
lib.rs

1use std::ffi::OsStr;
2use std::fs::{self, File};
3use std::io::{prelude::*, BufWriter};
4use std::path::PathBuf;
5use std::sync::Mutex;
6use std::{fmt, io};
7
8use log::{Metadata, Record};
9use smallvec::SmallVec;
10use termion::color;
11
12/// An output that will be logged to. The two major outputs for most Redox system programs are
13/// usually the log file, and the global stdout.
14pub struct Output {
15    // the actual endpoint to write to.
16    endpoint: Mutex<Box<dyn Write + Send + 'static>>,
17
18    // useful for devices like BufWrite or BufRead. You don't want the log file to never but
19    // written until the program exists.
20    flush_on_newline: bool,
21
22    // specifies the maximum log level possible
23    filter: log::LevelFilter,
24
25    // specifies whether the file should contain ASCII escape codes
26    ansi: bool,
27}
28impl fmt::Debug for Output {
29    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
30        f.debug_struct("Output")
31            .field("endpoint", &"opaque")
32            .field("flush_on_newline", &self.flush_on_newline)
33            .field("filter", &self.filter)
34            .field("ansi", &self.ansi)
35            .finish()
36    }
37}
38
39pub struct OutputBuilder {
40    endpoint: Box<dyn Write + Send + 'static>,
41    flush_on_newline: Option<bool>,
42    filter: Option<log::LevelFilter>,
43    ansi: Option<bool>,
44}
45impl OutputBuilder {
46    pub fn in_redox_logging_scheme<A, B, C>(
47        category: A,
48        subcategory: B,
49        logfile: C,
50    ) -> Result<Self, io::Error>
51    where
52        A: AsRef<OsStr>,
53        B: AsRef<OsStr>,
54        C: AsRef<OsStr>,
55    {
56        if !cfg!(target_os = "redox") {
57            return Ok(Self::with_endpoint(Vec::new()));
58        }
59
60        let mut path = PathBuf::from("/scheme/logging/");
61        path.push(category.as_ref());
62        path.push(subcategory.as_ref());
63        path.push(logfile.as_ref());
64        path.set_extension("log");
65
66        if let Some(parent) = path.parent() {
67            if !parent.exists() {
68                fs::create_dir_all(parent)?;
69            }
70        }
71
72        Ok(Self::with_endpoint(BufWriter::new(File::create(path)?)))
73    }
74
75    pub fn stdout() -> Self {
76        Self::with_endpoint(io::stdout())
77    }
78    pub fn stderr() -> Self {
79        Self::with_endpoint(io::stderr())
80    }
81
82    pub fn with_endpoint<T>(endpoint: T) -> Self
83    where
84        T: Write + Send + 'static,
85    {
86        Self::with_dyn_endpoint(Box::new(endpoint))
87    }
88    pub fn with_dyn_endpoint(endpoint: Box<dyn Write + Send + 'static>) -> Self {
89        Self {
90            endpoint,
91            flush_on_newline: None,
92            filter: None,
93            ansi: None,
94        }
95    }
96    pub fn flush_on_newline(mut self, flush: bool) -> Self {
97        self.flush_on_newline = Some(flush);
98        self
99    }
100    pub fn with_filter(mut self, filter: log::LevelFilter) -> Self {
101        self.filter = Some(filter);
102        self
103    }
104    pub fn with_ansi_escape_codes(mut self) -> Self {
105        self.ansi = Some(true);
106        self
107    }
108    pub fn build(self) -> Output {
109        Output {
110            endpoint: Mutex::new(self.endpoint),
111            filter: self.filter.unwrap_or(log::LevelFilter::Info),
112            flush_on_newline: self.flush_on_newline.unwrap_or(true),
113            ansi: self.ansi.unwrap_or(false),
114        }
115    }
116}
117
118const AVG_OUTPUTS: usize = 2;
119
120#[derive(Debug, Default)]
121pub struct RedoxLogger {
122    outputs: SmallVec<[Output; AVG_OUTPUTS]>,
123    min_filter: Option<log::LevelFilter>,
124    max_filter: Option<log::LevelFilter>,
125    max_level_in_use: Option<log::LevelFilter>,
126    min_level_in_use: Option<log::LevelFilter>,
127    process_name: Option<String>,
128}
129
130impl RedoxLogger {
131    pub fn new() -> Self {
132        Self::default()
133    }
134    fn adjust_output_level(
135        max_filter: Option<log::LevelFilter>,
136        min_filter: Option<log::LevelFilter>,
137        max_in_use: &mut Option<log::LevelFilter>,
138        min_in_use: &mut Option<log::LevelFilter>,
139        output: &mut Output,
140    ) {
141        if let Some(max) = max_filter {
142            output.filter = std::cmp::max(output.filter, max);
143        }
144        if let Some(min) = min_filter {
145            output.filter = std::cmp::min(output.filter, min);
146        }
147        match max_in_use {
148            &mut Some(ref mut max) => *max = std::cmp::max(output.filter, *max),
149            max @ &mut None => *max = Some(output.filter),
150        }
151        match min_in_use {
152            &mut Some(ref mut min) => *min = std::cmp::min(output.filter, *min),
153            min @ &mut None => *min = Some(output.filter),
154        }
155    }
156    pub fn with_output(mut self, mut output: Output) -> Self {
157        Self::adjust_output_level(
158            self.max_filter,
159            self.min_filter,
160            &mut self.max_level_in_use,
161            &mut self.min_level_in_use,
162            &mut output,
163        );
164        self.outputs.push(output);
165        self
166    }
167    pub fn with_min_level_override(mut self, min: log::LevelFilter) -> Self {
168        self.min_filter = Some(min);
169        for output in &mut self.outputs {
170            Self::adjust_output_level(
171                self.max_filter,
172                self.min_filter,
173                &mut self.max_level_in_use,
174                &mut self.min_level_in_use,
175                output,
176            );
177        }
178        self
179    }
180    pub fn with_max_level_override(mut self, max: log::LevelFilter) -> Self {
181        self.max_filter = Some(max);
182        for output in &mut self.outputs {
183            Self::adjust_output_level(
184                self.max_filter,
185                self.min_filter,
186                &mut self.max_level_in_use,
187                &mut self.min_level_in_use,
188                output,
189            );
190        }
191        self
192    }
193    pub fn with_process_name(mut self, name: String) -> Self {
194        self.process_name = Some(name);
195        self
196    }
197    pub fn enable(self) -> Result<&'static Self, log::SetLoggerError> {
198        let leak = Box::leak(Box::new(self));
199        log::set_logger(leak)?;
200        if let Some(max) = leak.max_level_in_use {
201            log::set_max_level(max);
202        } else {
203            log::set_max_level(log::LevelFilter::Off);
204        }
205        Ok(leak)
206    }
207    fn write_record<W: Write>(
208        ansi: bool,
209        record: &Record,
210        process_name: Option<&str>,
211        writer: &mut W,
212    ) -> io::Result<()> {
213        use log::Level;
214        use termion::style;
215
216        // TODO: Log offloading to another thread or thread pool, maybe?
217        // Time & Time Zone Formatting
218        let now_local = chrono::Local::now();
219        let time = now_local.format("%Y-%m-%dT%H-%M-%S%.3f");
220        let mut zone = format!("{}", now_local.format("%:z"));
221        if zone.as_str() == "+00:00" {
222            zone = "Z".to_string();
223        }
224
225        let target = record.module_path().unwrap_or(record.target());
226        let level = record.level();
227        let message = record.args();
228
229        let reset = color::Fg(color::Reset);
230
231        let show_lines = true;
232        let line_number = if show_lines { record.line() } else { None };
233
234        let process_name = process_name.unwrap_or("");
235        let line = &LineFmt(line_number, false);
236
237        if ansi {
238            let time_color = color::Fg(color::LightWhite);
239            let zone_color = color::Fg(color::White);
240
241            let trace_col = color::Fg(color::LightBlack);
242            let debug_col = color::Fg(color::White);
243            let info_col = color::Fg(color::LightBlue);
244            let warn_col = color::Fg(color::LightYellow);
245            let err_col = color::Fg(color::LightRed);
246
247            let level_color: &dyn fmt::Display = match level {
248                Level::Trace => &trace_col,
249                Level::Debug => &debug_col,
250                Level::Info => &info_col,
251                Level::Warn => &warn_col,
252                Level::Error => &err_col,
253            };
254
255            let dim_white = color::Fg(color::White);
256            let bright_white = color::Fg(color::LightWhite);
257            let regular_style = "";
258            let bold_style = style::Bold;
259
260            let [message_color, message_style]: [&dyn fmt::Display; 2] = match level {
261                Level::Trace | Level::Debug => [&dim_white, &regular_style],
262                Level::Info | Level::Warn | Level::Error => [&bright_white, &bold_style],
263            };
264            let target_color = color::Fg(color::White);
265
266            let i = style::Italic;
267            let b = style::Bold;
268            let r = reset;
269            let rs = style::Reset;
270
271            writeln!(
272                writer,
273                "{time}{zone} [{target}{line} {level}] {msg}",
274                time = format_args!("{i}{time_color}{time}{rs}{r}"),
275                zone = format_args!("{i}{zone_color}{zone}{rs}{r}"),
276                level = format_args!("{b}{level_color}{level}{rs}{r}"),
277                target = format_args!("{target_color}{process_name}@{target}{r}"),
278                msg = format_args!("{message_style}{message_color}{message}{rs}{r}"),
279            )
280        } else {
281            writeln!(
282                writer,
283                "{time}{zone} [{process_name}@{target}{line} {level}] {message}",
284            )
285        }
286    }
287}
288
289impl log::Log for RedoxLogger {
290    fn enabled(&self, metadata: &Metadata) -> bool {
291        self.max_level_in_use
292            .map(|min| metadata.level() >= min)
293            .unwrap_or(false)
294            && self
295                .min_level_in_use
296                .map(|max| metadata.level() <= max)
297                .unwrap_or(false)
298    }
299    fn log(&self, record: &Record) {
300        for output in &self.outputs {
301            if record.metadata().level() <= output.filter {
302                let mut endpoint_guard = match output.endpoint.lock() {
303                    Ok(e) => e,
304                    // poison error
305                    _ => continue,
306                };
307
308                let _ = Self::write_record(
309                    output.ansi,
310                    record,
311                    self.process_name.as_deref(),
312                    &mut *endpoint_guard,
313                );
314
315                if output.flush_on_newline {
316                    let _ = endpoint_guard.flush();
317                }
318            }
319        }
320    }
321    fn flush(&self) {
322        for output in &self.outputs {
323            match output.endpoint.lock() {
324                Ok(ref mut e) => {
325                    let _ = e.flush();
326                }
327                _ => continue,
328            }
329        }
330    }
331}
332
333struct LineFmt(Option<u32>, bool);
334impl fmt::Display for LineFmt {
335    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
336        if let Some(line) = self.0 {
337            if self.1 {
338                // ansi escape codes
339                let color = color::Fg(color::LightBlack);
340                let reset = color::Fg(color::Reset);
341                write!(f, "{color}:{line}{reset}")
342            } else {
343                // no ansi escape codes
344                write!(f, ":{line}")
345            }
346        } else {
347            write!(f, "")
348        }
349    }
350}