unros_core/logging/
mod.rs

1use std::{
2    panic::catch_unwind,
3    path::{Path, PathBuf},
4    sync::{Arc, Mutex, OnceLock},
5    time::Instant,
6};
7
8use anyhow::Context;
9use chrono::{Datelike, Timelike};
10use fern::colors::{Color, ColoredLevelConfig};
11use log::Level;
12
13use crate::{
14    logging::eyre::UnrosEyreMessage,
15    pubsub::{
16        Publisher, PublisherRef,
17    },
18    RunOptions,
19};
20
21pub mod dump;
22mod eyre;
23pub mod rate;
24
25/// Sets up a locally available set of logging macros.
26///
27/// The `log` crate allows users to configure the `target`
28/// parameter of a log, allowing developers to better filter
29/// messages by file. Unros takes this one step further by
30/// automatically setting this `target` parameter to be the name
31/// of the current node (as passed by the context). This allows
32/// two nodes of the same class to have different log targets
33/// if their names differ, which should help you to identify
34/// issues faster.
35#[macro_export]
36macro_rules! setup_logging {
37    ($context: ident) => {
38        setup_logging!($context $)
39    };
40    ($context: ident $dol:tt) => {
41        let _context = &$context;
42        #[allow(unused_macros)]
43        macro_rules! info {
44            ($dol($dol arg:tt)+) => {
45                $crate::log::info!(target: $context.get_name(), $dol ($dol arg)+)
46            };
47        }
48        #[allow(unused_macros)]
49        macro_rules! warn {
50            ($dol ($dol arg:tt)+) => {
51                $crate::log::warn!(target: $context.get_name(), $dol ($dol arg)+)
52            };
53        }
54        #[allow(unused_macros)]
55        macro_rules! error {
56            ($dol ($dol arg:tt)+) => {
57                $crate::log::error!(target: $context.get_name(), $dol ($dol arg)+)
58            };
59        }
60        #[allow(unused_macros)]
61        macro_rules! debug {
62            ($dol ($dol arg:tt)+) => {
63                $crate::log::debug!(target: $context.get_name(), $dol ($dol arg)+)
64            };
65        }
66    };
67}
68
69static SUB_LOGGING_DIR: OnceLock<PathBuf> = OnceLock::new();
70pub(crate) static START_TIME: OnceLock<Instant> = OnceLock::new();
71static LOG_PUB: OnceLock<PublisherRef<Arc<str>>> = OnceLock::new();
72
73/// Gets a reference to the `Publisher` for logs.
74/// 
75/// # Panics
76/// Panics if the logger has not been initialized. If this method
77/// is called inside of or after `start_unros_runtime`, the logger is
78/// always initialized.
79pub fn get_log_pub() -> PublisherRef<Arc<str>> {
80    LOG_PUB.get().unwrap().clone()
81}
82
83#[derive(Default)]
84struct LogPub {
85    publisher: Mutex<Publisher<Arc<str>>>,
86}
87
88impl log::Log for LogPub {
89    fn enabled(&self, metadata: &log::Metadata) -> bool {
90        !(metadata.target() == "unros_core::logging::dump" && metadata.level() == Level::Info)
91    }
92
93    fn log(&self, record: &log::Record) {
94        if !self.enabled(record.metadata()) {
95            return;
96        }
97        self.publisher.lock().unwrap().set(format!("{}", record.args()).into_boxed_str().into());
98    }
99
100    fn flush(&self) {}
101}
102
103/// Initializes the default logging implementation.
104///
105/// This is called automatically in `run_all` and `async_run_all`, but
106/// there may be additional logs produced before these methods that would
107/// be ignored if the logger was not set up yet. As such, you may call this
108/// method manually, when needed. Calling this multiple times is safe and
109/// will not return errors.
110pub(super) fn init_logger(run_options: &RunOptions) -> anyhow::Result<()> {
111    const LOGS_DIR: &str = "logs";
112
113    SUB_LOGGING_DIR.get_or_try_init::<_, anyhow::Error>(|| {
114        color_eyre::config::HookBuilder::default()
115            .panic_message(UnrosEyreMessage)
116            .install()
117            .map_err(|e| anyhow::anyhow!(e))?;
118
119        if !AsRef::<Path>::as_ref(LOGS_DIR)
120            .try_exists()
121            .context("Failed to check if logging directory exists. Do we have permissions?")?
122        {
123            std::fs::DirBuilder::new()
124                .create(LOGS_DIR)
125                .context("Failed to create logging directory. Do we have permissions?")?;
126        }
127        let mut runtime_name = run_options.runtime_name.to_string();
128        if !runtime_name.is_empty() {
129            runtime_name = "=".to_string() + &runtime_name;
130        }
131
132        let datetime = chrono::Local::now();
133        let log_folder_name = format!(
134            "{}-{:0>2}-{:0>2}={:0>2}-{:0>2}-{:0>2}{}",
135            datetime.year(),
136            datetime.month(),
137            datetime.day(),
138            datetime.hour(),
139            datetime.minute(),
140            datetime.second(),
141            runtime_name,
142        );
143
144        let log_folder_name = PathBuf::from(LOGS_DIR).join(log_folder_name);
145
146        std::fs::DirBuilder::new()
147            .create(&log_folder_name)
148            .context("Failed to create sub-logging directory. Do we have permissions?")?;
149
150        let colors = ColoredLevelConfig::new()
151            .warn(Color::Yellow)
152            .error(Color::Red)
153            .trace(Color::BrightBlack);
154
155        let _ = START_TIME.set(Instant::now());
156
157        let log_pub: Box<dyn log::Log> = Box::new(LogPub::default());
158
159        fern::Dispatch::new()
160            // Add blanket level filter -
161            .level(log::LevelFilter::Debug)
162            // Output to stdout, files, and other Dispatch configurations
163            .chain(
164                fern::Dispatch::new()
165                    .format(move |out, message, record| {
166                        let secs = START_TIME.get().unwrap().elapsed().as_secs_f32();
167                        out.finish(format_args!(
168                            "[{:0>1}:{:.2} {} {}] {}",
169                            (secs / 60.0).floor(),
170                            secs % 60.0,
171                            record.level(),
172                            record.target(),
173                            message
174                        ));
175                    })
176                    .chain(
177                        fern::log_file(log_folder_name.join(".log"))
178                            .context("Failed to create log file. Do we have permissions?")?,
179                    )
180                    .chain(log_pub),
181            )
182            .chain(
183                fern::Dispatch::new()
184                    .level(log::LevelFilter::Info)
185                    // This filter is to avoid logging panics to the console, since rust already does that.
186                    // Note that the 'panic' target is set by us in eyre.rs.
187                    .filter(|x| x.target() != "panic")
188                    .filter(|x| {
189                        !(x.target() == "unros_core::logging::dump" && x.level() == Level::Info)
190                    })
191                    .format(move |out, message, record| {
192                        let secs = START_TIME.get().unwrap().elapsed().as_secs_f32();
193                        out.finish(format_args!(
194                            "\x1B[{}m[{:0>1}:{:.2} {}] {}\x1B[0m",
195                            colors.get_color(&record.level()).to_fg_str(),
196                            (secs / 60.0).floor(),
197                            secs % 60.0,
198                            record.target(),
199                            message
200                        ));
201                    })
202                    .chain(std::io::stdout()),
203            )
204            // Apply globally
205            .apply()
206            .context("Logger should have initialized correctly")?;
207
208        if run_options.enable_console_subscriber {
209            if let Err(e) = catch_unwind(console_subscriber::init) {
210                log::error!("Failed to initialize console subscriber: {e:?}");
211            }
212        }
213        Ok(log_folder_name)
214    })?;
215
216    Ok(())
217}