unros_core/logging/
mod.rs1use 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#[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
73pub 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
103pub(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 .level(log::LevelFilter::Debug)
162 .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 .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()
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}