quick_tracing_derive/
lib.rs

1//! Initializing a multithreaded logger in Rust.(Derive)
2//!
3//! This library provides functionality to initialize a multithreaded logger with flexible configuration options.
4//! It supports both file I/O logging and logging to stdout with ANSI color support.
5use proc_macro2::TokenStream;
6use quote::quote;
7use syn::{parse_macro_input, ItemFn};
8
9/// A builder for initializing a multi-threaded logger.
10///
11/// Allows configuring the logger with optional settings for
12/// test name, standard I/O output, and log level filter.
13#[derive(deluxe::ParseMetaItem)]
14struct LoggerBuilder {
15    file: Option<String>,
16    test: Option<String>,
17    #[deluxe(default = true)]
18    stdio: bool,
19    #[deluxe(default = "TRACE".to_string())]
20    level: String,
21}
22
23pub(crate) fn parse_level(level: &str) -> TokenStream {
24    let level = level.to_ascii_lowercase();
25    let level = match level.as_str() {
26        "trace" => quote! { tracing::metadata::LevelFilter::TRACE },
27        "debug" => quote! { tracing::metadata::LevelFilter::DEBUG },
28        "info" => quote! {  tracing::metadata::LevelFilter::INFO },
29        "warn" => quote! {  tracing::metadata::LevelFilter::WARN },
30        "error" => quote! { tracing::metadata::LevelFilter::ERROR },
31        unknown => {
32            panic!("The log level must be one of TRACE|DEBUG|INFO|WARN|ERROR. But got {unknown}")
33        }
34    };
35    quote! { .filter(#level) }
36}
37
38/// An attribute to initialize a logger with various options.
39///
40/// # Attributes
41///
42/// - `test`: Sets the test name for the logger. Log output will be written to a file named `../logs/{test_name}.log`.
43/// - `file`: Sets the file path for the log output.
44/// - `stdio`: Enables standard I/O output for the logger.(default: true)
45/// - `level`: Sets the log level filter (e.g., `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. default: `TRACE`).
46///
47/// # Examples
48///
49/// - Trace mode + stdio
50///
51/// ```ignore
52/// #[quick_tracing::init]
53/// fn main() {
54///     tracing::debug!("Hello, world!");
55/// }
56/// ```
57///
58/// - Debug mode + Output file(If there is no parent directory, it will automatically create one.)
59///
60/// ```ignore
61/// #[quick_tracing::init(level= "DEBUG", file = "./log/test.log", stdio = false)]
62/// fn main() {
63///     tracing::debug!("Hello, world!");
64/// }
65/// ```
66#[proc_macro_attribute]
67pub fn init(
68    attr: proc_macro::TokenStream,
69    item: proc_macro::TokenStream,
70) -> proc_macro::TokenStream {
71    let LoggerBuilder {
72        file,
73        test,
74        stdio,
75        level,
76    } = match deluxe::parse::<LoggerBuilder>(attr) {
77        Ok(desc) => desc,
78        Err(e) => return e.into_compile_error().into(),
79    };
80
81    if file.is_some() && test.is_some() {
82        panic!("Use either one of `test` or `file`.")
83    };
84
85    let test = test.map_or(quote! {}, |name| {
86        quote! {
87            .test_name(#name).expect("Failed to create log directory.")
88        }
89    });
90    let file = file.map_or(quote! {}, |path| {
91        quote! {
92            .file({
93                let _ = std::path::Path::new(#path).parent().map(|dir| std::fs::create_dir_all(dir));
94                #path
95            })
96        }
97    });
98    let stdio = match stdio {
99        true => quote! { .stdio() },
100        false => quote!(),
101    };
102
103    let level_filter = parse_level(&level);
104
105    let logger_init = quote! {
106        let (worker_guard, default_guard) = quick_tracing::builder::LoggerBuilder::new()
107            #stdio
108            #test
109            #file
110            #level_filter
111            .build()
112            .expect("Failed to initialize logger");
113    };
114
115    let mut item_fn = parse_macro_input!(item as ItemFn);
116    item_fn
117        .block
118        .stmts
119        .insert(0, syn::parse2(logger_init).unwrap());
120
121    quote! { #item_fn }.into()
122}
123
124/// An attribute to initialize a logger with various options.
125///
126/// # Attributes
127///
128/// - `test`: Sets the test name for the logger. Log output will be written to a file named `../logs/{test_name}.log`.
129/// - `file`: Sets the file path for the log output.
130/// - `stdio`: Enables standard I/O output for the logger.(default: true)
131/// - `level`: Sets the log level filter (e.g., `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`. default: `TRACE`).
132///
133/// # Examples
134///
135/// - Trace mode + stdio
136///
137/// ```ignore
138/// #[quick_tracing::try_init]
139/// fn main() -> std::io::Result<()> {
140///     tracing::debug!("Hello, world!");
141///     Ok(())
142/// }
143/// ```
144///
145/// - Debug mode + Output file(If there is no parent directory, it will automatically create one.)
146///
147/// ```ignore
148/// #[quick_tracing::try_init(level= "DEBUG", file = "./log/test.log", stdio = false)]
149/// fn main() -> std::io::Result<()> {
150///     tracing::debug!("Hello, world!");
151///     Ok(())
152/// }
153/// ```
154#[proc_macro_attribute]
155pub fn try_init(
156    attr: proc_macro::TokenStream,
157    item: proc_macro::TokenStream,
158) -> proc_macro::TokenStream {
159    let LoggerBuilder {
160        file,
161        test,
162        stdio,
163        level,
164    } = match deluxe::parse::<LoggerBuilder>(attr) {
165        Ok(desc) => desc,
166        Err(e) => return e.into_compile_error().into(),
167    };
168
169    if file.is_some() && test.is_some() {
170        panic!("Use either one of `test` or `file`.")
171    };
172
173    let test = test.map_or(quote! {}, |name| {
174        quote! {
175            .test_name(#name)?
176        }
177    });
178    let file = file.map_or(quote! {}, |path| {
179        quote! {
180            .file({
181                if let Some(dir) = std::path::Path::new(#path).parent() {
182                    std::fs::create_dir_all(dir)?;
183                };
184                #path
185            })
186        }
187    });
188    let stdio = match stdio {
189        true => quote! { .stdio() },
190        false => quote!(),
191    };
192
193    let level_filter = parse_level(&level);
194
195    let logger_init = quote! {
196        let (worker_guard, default_guard) = quick_tracing::builder::LoggerBuilder::new()
197            #stdio
198            #test
199            #file
200            #level_filter
201            .build()?;
202    };
203
204    let mut item_fn = parse_macro_input!(item as ItemFn);
205    item_fn
206        .block
207        .stmts
208        .insert(0, syn::parse2(logger_init).unwrap());
209
210    quote! { #item_fn }.into()
211}