Skip to main content

esp_idf_svc/
log.rs

1//! Logging
2use core::fmt::Write;
3
4use alloc::collections::BTreeMap;
5use alloc::string::String;
6
7use ::log::{Level, LevelFilter, Metadata, Record};
8
9use crate::private::common::*;
10use crate::private::cstr::*;
11use crate::private::mutex::Mutex;
12use crate::sys::*;
13
14extern crate alloc;
15
16const RUST_LOG: Option<&str> = option_env!("RUST_LOG");
17
18/// Exposes the newlib/picolibc stdout file descriptor to allow writing formatted
19/// messages to stdout without a std dependency or allocation
20///
21/// Does lock the `stdout` file descriptor on `new` and does release the lock on `drop`,
22/// so that the logging does not get interleaved with other output due to multithreading
23struct EspStdout(*mut FILE);
24
25impl EspStdout {
26    fn new() -> Self {
27        #[cfg(not(esp_idf_libc_picolibc))]
28        let stdout_ptr = unsafe { __getreent().as_mut() }.unwrap()._stdout;
29        #[cfg(esp_idf_libc_picolibc)]
30        let stdout_ptr = unsafe { stdout };
31
32        let file = unsafe { stdout_ptr.as_mut() }.unwrap();
33
34        #[cfg(not(esp_idf_libc_picolibc))]
35        // Copied from here:
36        // https://github.com/bminor/newlib/blob/master/newlib/libc/stdio/local.h#L80
37        // https://github.com/bminor/newlib/blob/3bafe2fae7a0878598a82777c623edb2faa70b74/newlib/libc/include/sys/stdio.h#L13
38        if (file._flags2 & __SNLK as i32) == 0 && (file._flags & __SSTR as i16) == 0 {
39            unsafe {
40                _lock_acquire_recursive(&mut file._lock);
41            }
42        }
43
44        #[cfg(esp_idf_libc_picolibc)]
45        unsafe {
46            _lock_acquire_recursive(&mut file.lock);
47        }
48
49        Self(stdout_ptr)
50    }
51}
52
53impl Drop for EspStdout {
54    fn drop(&mut self) {
55        let file = unsafe { self.0.as_mut() }.unwrap();
56
57        #[cfg(not(esp_idf_libc_picolibc))]
58        // Copied from here:
59        // https://github.com/bminor/newlib/blob/master/newlib/libc/stdio/local.h#L85
60        // https://github.com/bminor/newlib/blob/3bafe2fae7a0878598a82777c623edb2faa70b74/newlib/libc/include/sys/stdio.h#L21
61        if (file._flags2 & __SNLK as i32) == 0 && (file._flags & __SSTR as i16) == 0 {
62            unsafe {
63                _lock_release_recursive(&mut file._lock);
64            }
65        }
66
67        #[cfg(esp_idf_libc_picolibc)]
68        unsafe {
69            _lock_release_recursive(&mut file.lock);
70        }
71    }
72}
73
74impl core::fmt::Write for EspStdout {
75    fn write_str(&mut self, s: &str) -> core::fmt::Result {
76        let slice = s.as_bytes();
77        unsafe {
78            fwrite(slice.as_ptr() as *const _, 1, slice.len() as u32, self.0);
79        }
80
81        Ok(())
82    }
83}
84
85#[allow(non_upper_case_globals)]
86#[allow(non_snake_case)]
87impl From<Newtype<esp_log_level_t>> for LevelFilter {
88    fn from(level: Newtype<esp_log_level_t>) -> Self {
89        match level.0 {
90            esp_log_level_t_ESP_LOG_NONE => LevelFilter::Off,
91            esp_log_level_t_ESP_LOG_ERROR => LevelFilter::Error,
92            esp_log_level_t_ESP_LOG_WARN => LevelFilter::Warn,
93            esp_log_level_t_ESP_LOG_INFO => LevelFilter::Info,
94            esp_log_level_t_ESP_LOG_DEBUG => LevelFilter::Debug,
95            esp_log_level_t_ESP_LOG_VERBOSE => LevelFilter::Trace,
96            _ => LevelFilter::Trace,
97        }
98    }
99}
100
101impl From<LevelFilter> for Newtype<esp_log_level_t> {
102    fn from(level: LevelFilter) -> Self {
103        Newtype(match level {
104            LevelFilter::Off => esp_log_level_t_ESP_LOG_NONE,
105            LevelFilter::Error => esp_log_level_t_ESP_LOG_ERROR,
106            LevelFilter::Warn => esp_log_level_t_ESP_LOG_WARN,
107            LevelFilter::Info => esp_log_level_t_ESP_LOG_INFO,
108            LevelFilter::Debug => esp_log_level_t_ESP_LOG_DEBUG,
109            LevelFilter::Trace => esp_log_level_t_ESP_LOG_VERBOSE,
110        })
111    }
112}
113
114#[allow(non_upper_case_globals)]
115#[allow(non_snake_case)]
116impl From<Newtype<esp_log_level_t>> for Level {
117    fn from(level: Newtype<esp_log_level_t>) -> Self {
118        match level.0 {
119            esp_log_level_t_ESP_LOG_ERROR => Level::Error,
120            esp_log_level_t_ESP_LOG_WARN => Level::Warn,
121            esp_log_level_t_ESP_LOG_INFO => Level::Info,
122            esp_log_level_t_ESP_LOG_DEBUG => Level::Debug,
123            esp_log_level_t_ESP_LOG_VERBOSE => Level::Trace,
124            _ => Level::Trace,
125        }
126    }
127}
128
129impl From<Level> for Newtype<esp_log_level_t> {
130    fn from(level: Level) -> Self {
131        Newtype(match level {
132            Level::Error => esp_log_level_t_ESP_LOG_ERROR,
133            Level::Warn => esp_log_level_t_ESP_LOG_WARN,
134            Level::Info => esp_log_level_t_ESP_LOG_INFO,
135            Level::Debug => esp_log_level_t_ESP_LOG_DEBUG,
136            Level::Trace => esp_log_level_t_ESP_LOG_VERBOSE,
137        })
138    }
139}
140
141/// Trait for a log filter backend that can be used with the `EspIdfLogger`.
142pub trait LogFilterBackend {
143    /// Initialize the log filter backend.
144    fn initialize(&self) {}
145
146    /// Check if logging for the given metadata is enabled.
147    fn enabled(&self, metadata: &Metadata) -> bool;
148}
149
150impl<T> LogFilterBackend for &T
151where
152    T: LogFilterBackend,
153{
154    fn initialize(&self) {
155        (**self).initialize()
156    }
157
158    fn enabled(&self, metadata: &Metadata) -> bool {
159        (**self).enabled(metadata)
160    }
161}
162
163/// Log filter backend based on the ESP-IDF logging configuration.
164///
165/// This filter is useful when the user would like to control the verbosity
166/// of the logging system based on the ESP-IDF configuration settings, which
167/// should apply both to the ESP-IDF native C logging, as well as to logging from Rust.
168///
169/// This backend uses the ESP-IDF logging system to filter log messages based on their target and level.
170/// Specifically:
171/// - The `log` crate is set to max level equal to the `CONFIG_LOG_MAXIMUM_LEVEL` ESP-IDF configuration.
172/// - The `set_target_level` method allows setting the log level for specific log targets
173///   (both targets based on Rust logging - i.e. most often than not Rust modules, as well as native ESP-IDF targets).
174pub struct EspIdfLogFilter {
175    cache: Mutex<BTreeMap<String, CString>>,
176}
177
178impl EspIdfLogFilter {
179    /// Create a new instance of `EspIdfLogFilter`.
180    pub const fn new() -> Self {
181        Self {
182            cache: Mutex::new(BTreeMap::new()),
183        }
184    }
185
186    /// Initialize the ESP-IDF log filter backend.
187    pub fn initialize(&self) {
188        ::log::set_max_level(self.get_max_level());
189    }
190
191    /// Return the maximum log level configured in the ESP-IDF.
192    pub fn get_max_level(&self) -> LevelFilter {
193        LevelFilter::from(Newtype(CONFIG_LOG_MAXIMUM_LEVEL))
194    }
195
196    /// Set the log level for a specific target.
197    ///
198    /// Arguments:
199    /// - `target`: The target for which to set the log level. This can be a Rust log target, or an ESP-IDF native target.
200    /// - `level_filter`: The log level to set for the target.
201    pub fn set_target_level(
202        &self,
203        target: impl AsRef<str>,
204        level_filter: LevelFilter,
205    ) -> Result<(), EspError> {
206        let target = target.as_ref();
207
208        let mut cache = self.cache.lock();
209
210        let ctarget = loop {
211            if let Some(ctarget) = cache.get(target) {
212                break ctarget;
213            }
214
215            let ctarget = to_cstring_arg(target)?;
216
217            cache.insert(target.into(), ctarget);
218        };
219
220        unsafe {
221            esp_log_level_set(
222                ctarget.as_c_str().as_ptr(),
223                Newtype::<esp_log_level_t>::from(level_filter).0,
224            );
225        }
226
227        Ok(())
228    }
229
230    /// Check if logging for the given metadata is enabled,
231    /// based on the ESP-IDF current log level, including taeget-specific log levels.
232    pub fn enabled(&self, metadata: &Metadata) -> bool {
233        let level = Newtype::<esp_log_level_t>::from(metadata.level()).0;
234
235        let mut cache = self.cache.lock();
236
237        let ctarget = loop {
238            if let Some(ctarget) = cache.get(metadata.target()) {
239                break ctarget;
240            }
241
242            if let Ok(ctarget) = to_cstring_arg(metadata.target()) {
243                cache.insert(metadata.target().into(), ctarget);
244            } else {
245                return true;
246            }
247        };
248
249        let max_level = unsafe { esp_log_level_get(ctarget.as_c_str().as_ptr()) };
250        level <= max_level
251    }
252}
253
254impl Default for EspIdfLogFilter {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260impl LogFilterBackend for EspIdfLogFilter {
261    fn initialize(&self) {
262        self.initialize();
263    }
264
265    fn enabled(&self, metadata: &Metadata) -> bool {
266        self.enabled(metadata)
267    }
268}
269
270/// A log filter backend that does not consider the ESP-IDF configuration settings
271/// that control the log verbosity and does not filter anything.
272///
273/// This way, the control of the log verbosity from within Rust is completely disconnected
274/// from the log verbosity for the ESP-IDF native C code.
275impl LogFilterBackend for () {
276    fn enabled(&self, _metadata: &Metadata) -> bool {
277        // Logging verbosity is controlled by the Rust log crate settings
278        true
279    }
280}
281
282static INTEGRATED_LOGGER: EspIdfLogger<EspIdfLogFilter> = EspIdfLogger::new(EspIdfLogFilter::new());
283static LOGGER: EspIdfLogger = EspIdfLogger::new(());
284
285/// A type alias for the ESP-IDf logger configured with the ESP-IDF log filter.
286///
287/// For backwards compatibility.
288pub type EspLogger = EspIdfLogger<EspIdfLogFilter>;
289
290impl EspIdfLogger<EspIdfLogFilter> {
291    /// For backwards compatibility
292    ///
293    /// Equivalent to calling `init_from_esp_idf()`
294    pub fn initialize_default() {
295        init_from_esp_idf();
296    }
297}
298
299/// A logger that integrates with the ESP-IDF logging system.
300///
301/// Specifically:
302/// - It logs to `stdout`/`stderr` just like the ESP-IDF native C logging functions
303/// - The format of the logs matches the ESP-IDF native C logging format
304/// - If the `EspIdfLogFilter` backend is used, it respects the ESP-IDF log level configuration
305#[derive(Debug)]
306pub struct EspIdfLogger<T = ()> {
307    filter: T,
308}
309
310impl<T> EspIdfLogger<T> {
311    /// Create a new instance of `EspIdfLogger` with the specified log filter backend.
312    ///
313    /// # Arguments
314    /// - `filter`: The log filter backend to use for filtering log messages.
315    pub const fn new(filter: T) -> Self {
316        Self { filter }
317    }
318
319    /// Return a reference to the log filter backend used by this logger.
320    pub fn filter(&self) -> &T {
321        &self.filter
322    }
323
324    fn get_marker(level: Level) -> &'static str {
325        match level {
326            Level::Error => "E",
327            Level::Warn => "W",
328            Level::Info => "I",
329            Level::Debug => "D",
330            Level::Trace => "V",
331        }
332    }
333
334    fn get_color(_level: Level) -> Option<u8> {
335        #[cfg(esp_idf_log_colors)]
336        {
337            match _level {
338                Level::Error => Some(31), // LOG_COLOR_RED
339                Level::Warn => Some(33),  // LOG_COLOR_BROWN
340                Level::Info => Some(32),  // LOG_COLOR_GREEN,
341                _ => None,
342            }
343        }
344
345        #[cfg(not(esp_idf_log_colors))]
346        {
347            None
348        }
349    }
350}
351
352impl<T> ::log::Log for EspIdfLogger<T>
353where
354    T: LogFilterBackend + Send + Sync,
355{
356    fn enabled(&self, metadata: &Metadata) -> bool {
357        self.filter.enabled(metadata)
358    }
359
360    fn log(&self, record: &Record) {
361        let metadata = record.metadata();
362
363        if self.enabled(metadata) {
364            let marker = Self::get_marker(metadata.level());
365            let target = record.metadata().target();
366            let args = record.args();
367            let color = Self::get_color(record.level());
368
369            let mut stdout_writer = EspStdout::new();
370
371            if let Some(color) = color {
372                write!(stdout_writer, "\x1b[0;{color}m").unwrap();
373            }
374            write!(stdout_writer, "{marker} (").unwrap();
375            if cfg!(esp_idf_log_timestamp_source_rtos) {
376                let timestamp = unsafe { esp_log_timestamp() };
377                write!(stdout_writer, "{timestamp}").unwrap();
378            } else if cfg!(esp_idf_log_timestamp_source_system) {
379                // TODO: https://github.com/esp-rs/esp-idf-svc/pull/494 - official usage of
380                // `esp_log_timestamp_str()` should be tracked and replace the not thread-safe
381                // `esp_log_system_timestamp()` which has a race condition flaw due to
382                // returning a pointer to a static buffer containing the c-string.
383                let timestamp =
384                    unsafe { CStr::from_ptr(esp_log_system_timestamp()).to_str().unwrap() };
385                write!(stdout_writer, "{timestamp}").unwrap();
386            }
387            write!(stdout_writer, ") {target}: {args}").unwrap();
388            if color.is_some() {
389                write!(stdout_writer, "\x1b[0m").unwrap();
390            }
391            writeln!(stdout_writer).unwrap();
392        }
393    }
394
395    fn flush(&self) {}
396}
397
398/// Initialize the Rust logging system with the ESP-IDF logger and with the noop log filter backend
399/// (i.e. logging verbosity is controlled by the Rust log crate settings and disconnected from the ESP-IDF configuration settings).
400///
401/// Arguments:
402/// - `filter`: The log level filter to set in the `log` crate.
403pub fn init(filter: LevelFilter) -> &'static EspIdfLogger<()> {
404    init_with_logger(&LOGGER);
405
406    ::log::set_max_level(filter);
407
408    &LOGGER
409}
410
411/// Initialize the Rust logging system with the ESP-IDF logger and with the noop log filter backend
412/// (i.e. logging verbosity is controlled by the `RUST_LOG` environment variable).
413///
414/// This function reads the `RUST_LOG` environment variable to determine the log level.
415pub fn init_from_env() -> &'static EspIdfLogger<()> {
416    let level = match RUST_LOG.unwrap_or("info").to_ascii_lowercase().as_str() {
417        "off" | "none" => LevelFilter::Off,
418        "error" => LevelFilter::Error,
419        "warn" | "warning" => LevelFilter::Warn,
420        "info" => LevelFilter::Info,
421        "debug" => LevelFilter::Debug,
422        "trace" => LevelFilter::Trace,
423        _ => LevelFilter::Info, // Default to Info if the level is not recognized
424    };
425
426    init(level)
427}
428
429/// Initialize the Rust logging system with the ESP-IDF logger and with the ESP-IDF log filter backend
430/// (i.e. logging verbosity is controlled by the ESP-IDF configuration settings).
431pub fn init_from_esp_idf() -> &'static EspIdfLogger<EspIdfLogFilter> {
432    init_with_logger(&INTEGRATED_LOGGER);
433
434    &INTEGRATED_LOGGER
435}
436
437/// Initialize the Rust logging system with the provided ESP-IDF logger.
438fn init_with_logger<T>(logger: &'static EspIdfLogger<T>)
439where
440    T: LogFilterBackend + Send + Sync,
441{
442    ::log::set_logger(logger)
443        .map(|()| logger.filter().initialize())
444        .unwrap();
445}