defmt_logger/
lib.rs

1//! A [`log`] logger that supports formatting [`defmt`] frames
2//!
3//! [`log`]: https://crates.io/crates/log
4//! [`defmt`]: https://crates.io/crates/defmt
5
6#![cfg(feature = "unstable")]
7
8use ansi_term::Colour;
9use colored::{Color, Colorize};
10use defmt_decoder::Frame;
11use difference::{Changeset, Difference};
12use log::{Level, Log, Metadata, Record};
13
14use std::{
15    fmt::Write as _,
16    io,
17    sync::atomic::{AtomicUsize, Ordering},
18};
19
20const DEFMT_TARGET_MARKER: &str = "defmt@";
21
22/// Initializes the `defmt-logger` logger.
23pub fn init(verbose: bool) {
24    log::set_boxed_logger(Box::new(Logger {
25        verbose,
26        timing_align: AtomicUsize::new(8),
27    }))
28    .unwrap();
29    log::set_max_level(log::LevelFilter::Trace);
30}
31
32/// Logs a defmt frame using the `log` facade.
33pub fn log_defmt(
34    frame: &Frame<'_>,
35    file: Option<&str>,
36    line: Option<u32>,
37    module_path: Option<&str>,
38) {
39    let level = match frame.level() {
40        defmt_decoder::Level::Trace => Level::Trace,
41        defmt_decoder::Level::Debug => Level::Debug,
42        defmt_decoder::Level::Info => Level::Info,
43        defmt_decoder::Level::Warn => Level::Warn,
44        defmt_decoder::Level::Error => Level::Error,
45    };
46
47    let target = format!("{}{}", DEFMT_TARGET_MARKER, frame.timestamp());
48    let display = frame.display_message();
49
50    log::logger().log(
51        &Record::builder()
52            .args(format_args!("{}", display))
53            .level(level)
54            .target(&target)
55            .module_path(module_path)
56            .file(file)
57            .line(line)
58            .build(),
59    );
60}
61
62struct Logger {
63    /// Whether to log `debug!` and `trace!`-level host messages.
64    verbose: bool,
65
66    /// Number of characters used by the timestamp. This may increase over time and is used to align
67    /// messages.
68    timing_align: AtomicUsize,
69}
70
71impl Log for Logger {
72    fn enabled(&self, metadata: &Metadata) -> bool {
73        if metadata.target().starts_with(DEFMT_TARGET_MARKER) {
74            // defmt is configured at firmware-level, we will print all of it.
75            true
76        } else {
77            // Host logs use `info!` as the default level, but with the `verbose` flag set we log at
78            // `trace!` level instead.
79            if self.verbose {
80                metadata.target().starts_with("probe_run")
81            } else {
82                metadata.target().starts_with("probe_run") && metadata.level() <= Level::Info
83            }
84        }
85    }
86
87    fn log(&self, record: &Record) {
88        if !self.enabled(record.metadata()) {
89            return;
90        }
91
92        let level_color = match record.level() {
93            Level::Error => Color::Red,
94            Level::Warn => Color::Yellow,
95            Level::Info => Color::Green,
96            Level::Debug => Color::BrightWhite,
97            Level::Trace => Color::BrightBlack,
98        };
99
100        let target = record.metadata().target();
101        let is_defmt = target.starts_with(DEFMT_TARGET_MARKER);
102
103        let timestamp = if is_defmt {
104            let timestamp = target[DEFMT_TARGET_MARKER.len()..].parse::<u64>().unwrap();
105            let seconds = timestamp / 1000000;
106            let micros = timestamp % 1000000;
107            format!("{}.{:06}", seconds, micros)
108        } else {
109            // Mark host logs.
110            format!("(HOST)")
111        };
112
113        self.timing_align
114            .fetch_max(timestamp.len(), Ordering::Relaxed);
115
116        let (stdout, stderr, mut stdout_lock, mut stderr_lock);
117        let sink: &mut dyn io::Write = if is_defmt {
118            // defmt goes to stdout, since it's the primary output produced by this tool.
119            stdout = io::stdout();
120            stdout_lock = stdout.lock();
121            &mut stdout_lock
122        } else {
123            // Everything else goes to stderr.
124            stderr = io::stderr();
125            stderr_lock = stderr.lock();
126            &mut stderr_lock
127        };
128
129        writeln!(
130            sink,
131            "{timestamp:>0$} {level:5} {args}",
132            self.timing_align.load(Ordering::Relaxed),
133            timestamp = timestamp,
134            level = record.level().to_string().color(level_color),
135            args = color_diff(record.args().to_string()),
136        )
137        .ok();
138
139        if let Some(file) = record.file() {
140            // NOTE will be `Some` if `file` is `Some`
141            let mod_path = record.module_path().unwrap();
142            // Always include location info for defmt output.
143            if is_defmt || self.verbose {
144                let mut loc = file.to_string();
145                if let Some(line) = record.line() {
146                    loc.push_str(&format!(":{}", line));
147                }
148                writeln!(sink, "{}", format!("└─ {} @ {}", mod_path, loc).dimmed()).ok();
149            }
150        }
151    }
152
153    fn flush(&self) {}
154}
155
156// color the output of `defmt::assert_eq`
157// HACK we should not re-parse formatted output but instead directly format into a color diff
158// template; that may require specially tagging log messages that come from `defmt::assert_eq`
159fn color_diff(text: String) -> String {
160    let lines = text.lines().collect::<Vec<_>>();
161    let nlines = lines.len();
162    if nlines > 2 {
163        let left = lines[nlines - 2];
164        let right = lines[nlines - 1];
165
166        const LEFT_START: &str = " left: `";
167        const RIGHT_START: &str = "right: `";
168        const END: &str = "`";
169        if left.starts_with(LEFT_START)
170            && left.ends_with(END)
171            && right.starts_with(RIGHT_START)
172            && right.ends_with(END)
173        {
174            const DARK_RED: Colour = Colour::Fixed(52);
175            const DARK_GREEN: Colour = Colour::Fixed(22);
176
177            // `defmt::assert_eq!` output
178            let left = &left[LEFT_START.len()..left.len() - END.len()];
179            let right = &right[RIGHT_START.len()..right.len() - END.len()];
180
181            let mut buf = lines[..nlines - 2].join("\n").bold().to_string();
182            buf.push('\n');
183
184            let changeset = Changeset::new(left, right, "");
185
186            writeln!(
187                buf,
188                "{} {} / {}",
189                "diff".bold(),
190                "< left".red(),
191                "right >".green()
192            )
193            .ok();
194            write!(buf, "{}", "<".red()).ok();
195            for diff in &changeset.diffs {
196                match diff {
197                    Difference::Same(s) => {
198                        write!(buf, "{}", s.red()).ok();
199                    }
200                    Difference::Add(_) => continue,
201                    Difference::Rem(s) => {
202                        write!(buf, "{}", Colour::Red.on(DARK_RED).bold().paint(s)).ok();
203                    }
204                }
205            }
206            buf.push('\n');
207
208            write!(buf, "{}", ">".green()).ok();
209            for diff in &changeset.diffs {
210                match diff {
211                    Difference::Same(s) => {
212                        write!(buf, "{}", s.green()).ok();
213                    }
214                    Difference::Rem(_) => continue,
215                    Difference::Add(s) => {
216                        write!(buf, "{}", Colour::Green.on(DARK_GREEN).bold().paint(s)).ok();
217                    }
218                }
219            }
220            return buf;
221        }
222    }
223
224    // keep output as it is
225    text.bold().to_string()
226}