Skip to main content

mtlog_tokio/
lib.rs

1//! # mtlog-tokio
2//! Scoped logging for tokio runtimes with support for log files.
3//!
4//! ## Usage
5//! ```toml
6//! // Cargo.toml
7//! ...
8//! [dependencies]
9//! mtlog-tokio = "0.2"
10//! tokio = {version = "1", features = ["full"]}
11//! ```
12//!
13//! ```rust
14//! use mtlog_tokio::logger_config;
15//!
16//! #[tokio::main]
17//! async fn main() {
18//!     logger_config()
19//!         .scope_global(async move {
20//!             log::info!("Hello, world!");
21//!             // logs are automatically flushed when scope_global completes
22//!         }).await;
23//! }
24//! ```
25//!
26//! ## Multi-threaded logging
27//! ```rust
28//! use mtlog_tokio::logger_config;
29//!
30//! #[tokio::main]
31//! async fn main() {
32//!     logger_config()
33//!         .with_name("main")
34//!         .scope_global(async move {
35//!             log::info!("Hello, world from main thread!");
36//!             let handles: Vec<_> = (0..5).map(|i| {
37//!                 tokio::spawn(async move {
38//!                     logger_config()
39//!                         .with_name(&format!("thread {i}"))
40//!                         .scope_local(async move {
41//!                             log::warn!("Hello, world from thread {i}!")
42//!                         }).await;
43//!                 })
44//!             }).collect();
45//!             for h in handles { h.await.unwrap(); }
46//!             // logs are automatically flushed when scope_global completes
47//!         }).await;
48//! }
49//! ```
50//!
51//! ## Logging to files
52//! Files can be used to log messages. The log file is created if it does not exist and appended to if it does.
53//! Threads can log to different files. If no file is specified in local config, the global file is used.
54//!
55//! ```rust
56//! use mtlog_tokio::logger_config;
57//!
58//! #[tokio::main]
59//! async fn main() {
60//!     logger_config()
61//!         .with_log_file("/tmp/app.log")
62//!         .unwrap()
63//!         .no_stdout() // disable stdout logging if needed
64//!         .scope_global(async move {
65//!             log::info!("Hello, world!");
66//!             // logs are automatically flushed when scope_global completes
67//!         }).await;
68//!     assert!(std::fs::read_to_string("/tmp/app.log").unwrap().ends_with("Hello, world!\n"));
69//! }
70//! ```
71
72use log::{LevelFilter, Log};
73use mtlog_core::{
74    spawn_log_thread_file, spawn_log_thread_stdout, FileLogger, LogFile, LogFileSizeRotation,
75    LogFileTimeRotation, LogMessage, LogSender, LogStdout,
76};
77
78pub use mtlog_core::{SizeRotationConfig, TimeRotationConfig};
79use std::{
80    future::Future,
81    path::Path,
82    sync::{Arc, LazyLock, RwLock},
83};
84
85/// Configuration for the logger.
86#[derive(Clone)]
87struct LogConfig {
88    /// Optional log message sender to a thread handling file logging.
89    sender_file: Option<Arc<LogSender>>,
90    /// Optional log message sender to a thread handling stdout.
91    sender_stdout: Option<Arc<LogSender>>,
92    /// Optional logger name.
93    name: Option<String>,
94    /// Maximum log level
95    level: LevelFilter,
96}
97
98/// Global configuration for the logger, accessible across threads.
99static GLOBAL_LOG_CONFIG: LazyLock<Arc<RwLock<LogConfig>>> = LazyLock::new(|| {
100    log::set_boxed_logger(Box::new(MTLogger)).unwrap();
101    log::set_max_level(LevelFilter::Info);
102    let sender = spawn_log_thread_stdout(LogStdout::default());
103    Arc::new(RwLock::new(LogConfig {
104        sender_stdout: Some(Arc::new(sender)),
105        sender_file: None,
106        name: None,
107        level: LevelFilter::Info,
108    }))
109});
110
111tokio::task_local! {
112    /// Thread-local logger configuration for finer control over logging settings per thread.
113    pub static LOG_CONFIG: LogConfig;
114}
115
116/// Custom logger implementation for handling log records.
117struct MTLogger;
118
119impl Log for MTLogger {
120    fn enabled(&self, _: &log::Metadata) -> bool {
121        true
122    }
123
124    fn log(&self, record: &log::Record) {
125        LOG_CONFIG.with(|config| {
126            let level = record.level();
127            if level > config.level {
128                return;
129            }
130            let log_message = Arc::new(LogMessage {
131                level,
132                name: config.name.clone(),
133                message: record.args().to_string(),
134            });
135            if let Some(sender) = &config.sender_stdout {
136                sender
137                    .send(log_message.clone())
138                    .expect("Unable to send log message to stdout logging thread");
139            }
140            if let Some(sender) = &config.sender_file {
141                sender
142                    .send(log_message)
143                    .expect("Unable to send log message to file logging thread");
144            }
145        });
146    }
147
148    fn flush(&self) {
149        if let Some(s) = GLOBAL_LOG_CONFIG.write().unwrap().sender_stdout.as_deref() {
150            s.shutdown();
151        }
152        if let Some(s) = GLOBAL_LOG_CONFIG.write().unwrap().sender_file.as_deref() {
153            s.shutdown();
154        }
155    }
156}
157
158/// Builder for configuring and initializing the logger.
159pub struct ConfigBuilder {
160    log_file: Option<FileLogger>,
161    no_stdout: bool,
162    no_file: bool,
163    log_level: LevelFilter,
164    name: Option<String>,
165}
166
167impl Default for ConfigBuilder {
168    fn default() -> Self {
169        Self {
170            log_file: None,
171            no_stdout: false,
172            no_file: false,
173            log_level: LevelFilter::Info,
174            name: None,
175        }
176    }
177}
178
179impl ConfigBuilder {
180    fn build(self) -> LogConfig {
181        let Self {
182            log_file,
183            no_stdout,
184            no_file,
185            log_level,
186            name,
187        } = self;
188        let sender_file = if no_file {
189            None
190        } else if let Some(log_file) = log_file {
191            let sender = spawn_log_thread_file(log_file);
192            Some(Arc::new(sender))
193        } else {
194            GLOBAL_LOG_CONFIG.read().unwrap().sender_file.clone()
195        };
196        let sender_stdout = if no_stdout {
197            None
198        } else {
199            GLOBAL_LOG_CONFIG.read().unwrap().sender_stdout.clone()
200        };
201        LogConfig {
202            sender_file,
203            sender_stdout,
204            name,
205            level: log_level,
206        }
207    }
208
209    /// Sets a log file.
210    pub fn with_log_file<P: AsRef<Path>>(self, path: P) -> Result<Self, std::io::Error> {
211        Ok(Self {
212            log_file: Some(FileLogger::Single(LogFile::new(path)?)),
213            ..self
214        })
215    }
216    /// Maybe sets a log file.
217    pub fn maybe_with_log_file<P: AsRef<Path>>(
218        self,
219        path: Option<P>,
220    ) -> Result<Self, std::io::Error> {
221        Ok(Self {
222            log_file: path
223                .map(|p| LogFile::new(p).map(FileLogger::Single))
224                .transpose()?,
225            ..self
226        })
227    }
228    /// Sets time-based log file rotation.
229    pub fn with_time_rotation(self, config: TimeRotationConfig) -> Result<Self, std::io::Error> {
230        Ok(Self {
231            log_file: Some(FileLogger::TimeRotation(LogFileTimeRotation::new(config)?)),
232            ..self
233        })
234    }
235    /// Sets size-based log file rotation.
236    pub fn with_size_rotation(self, config: SizeRotationConfig) -> Result<Self, std::io::Error> {
237        Ok(Self {
238            log_file: Some(FileLogger::SizeRotation(LogFileSizeRotation::new(config)?)),
239            ..self
240        })
241    }
242    /// Ignore stdout logging
243    pub fn no_stdout(self) -> Self {
244        Self {
245            no_stdout: true,
246            ..self
247        }
248    }
249    /// Dynamically set the stdout flag.
250    pub fn with_stdout(self, yes: bool) -> Self {
251        Self {
252            no_stdout: !yes,
253            ..self
254        }
255    }
256    /// Ignore file logging
257    pub fn no_file(self) -> Self {
258        Self {
259            no_file: true,
260            ..self
261        }
262    }
263    /// Sets a log name
264    pub fn with_name(self, name: &str) -> Self {
265        Self {
266            name: Some(name.into()),
267            ..self
268        }
269    }
270    /// Maybe sets a log name
271    pub fn maybe_with_name(self, name: Option<&str>) -> Self {
272        Self {
273            name: name.map(String::from),
274            ..self
275        }
276    }
277    /// Initialize the logger globally and run the provided future.
278    /// The logger is automatically shut down when the future completes.
279    pub async fn scope_global<F: Future>(self, f: F) -> F::Output {
280        let config = self.build();
281        let mut senders = Vec::new();
282        if let Some(ref sender) = config.sender_stdout {
283            senders.push(Arc::clone(sender));
284        }
285        if let Some(ref sender) = config.sender_file {
286            senders.push(Arc::clone(sender));
287        }
288        *GLOBAL_LOG_CONFIG.write().unwrap() = config.clone();
289        let result = LOG_CONFIG.scope(config, f).await;
290        // Shutdown all senders to ensure logs are flushed
291        for sender in senders {
292            sender.shutdown();
293        }
294        result
295    }
296    // Initalize the logger for the current thread
297    pub async fn scope_local<F: Future>(self, f: F) -> F::Output {
298        LOG_CONFIG.scope(self.build(), f).await
299    }
300}
301
302/// Returns a default ConfigBuilder for configuring the logger.
303pub fn logger_config() -> ConfigBuilder {
304    ConfigBuilder::default()
305}