debug_log/
lib.rs

1//!
2//! Dead simple log utils for debug in Rust.
3//!
4//! - 🦀 Enabled only in debug mode when DEBUG environment variable is set
5//! - 🔊 Only perform log in files whose paths match `DEBUG="filename"`. Match all by
6//!   using `DEBUG=""`, or `DEBUG="*"`
7//! - 📦 Group output with `debug_group`
8//! - 📤 WASM support. It will use the console API.
9//!
10//! The output log is super easy to read on VS Code with sticky scroll enabled.
11//!
12//! <img src="https://user-images.githubusercontent.com/18425020/202741062-0467b470-32ca-4a23-b280-73fa7d4c7868.gif" width="600"/>
13//!
14//! # Example
15//!
16//! ```rust
17//! use debug_log::{debug_dbg, debug_log, group};
18//! group!("A Group");
19//! {
20//!     group!("Sub A Group");
21//!     let arr: Vec<_> = (0..3).collect();
22//!     debug_dbg!(&arr);
23//!     {
24//!         group!("Sub Sub A Group");
25//!         debug_dbg!(&arr);
26//!     }
27//!     debug_log!("Hi");
28//!     debug_dbg!(&arr);
29//! }
30//!
31//! {
32//!     group!("B Group");
33//!     debug_log!("END");
34//! }
35//! ```
36//!
37//! Run with `DEBUG=* cargo run`
38//!
39//! Output
40//!
41//! ```log
42//! A Group {
43//!     Sub A Group {
44//!         [src/lib.rs:144] &arr = [
45//!             0,
46//!             1,
47//!             2,
48//!         ]
49//!         Sub Sub A Group {
50//!             [src/lib.rs:147] &arr = [
51//!                 0,
52//!                 1,
53//!                 2,
54//!             ]
55//!         }
56//!         [src/lib.rs:150] Hi
57//!         [src/lib.rs:151] &arr = [
58//!             0,
59//!             1,
60//!             2,
61//!         ]
62//!     }
63//!     B Group {
64//!         [src/lib.rs:157] END
65//!     }
66//! }
67//! ```
68
69#[cfg(all(debug_assertions))]
70mod debug {
71    use std::sync::Mutex;
72
73    use once_cell::sync::Lazy;
74
75    static DEBUG: Lazy<Mutex<Option<String>>> =
76        Lazy::new(|| Mutex::new(std::option_env!("DEBUG").map(|x| x.to_owned())));
77    static LEVELS: Mutex<Vec<String>> = Mutex::new(Vec::new());
78
79    /// Change the DEBUG value to filter tests
80    pub fn set_debug(s: &str) {
81        *DEBUG.lock().unwrap() = Some(s.to_owned());
82    }
83
84    pub mod console {
85        #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
86        use wasm_bindgen::prelude::wasm_bindgen;
87
88        #[cfg(all(feature = "wasm", target_arch = "wasm32"))]
89        #[wasm_bindgen]
90        extern "C" {
91            // Use `js_namespace` here to bind `console.log(..)` instead of just
92            // `log(..)`
93            #[wasm_bindgen(js_namespace = console)]
94            pub fn log(s: &str);
95
96            #[wasm_bindgen(js_namespace = console)]
97            pub fn group(s: &str);
98
99            #[wasm_bindgen(js_namespace = console)]
100            pub fn groupEnd();
101        }
102
103        #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
104        pub use patch::*;
105        #[cfg(not(all(feature = "wasm", target_arch = "wasm32")))]
106        mod patch {
107            pub fn log(s: &str) {
108                eprintln!("{}", s);
109            }
110
111            pub fn group(s: &str) {
112                eprintln!("{}", s);
113            }
114
115            pub fn groupEnd() {}
116        }
117    }
118
119    #[doc(hidden)]
120    #[macro_export]
121    macro_rules! inner_println {
122        ($($arg:tt)+) => {{
123            if $crate::should_log(&file!()) {
124                if cfg!(all(feature = "wasm", target_arch = "wasm32")) {
125                    let s = format!($($arg)+);
126                    $crate::console::log(&s);
127                } else {
128                    eprintln!($($arg)+);
129                }
130            }
131        }};
132        () => {
133            if $crate::should_log(&file!()) {
134                if cfg!(all(feature = "wasm", target_arch = "wasm32")) {
135                    $crate::console::log("");
136                } else {
137                    eprintln!();
138                }
139            }
140        };
141    }
142
143    #[doc(hidden)]
144    pub fn get_level() -> usize {
145        LEVELS.lock().unwrap().len()
146    }
147
148    #[doc(hidden)]
149    pub fn indent(name: &str) {
150        let space = format!("{}", "    ".repeat(get_level()));
151        inner_println!("{}{} {{", space, name);
152        LEVELS.lock().unwrap().push(name.to_string())
153    }
154
155    #[doc(hidden)]
156    pub fn outdent() {
157        LEVELS.lock().unwrap().pop();
158        let space = format!("{}", "    ".repeat(get_level()));
159        inner_println!("{}}}", space);
160    }
161
162    #[doc(hidden)]
163    pub fn dbg<T: std::fmt::Debug>(value: T, name: &str, line: &str) {
164        let s = format!("{:#?}", value);
165        let mut ans = String::new();
166        ans.push_str(&"    ".repeat(get_level()));
167        ans.push_str(format!("[{}] {} = ", line, name).as_str());
168        for (i, line) in s.split('\n').enumerate() {
169            if i != 0 {
170                ans.push_str(&"    ".repeat(get_level()));
171            }
172            ans.push_str(line);
173            ans.push('\n')
174        }
175
176        if ans.ends_with('\n') {
177            ans.drain(ans.len() - 1..);
178        }
179
180        inner_println!("{}", ans);
181    }
182
183    #[doc(hidden)]
184    pub fn prepend_indent(s: String) -> String {
185        let mut ans = String::new();
186        for (i, line) in s.split('\n').enumerate() {
187            if i != 0 {
188                ans.push_str(&"    ".repeat(get_level()));
189            }
190            ans.push_str(line);
191            ans.push('\n')
192        }
193        ans
194    }
195
196    #[doc(hidden)]
197    pub fn should_log(file: &str) -> bool {
198        let lock = DEBUG.lock().unwrap();
199        lock.as_ref()
200            .map_or(false, |x| !x.is_empty() && (x == "*" || file.contains(x)))
201    }
202
203    /// Group the following logs until the guard is dropped
204    #[macro_export]
205    macro_rules! group {
206        ($($arg:tt)*) => {
207            let __debug_log_group_guard = {
208                let line = format!("{}:{}", file!(), line!());
209                let mut guard = None;
210                if $crate::should_log(&line) {
211                    $crate::indent(&format!($($arg)*));
212                    guard = Some($crate::GroupGuard);
213                }
214                guard
215            };
216        };
217        () => {
218            let mut __debug_log_group_guard= None;
219            if $crate::should_log(&file!()) {
220                $crate::indent("".to_string());
221                __debug_log_group_guard = Some($crate::GroupGuard);
222            }
223        };
224    }
225
226    #[doc(hidden)]
227    pub struct GroupGuard;
228    impl Drop for GroupGuard {
229        fn drop(&mut self) {
230            crate::outdent();
231        }
232    }
233
234    /// It can be filtered by DEBUG env and can only log on debug mode
235    #[macro_export]
236    macro_rules! debug_dbg {
237        ($($val:expr),+ $(,)?) => {
238            let line = format!("{}:{}", file!(), line!());
239            if $crate::should_log(&line) {
240                ($($crate::dbg($val, stringify!($val), &line)),+,);
241            }
242        };
243        () => {
244            let line = format!("{}:{}", file!(), line!());
245            if $crate::should_log(&line) {
246                let space = format!("{}", "    ".repeat($crate::get_level()));
247                $crate::inner_println!("{}[{}] ",space, line);
248            }
249        }
250    }
251
252    /// Use it like println!(). Except it can be filtered by DEBUG env and can only log on debug mode
253    #[macro_export]
254    macro_rules! debug_log {
255        ($($arg:tt)*) => {{
256            let line = format!("{}:{}", file!(), line!());
257            if $crate::should_log(&line) {
258                let prefix = format!("{}[{}] ", "    ".repeat($crate::get_level()), line);
259                let s = format!($($arg)*);
260                $crate::inner_println!("{}{}", prefix, $crate::prepend_indent(s));
261            }
262        }};
263        () => {
264            if $crate::should_log(&file!()) {
265                $crate::inner_println();
266            }
267        };
268    }
269}
270
271#[cfg(not(debug_assertions))]
272mod debug {
273    pub fn set_debug(s: &str) {}
274
275    /// Group the following logs until the guard is dropped
276    #[macro_export]
277    macro_rules! group {
278        ($($arg:tt)*) => {};
279        () => {};
280    }
281
282    /// Use it like println!(). Except it can be filtered by DEBUG env and can only log on debug mode
283    #[macro_export]
284    macro_rules! debug_log {
285        ($($arg:tt)*) => {{}};
286        () => {};
287    }
288
289    /// It's just dbg!() with indent and can be filtered by DEBUG env
290    #[macro_export]
291    macro_rules! debug_dbg {
292        ($($val:expr),+ $(,)?) => {};
293        () => {};
294    }
295
296    #[doc(hidden)]
297    pub struct GroupGuard;
298}
299
300pub use debug::*;
301
302#[cfg(test)]
303mod tests {
304    use crate::{debug_dbg, debug_log, group};
305
306    #[test]
307    /// Run this test with
308    /// DEBUG=* cargo test -- --nocapture &> data.log
309    fn it_works() {
310        group!("A Group");
311        group!("C Group");
312        {
313            group!("Sub A Group");
314            let arr: Vec<_> = (0..3).collect();
315            debug_dbg!(&arr);
316            {
317                group!("Sub Sub A Group");
318                debug_dbg!(&arr);
319            }
320            debug_log!("Hi");
321            debug_dbg!(&arr);
322        }
323
324        {
325            group!("B Group");
326            debug_log!("END");
327        }
328    }
329}