Skip to main content

ax_log/
lib.rs

1//! Macros for multi-level formatted logging used by
2//! [ArceOS](https://github.com/arceos-org/arceos).
3//!
4//! The log macros, in descending order of level, are: [`error!`], [`warn!`],
5//! [`info!`], [`debug!`], and [`trace!`].
6//!
7//! If it is used in `no_std` environment, the users need to implement the
8//! [`LogIf`] to provide external functions such as console output.
9//!
10//! To use in the `std` environment, please enable the `std` feature:
11//!
12//! ```toml
13//! [dependencies]
14//! ax-log = { version = "0.1", features = ["std"] }
15//! ```
16//!
17//! # Cargo features:
18//!
19//! - `std`: Use in the `std` environment. If it is enabled, you can use console
20//!   output without implementing the [`LogIf`] trait. This is disabled by default.
21//!
22//! # Examples
23//!
24//! ```
25//! # #[cfg(feature = "std")]
26//! # {
27//! use ax_log::{debug, error, info, trace, warn};
28//!
29//! // Initialize the logger.
30//! ax_log::init();
31//! // Set the maximum log level to `info`.
32//! ax_log::set_max_level("info");
33//!
34//! // The following logs will be printed.
35//! error!("error");
36//! warn!("warn");
37//! info!("info");
38//!
39//! // The following logs will not be printed.
40//! debug!("debug");
41//! trace!("trace");
42//! # }
43//! ```
44
45#![cfg_attr(not(feature = "std"), no_std)]
46
47extern crate log;
48
49use core::{
50    fmt::{self, Write},
51    str::FromStr,
52};
53
54#[cfg(not(feature = "std"))]
55use ax_crate_interface::call_interface;
56use log::{Level, LevelFilter, Log, Metadata, Record};
57pub use log::{debug, error, info, trace, warn};
58
59/// Prints to the console.
60///
61/// Equivalent to the [`ax_println!`] macro except that a newline is not printed at
62/// the end of the message.
63#[macro_export]
64macro_rules! ax_print {
65    ($($arg:tt)*) => {
66        $crate::__print_impl(format_args!($($arg)*));
67    }
68}
69
70/// Prints to the console, with a newline.
71#[macro_export]
72macro_rules! ax_println {
73    () => { $crate::ax_print!("\n") };
74    ($($arg:tt)*) => {
75        $crate::__print_impl(format_args!("{}\n", format_args!($($arg)*)));
76    }
77}
78
79macro_rules! with_color {
80    ($color_code:expr, $($arg:tt)*) => {
81        format_args!("\u{1B}[{}m{}\u{1B}[m", $color_code as u8, format_args!($($arg)*))
82    };
83}
84
85#[repr(u8)]
86#[allow(dead_code)]
87enum ColorCode {
88    Black         = 30,
89    Red           = 31,
90    Green         = 32,
91    Yellow        = 33,
92    Blue          = 34,
93    Magenta       = 35,
94    Cyan          = 36,
95    White         = 37,
96    BrightBlack   = 90,
97    BrightRed     = 91,
98    BrightGreen   = 92,
99    BrightYellow  = 93,
100    BrightBlue    = 94,
101    BrightMagenta = 95,
102    BrightCyan    = 96,
103    BrightWhite   = 97,
104}
105
106/// Extern interfaces that must be implemented in other crates.
107#[ax_crate_interface::def_interface]
108pub trait LogIf {
109    /// Writes a string to the console.
110    fn console_write_str(s: &str);
111
112    /// Gets current clock time.
113    fn current_time() -> core::time::Duration;
114
115    /// Gets current CPU ID.
116    ///
117    /// Returns [`None`] if you don't want to show the CPU ID in the log.
118    fn current_cpu_id() -> Option<usize>;
119
120    /// Gets current task ID.
121    ///
122    /// Returns [`None`] if you don't want to show the task ID in the log.
123    fn current_task_id() -> Option<u64>;
124}
125
126struct Logger;
127
128impl Write for Logger {
129    fn write_str(&mut self, s: &str) -> fmt::Result {
130        cfg_if::cfg_if! {
131            if #[cfg(feature = "std")] {
132                std::print!("{s}");
133            } else {
134                call_interface!(LogIf::console_write_str, s);
135            }
136        }
137        Ok(())
138    }
139}
140
141const LOG_BUFFER_SIZE: usize = 2048;
142const LOG_TRUNCATED_WITH_NEWLINE: &str = "\u{1B}[m<log truncated>\n";
143
144struct LogBuffer<const N: usize> {
145    buf: [u8; N],
146    len: usize,
147    truncated: bool,
148}
149
150impl<const N: usize> LogBuffer<N> {
151    const fn new() -> Self {
152        Self {
153            buf: [0; N],
154            len: 0,
155            truncated: false,
156        }
157    }
158
159    fn as_str(&self) -> &str {
160        // SAFETY: LogBuffer only appends complete UTF-8 strings or prefixes
161        // ending at UTF-8 character boundaries.
162        unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
163    }
164
165    fn append_truncation_marker(&mut self) {
166        if !self.truncated {
167            return;
168        }
169
170        let marker = LOG_TRUNCATED_WITH_NEWLINE.as_bytes();
171        if N < marker.len() {
172            self.len = 0;
173            return;
174        }
175
176        if self.len + marker.len() > N {
177            let mut keep = self.len.min(N - marker.len());
178            while !self.is_char_boundary(keep) {
179                keep -= 1;
180            }
181            self.len = keep;
182        }
183
184        self.buf[self.len..self.len + marker.len()].copy_from_slice(marker);
185        self.len += marker.len();
186    }
187
188    fn is_char_boundary(&self, index: usize) -> bool {
189        index == 0 || index == self.len || (self.buf[index] & 0b1100_0000) != 0b1000_0000
190    }
191}
192
193impl<const N: usize> Write for LogBuffer<N> {
194    fn write_str(&mut self, s: &str) -> fmt::Result {
195        let available = N.saturating_sub(self.len);
196        if s.len() <= available {
197            self.buf[self.len..self.len + s.len()].copy_from_slice(s.as_bytes());
198            self.len += s.len();
199            return Ok(());
200        }
201
202        self.truncated = true;
203        let end = s
204            .char_indices()
205            .map(|(index, _)| index)
206            .take_while(|&index| index <= available)
207            .last()
208            .unwrap_or(0);
209        self.buf[self.len..self.len + end].copy_from_slice(&s.as_bytes()[..end]);
210        self.len += end;
211        Ok(())
212    }
213}
214
215impl Log for Logger {
216    #[inline]
217    fn enabled(&self, _metadata: &Metadata) -> bool {
218        true
219    }
220
221    fn log(&self, record: &Record) {
222        if !self.enabled(record.metadata()) {
223            return;
224        }
225
226        let level = record.level();
227        let line = record.line().unwrap_or(0);
228        let path = record.target();
229        let args_color = match level {
230            Level::Error => ColorCode::Red,
231            Level::Warn => ColorCode::Yellow,
232            Level::Info => ColorCode::Green,
233            Level::Debug => ColorCode::Cyan,
234            Level::Trace => ColorCode::BrightBlack,
235        };
236
237        cfg_if::cfg_if! {
238            if #[cfg(feature = "std")] {
239                print_log_fmt(with_color!(
240                    ColorCode::White,
241                    "[{time} {path}:{line}] {args}\n",
242                    time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.6f"),
243                    path = path,
244                    line = line,
245                    args = with_color!(args_color, "{}", record.args()),
246                ));
247            } else {
248                let cpu_id = call_interface!(LogIf::current_cpu_id);
249                let tid = call_interface!(LogIf::current_task_id);
250                let now = call_interface!(LogIf::current_time);
251                if let Some(cpu_id) = cpu_id {
252                    if let Some(tid) = tid {
253                        // show CPU ID and task ID
254                        print_log_fmt(with_color!(
255                            ColorCode::White,
256                            "[{:>3}.{:06} {cpu_id}:{tid} {path}:{line}] {args}\n",
257                            now.as_secs(),
258                            now.subsec_micros(),
259                            cpu_id = cpu_id,
260                            tid = tid,
261                            path = path,
262                            line = line,
263                            args = with_color!(args_color, "{}", record.args()),
264                        ));
265                    } else {
266                        // show CPU ID only
267                        print_log_fmt(with_color!(
268                            ColorCode::White,
269                            "[{:>3}.{:06} {cpu_id} {path}:{line}] {args}\n",
270                            now.as_secs(),
271                            now.subsec_micros(),
272                            cpu_id = cpu_id,
273                            path = path,
274                            line = line,
275                            args = with_color!(args_color, "{}", record.args()),
276                        ));
277                    }
278                } else {
279                    // neither CPU ID nor task ID is shown
280                    print_log_fmt(with_color!(
281                        ColorCode::White,
282                        "[{:>3}.{:06} {path}:{line}] {args}\n",
283                        now.as_secs(),
284                        now.subsec_micros(),
285                        path = path,
286                        line = line,
287                        args = with_color!(args_color, "{}", record.args()),
288                    ));
289                }
290            }
291        }
292    }
293
294    fn flush(&self) {}
295}
296
297fn write_fmt_locked(args: fmt::Arguments) -> fmt::Result {
298    use ax_kspin::SpinNoIrq; // TODO: more efficient
299    static LOCK: SpinNoIrq<()> = SpinNoIrq::new(());
300
301    // Panic and oops paths must not re-enter the normal print lock because its
302    // unlock path may restore preemption/IRQs and trigger more complex control
303    // flow while the kernel is already failing.
304    if axpanic::oops_in_progress() {
305        return Logger.write_fmt(args);
306    }
307
308    let _guard = LOCK.lock();
309    Logger.write_fmt(args)
310}
311
312fn print_log_fmt(args: fmt::Arguments) {
313    if axpanic::oops_in_progress() {
314        Logger.write_fmt(args).unwrap();
315        return;
316    }
317
318    // Log records are internal tracing messages with a clear record boundary, so
319    // they may be truncated to keep Display formatting outside the print lock.
320    // They always include a trailing newline, even when ANSI reset bytes are
321    // formatted after it. Keep that newline after replacing truncated content.
322    let mut buf = LogBuffer::<LOG_BUFFER_SIZE>::new();
323    buf.write_fmt(args).unwrap();
324    buf.append_truncation_marker();
325    write_fmt_locked(format_args!("{}", buf.as_str())).unwrap();
326}
327
328/// Prints the formatted string to the console.
329pub fn print_fmt(args: fmt::Arguments) -> fmt::Result {
330    // Direct console output preserves the caller's original bytes and length.
331    // This keeps ax_print!/ax_println! and axstd println! behavior unchanged;
332    // callers that format under this lock must still avoid recursive printing.
333    write_fmt_locked(args)
334}
335
336#[doc(hidden)]
337pub fn __print_impl(args: fmt::Arguments) {
338    print_fmt(args).unwrap();
339}
340
341/// Initializes the logger.
342///
343/// This function should be called before any log macros are used, otherwise
344/// nothing will be printed.
345pub fn init() {
346    log::set_logger(&Logger).unwrap();
347    log::set_max_level(LevelFilter::Warn);
348}
349
350/// Set the maximum log level.
351///
352/// Unlike the features such as `log-level-error`, setting the logging level in
353/// this way incurs runtime overhead. In addition, this function is no effect
354/// when those features are enabled.
355///
356/// `level` should be one of `off`, `error`, `warn`, `info`, `debug`, `trace`.
357pub fn set_max_level(level: &str) {
358    let lf = LevelFilter::from_str(level)
359        .ok()
360        .unwrap_or(LevelFilter::Off);
361    log::set_max_level(lf);
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn log_buffer_truncates_at_utf8_boundary() {
370        let mut buf = LogBuffer::<22>::new();
371        write!(buf, "ab你好xxxxxxxxxxxxxxxx\n").unwrap();
372        buf.append_truncation_marker();
373
374        assert_eq!(buf.as_str(), "ab\u{1B}[m<log truncated>\n");
375    }
376
377    #[test]
378    fn log_buffer_keeps_untruncated_message() {
379        let mut buf = LogBuffer::<32>::new();
380        write!(buf, "short").unwrap();
381        buf.append_truncation_marker();
382
383        assert_eq!(buf.as_str(), "short");
384    }
385}