Skip to main content

charset_norm/
log.rs

1//! Diagnostics emitted while detecting.
2//!
3//! Detection reports its progress through a [`Logger`]: which encodings it
4//! tries, why it rejects them, and which one it settles on. Nothing is
5//! emitted by default ([`NoLogger`]). Implement the trait to route messages
6//! elsewhere, or enable the `log` feature and use `LogCrate` to forward them
7//! to the [`log`](https://docs.rs/log) facade.
8//!
9//! Messages are formatted only when [`Logger::enabled`] returns `true` for
10//! their level, so a logger that ignores a level costs nothing for it.
11//!
12//! ```
13//! use std::cell::RefCell;
14//! use charset_norm::{DetectionOptions, Level, Logger, from_bytes_with};
15//!
16//! #[derive(Default)]
17//! struct Collect(RefCell<Vec<String>>);
18//!
19//! impl Logger for Collect {
20//!     fn enabled(&self, level: Level) -> bool {
21//!         level >= Level::Debug
22//!     }
23//!
24//!     fn log(&self, _level: Level, message: &str) {
25//!         self.0.borrow_mut().push(message.to_owned());
26//!     }
27//! }
28//!
29//! let logger = Collect::default();
30//! from_bytes_with("Hello, world! Plain ASCII text.".as_bytes(), &DetectionOptions::default(), &logger);
31//! assert!(!logger.0.borrow().is_empty());
32//! ```
33
34/// Severity of a diagnostic message.
35///
36/// Levels are ordered: `Trace < Debug`.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub enum Level {
39    /// Detailed tracing of the detection process: every candidate tried,
40    /// and the chaos breakdown when
41    /// [`DetectionOptions::explain`](crate::DetectionOptions::explain) is set.
42    Trace,
43    /// Notable detection outcomes, such as the encoding settled on.
44    Debug,
45}
46
47impl Level {
48    /// The equivalent numeric level of Python's `logging` module.
49    #[must_use]
50    pub fn python_level(self) -> i32 {
51        match self {
52            Level::Trace => 5,
53            Level::Debug => 10,
54        }
55    }
56}
57
58/// Receiver for detection diagnostics.
59///
60/// Implementations must be cheap to call from the detector's loop. See the
61/// [module documentation](self) for an example.
62pub trait Logger {
63    /// Whether messages at `level` are wanted. Messages are only formatted
64    /// when this returns `true`.
65    fn enabled(&self, level: Level) -> bool;
66
67    /// Record one message.
68    fn log(&self, level: Level, message: &str);
69}
70
71/// A logger that discards everything.
72#[derive(Clone, Copy, Debug, Default)]
73pub struct NoLogger;
74
75impl Logger for NoLogger {
76    fn enabled(&self, _level: Level) -> bool {
77        false
78    }
79
80    fn log(&self, _level: Level, _message: &str) {}
81}
82
83/// Forwards diagnostics to the [`log`](https://docs.rs/log) facade under the
84/// `charset_norm` target.
85///
86/// [`Level::Trace`] maps to `log::Level::Trace` and [`Level::Debug`] to
87/// `log::Level::Debug`; the facade's filter decides what is emitted.
88///
89/// ```
90/// use charset_norm::{DetectionOptions, from_bytes_with, log::LogCrate};
91///
92/// let results = from_bytes_with(b"Hello, world!", &DetectionOptions::default(), &LogCrate);
93/// # assert!(results.best().is_some());
94/// ```
95#[cfg(feature = "log")]
96#[cfg_attr(docsrs, doc(cfg(feature = "log")))]
97#[derive(Clone, Copy, Debug, Default)]
98pub struct LogCrate;
99
100#[cfg(feature = "log")]
101impl LogCrate {
102    fn level(level: Level) -> log::Level {
103        match level {
104            Level::Trace => log::Level::Trace,
105            Level::Debug => log::Level::Debug,
106        }
107    }
108}
109
110#[cfg(feature = "log")]
111impl Logger for LogCrate {
112    fn enabled(&self, level: Level) -> bool {
113        log::log_enabled!(target: "charset_norm", Self::level(level))
114    }
115
116    fn log(&self, level: Level, message: &str) {
117        log::log!(target: "charset_norm", Self::level(level), "{message}");
118    }
119}
120
121/// Format and emit `message` only when `level` is enabled.
122pub(crate) fn emit(logger: &dyn Logger, level: Level, message: impl FnOnce() -> String) {
123    if logger.enabled(level) {
124        logger.log(level, &message());
125    }
126}