probe_rs_cli_util/
logging.rs

1use colored::*;
2use env_logger::{Builder, Logger};
3use indicatif::ProgressBar;
4use is_terminal::IsTerminal;
5use log::{Level, LevelFilter, Log, Record};
6use once_cell::sync::Lazy;
7#[cfg(feature = "sentry")]
8use sentry::{
9    integrations::panic::PanicIntegration,
10    types::{Dsn, Uuid},
11    Breadcrumb,
12};
13use simplelog::{CombinedLogger, SharedLogger};
14#[cfg(feature = "sentry")]
15use std::{borrow::Cow, error::Error, panic::PanicInfo, str::FromStr};
16use std::{
17    fmt::{self},
18    io::Write,
19    sync::{
20        atomic::{AtomicUsize, Ordering},
21        Arc, RwLock,
22    },
23};
24use terminal_size::{Height, Width};
25
26/// The maximum window width of the terminal, given in characters possible.
27static MAX_WINDOW_WIDTH: AtomicUsize = AtomicUsize::new(0);
28
29/// Stores the progress bar for the logging facility.
30static PROGRESS_BAR: Lazy<RwLock<Option<Arc<ProgressBar>>>> = Lazy::new(|| RwLock::new(None));
31
32#[cfg(feature = "sentry")]
33static LOG: Lazy<Arc<RwLock<Vec<Breadcrumb>>>> = Lazy::new(|| Arc::new(RwLock::new(vec![])));
34
35/// A structure to hold a string with a padding attached to the start of it.
36struct Padded<T> {
37    value: T,
38    width: usize,
39}
40
41impl<T: fmt::Display> fmt::Display for Padded<T> {
42    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
43        write!(f, "{: <width$}", self.value, width = self.width)
44    }
45}
46
47/// Get the maximum between the window width and the length of the given string.
48fn max_target_width(target: &str) -> usize {
49    let max_width = MAX_WINDOW_WIDTH.load(Ordering::Relaxed);
50    if max_width < target.len() {
51        MAX_WINDOW_WIDTH.store(target.len(), Ordering::Relaxed);
52        target.len()
53    } else {
54        max_width
55    }
56}
57
58/// Helper to receive a color for a given level.
59fn colored_level(level: Level) -> ColoredString {
60    match level {
61        Level::Trace => "TRACE".magenta().bold(),
62        Level::Debug => "DEBUG".blue().bold(),
63        Level::Info => " INFO".green().bold(),
64        Level::Warn => " WARN".yellow().bold(),
65        Level::Error => "ERROR".red().bold(),
66    }
67}
68
69struct ShareableLogger {
70    env_logger: Logger,
71    output_is_terminal: bool,
72}
73
74impl Log for ShareableLogger {
75    fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
76        metadata.level() <= self.env_logger.filter()
77    }
78
79    fn log(&self, record: &Record<'_>) {
80        if self.enabled(record.metadata()) {
81            // If the output is not an interactive terminal,
82            // indicatif will not display anything, so messages
83            // forwared to it would be swallowed.
84            if !self.output_is_terminal {
85                self.env_logger.log(record);
86            } else {
87                let guard = PROGRESS_BAR.write().unwrap();
88
89                // Print the log message above the progress bar, if one is present.
90                if let Some(pb) = &*guard {
91                    let target = record.target();
92                    let max_width = max_target_width(target);
93
94                    let level = colored_level(record.level());
95
96                    let target = Padded {
97                        value: target.bold(),
98                        width: max_width,
99                    };
100
101                    pb.println(format!("       {} {} > {}", level, target, record.args()));
102                } else {
103                    self.env_logger.log(record);
104                }
105            }
106        }
107    }
108
109    fn flush(&self) {
110        self.env_logger.flush();
111    }
112}
113
114impl SharedLogger for ShareableLogger {
115    fn level(&self) -> LevelFilter {
116        self.env_logger.filter()
117    }
118
119    fn config(&self) -> Option<&simplelog::Config> {
120        None
121    }
122
123    fn as_log(self: Box<Self>) -> Box<dyn log::Log> {
124        Box::new(*self)
125    }
126}
127
128/// Initialize the logger.
129///
130/// There are two sources for log level configuration:
131///
132/// - The log level value passed to this function
133/// - The user can set the `RUST_LOG` env var, which overrides the log level passed to this function.
134///
135/// The config file only accepts a log level, while the `RUST_LOG` variable
136/// supports the full `env_logger` syntax, including filtering by crate and
137/// module.
138pub fn init(level: Option<Level>) {
139    // User visible logging.
140
141    let mut log_builder = Builder::new();
142
143    // First, apply the log level given to this function.
144    if let Some(level) = level {
145        log_builder.filter_level(level.to_level_filter());
146    } else {
147        log_builder.filter_level(LevelFilter::Warn);
148    }
149
150    // Then override that with the `RUST_LOG` env var, if set.
151    if let Ok(s) = ::std::env::var("RUST_LOG") {
152        log_builder.parse_filters(&s);
153    }
154
155    // Define our custom log format.
156    log_builder.format(move |f, record| {
157        let target = record.target();
158        let max_width = max_target_width(target);
159
160        let level = colored_level(record.level());
161
162        let mut style = f.style();
163        let target = style.set_bold(true).value(Padded {
164            value: target,
165            width: max_width,
166        });
167
168        writeln!(f, "       {} {} > {}", level, target, record.args())
169    });
170
171    // Sentry logging (all log levels except tracing (to not clog the server disk & internet sink)).
172    #[cfg(feature = "sentry")]
173    let mut sentry = {
174        let mut sentry = Builder::new();
175
176        // Always use the Debug log level.
177        sentry.filter_level(LevelFilter::Debug);
178
179        // Define our custom log format.
180        sentry.format(move |_f, record| {
181            let mut log_guard = LOG.write().unwrap();
182            log_guard.push(Breadcrumb {
183                level: match record.level() {
184                    Level::Error => sentry::Level::Error,
185                    Level::Warn => sentry::Level::Warning,
186                    Level::Info => sentry::Level::Info,
187                    Level::Debug => sentry::Level::Debug,
188                    // This mapping is intended as unfortunately, Sentry does not have any trace level for events & breadcrumbs.
189                    Level::Trace => sentry::Level::Debug,
190                },
191                category: Some(record.target().to_string()),
192                message: Some(format!("{}", record.args())),
193                ..Default::default()
194            });
195
196            Ok(())
197        });
198
199        sentry
200    };
201
202    let output_is_terminal = std::io::stderr().is_terminal();
203
204    CombinedLogger::init(vec![
205        Box::new(ShareableLogger {
206            env_logger: log_builder.build(),
207            output_is_terminal,
208        }),
209        #[cfg(feature = "sentry")]
210        Box::new(ShareableLogger {
211            env_logger: sentry.build(),
212            output_is_terminal,
213        }),
214    ])
215    .unwrap();
216}
217
218/// Sets the currently displayed progress bar of the CLI.
219pub fn set_progress_bar(progress: Arc<ProgressBar>) {
220    let mut guard = PROGRESS_BAR.write().unwrap();
221    *guard = Some(progress);
222}
223
224/// Disables the currently displayed progress bar of the CLI.
225pub fn clear_progress_bar() {
226    let mut guard = PROGRESS_BAR.write().unwrap();
227    *guard = None;
228}
229
230#[cfg(feature = "sentry")]
231fn send_logs() {
232    let mut log_guard = LOG.write().unwrap();
233
234    for breadcrumb in log_guard.drain(..) {
235        sentry::add_breadcrumb(breadcrumb);
236    }
237}
238
239#[cfg(feature = "sentry")]
240fn sentry_config(release: String) -> sentry::ClientOptions {
241    sentry::ClientOptions {
242        dsn: Some(
243            Dsn::from_str(
244                "https://820ae3cb7b524b59af68d652aeb8ac3a@o473674.ingest.sentry.io/5508777",
245            )
246            .unwrap(),
247        ),
248        release: Some(Cow::<'static>::Owned(release)),
249        #[cfg(debug_assertions)]
250        environment: Some(Cow::Borrowed("Development")),
251        #[cfg(not(debug_assertions))]
252        environment: Some(Cow::Borrowed("Production")),
253        default_integrations: false,
254        ..Default::default()
255    }
256}
257
258#[derive(Clone, Debug)]
259pub struct Metadata {
260    pub chip: Option<String>,
261    pub probe: Option<String>,
262    pub speed: Option<String>,
263    pub release: String,
264    pub commit: String,
265}
266
267#[cfg(feature = "sentry")]
268/// Sets the metadata concerning the current probe-rs session on the sentry scope.
269fn set_metadata(metadata: &Metadata) {
270    sentry::configure_scope(|scope| {
271        if let Some(chip) = metadata.chip.as_ref() {
272            scope.set_tag("chip", chip);
273        }
274        if let Some(probe) = metadata.probe.as_ref() {
275            scope.set_tag("probe", probe);
276        }
277        if let Some(speed) = metadata.speed.as_ref() {
278            scope.set_tag("speed", speed);
279        }
280        scope.set_tag("commit", &metadata.commit);
281    })
282}
283
284#[cfg(feature = "sentry")]
285const SENTRY_SUCCESS: &str = r"Your error was reported successfully. If you don't mind, please open an issue on Github and include the UUID:";
286
287#[cfg(feature = "sentry")]
288fn print_uuid(uuid: Uuid) {
289    let size = terminal_size::terminal_size();
290    if let Some((Width(w), Height(_h))) = size {
291        let lines = chunk_string(&format!("{SENTRY_SUCCESS} {uuid}"), w as usize - 14);
292
293        for (i, l) in lines.iter().enumerate() {
294            if i == 0 {
295                println!("  {} {}", "Thank You!".cyan().bold(), l);
296            } else {
297                println!("             {l}");
298            }
299        }
300    } else {
301        print!("{SENTRY_HINT}");
302    }
303}
304
305#[cfg(feature = "sentry")]
306/// Captures an std::error::Error with sentry and sends all previously captured logs.
307pub fn capture_error<E>(metadata: &Metadata, error: &E)
308where
309    E: Error + ?Sized,
310{
311    let _guard = sentry::init(sentry_config(metadata.release.clone()));
312    set_metadata(metadata);
313    send_logs();
314    let uuid = sentry::capture_error(error);
315    print_uuid(uuid);
316}
317
318#[cfg(feature = "sentry")]
319/// Captures an anyhow error with sentry and sends all previously captured logs.
320pub fn capture_anyhow(metadata: &Metadata, error: &anyhow::Error) {
321    let _guard = sentry::init(sentry_config(metadata.release.clone()));
322    set_metadata(metadata);
323    send_logs();
324    let uuid = sentry::integrations::anyhow::capture_anyhow(error);
325    print_uuid(uuid);
326}
327
328#[cfg(feature = "sentry")]
329/// Captures a panic with sentry and sends all previously captured logs.
330pub fn capture_panic(metadata: &Metadata, info: &PanicInfo<'_>) {
331    let _guard = sentry::init(sentry_config(metadata.release.clone()));
332    set_metadata(metadata);
333    send_logs();
334    let uuid = sentry::capture_event(PanicIntegration::new().event_from_panic_info(info));
335    print_uuid(uuid);
336}
337
338/// Ask for a line of text.
339fn text() -> std::io::Result<String> {
340    // Read up to the first newline or EOF.
341
342    let mut out = String::new();
343    std::io::stdin().read_line(&mut out)?;
344
345    // Only capture up to the first newline.
346    if let Some(mut newline) = out.find('\n') {
347        if newline > 0 && out.as_bytes()[newline - 1] == b'\r' {
348            newline -= 1;
349        }
350        out.truncate(newline);
351    }
352
353    Ok(out)
354}
355
356const SENTRY_HINT: &str = r"Unfortunately probe-rs encountered an unhandled problem. To help the devs, you can automatically log the error to sentry.io. Your data will be transmitted completely anonymously and cannot be associated with you directly. To hide this message in the future, please set $PROBE_RS_SENTRY to 'true' or 'false'. Do you wish to transmit the data? Y/n: ";
357
358/// Chunks the given string into pieces of maximum_length whilst honoring word boundaries.
359fn chunk_string(s: &str, max_width: usize) -> Vec<String> {
360    let string = s.chars().collect::<Vec<char>>();
361
362    let mut result = vec![];
363
364    let mut last_ws = 0;
365    let mut offset = 0;
366    let mut i = 0;
367    let mut t_max_width = max_width;
368    while i < string.len() {
369        let c = string[i];
370        if c.is_whitespace() {
371            last_ws = i;
372        }
373        if i > offset + t_max_width {
374            if last_ws > offset {
375                let s = string[offset..last_ws].iter().collect::<String>();
376                result.push(s);
377                t_max_width = max_width;
378            } else {
379                t_max_width += 1;
380            }
381
382            offset = last_ws + 1;
383            i = last_ws + 1;
384        } else {
385            i += 1;
386        }
387    }
388    result.push(string[offset..].iter().collect::<String>());
389    result
390}
391
392/// Displays the text to ask if the crash should be reported.
393pub fn ask_to_log_crash() -> bool {
394    if let Ok(var) = std::env::var("PROBE_RS_SENTRY") {
395        var == "true"
396    } else {
397        let size = terminal_size::terminal_size();
398        if let Some((Width(w), Height(_h))) = size {
399            let lines = chunk_string(SENTRY_HINT, w as usize - 14);
400
401            for (i, l) in lines.iter().enumerate() {
402                if i == 0 {
403                    println!("        {} {}", "Hint".blue().bold(), l);
404                } else if i == lines.len() - 1 {
405                    print!("             {l}");
406                } else {
407                    println!("             {l}");
408                }
409            }
410        } else {
411            print!("{SENTRY_HINT}");
412        }
413
414        std::io::stdout().flush().ok();
415        let result = if let Ok(s) = text() {
416            let s = s.to_lowercase();
417            "yes".starts_with(&s)
418        } else {
419            false
420        };
421
422        println!();
423
424        result
425    }
426}
427
428/// Writes an error to stderr.
429/// This function respects the progress bars of the CLI that might be displayed and displays the message above it if any are.
430pub fn eprintln(message: impl AsRef<str>) {
431    if let Ok(guard) = PROGRESS_BAR.try_write() {
432        match guard.as_ref() {
433            Some(pb) if !pb.is_finished() => {
434                pb.println(message.as_ref());
435            }
436            _ => {
437                eprintln!("{}", message.as_ref());
438            }
439        }
440    } else {
441        eprintln!("{}", message.as_ref());
442    }
443}
444
445/// Writes a message to stdout with a newline at the end.
446/// This function respects the progress bars of the CLI that might be displayed and displays the message above it if any are.
447pub fn println(message: impl AsRef<str>) {
448    if let Ok(guard) = PROGRESS_BAR.try_write() {
449        match guard.as_ref() {
450            Some(pb) if !pb.is_finished() => {
451                pb.println(message.as_ref());
452            }
453            _ => {
454                println!("{}", message.as_ref());
455            }
456        }
457    } else {
458        println!("{}", message.as_ref());
459    }
460}