iroh_node_util/
logging.rs

1//! Utilities for logging
2use std::{env, path::Path};
3
4use derive_more::FromStr;
5use serde::{Deserialize, Serialize};
6use serde_with::{DeserializeFromStr, SerializeDisplay};
7use tracing_appender::{non_blocking, rolling};
8use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, Layer};
9
10/// `RUST_LOG` statement used by default in file logging.
11// rustyline is annoying
12pub(crate) const DEFAULT_FILE_RUST_LOG: &str = "rustyline=warn,debug";
13
14/// Parse `<bin>_FILE_RUST_LOG` as [`tracing_subscriber::EnvFilter`]. Returns `None` if not
15/// present.
16pub fn env_file_rust_log(bin: &'static str) -> Option<anyhow::Result<EnvFilter>> {
17    let env_file_rust_log = format!("{}_FILE_RUST_LOG", bin.to_uppercase());
18    match env::var(env_file_rust_log) {
19        Ok(s) => Some(crate::logging::EnvFilter::from_str(&s).map_err(Into::into)),
20        Err(e) => match e {
21            env::VarError::NotPresent => None,
22            e @ env::VarError::NotUnicode(_) => Some(Err(e.into())),
23        },
24    }
25}
26
27/// Initialize logging both in the terminal and file based.
28///
29/// The terminal based logging layer will:
30/// - use the default [`fmt::format::Format`].
31/// - log to [`std::io::Stderr`]
32///
33/// The file base logging layer will:
34/// - use the default [`fmt::format::Format`] save for:
35///   - including line numbers.
36///   - not using ansi colors.
37/// - create log files in the [`FileLogging::dir`] directory. If not provided, the `logs` dir
38///   inside the given `logs_root` is used.
39/// - rotate files every [`FileLogging::rotation`].
40/// - keep at most [`FileLogging::max_files`] log files.
41/// - use the filtering defined by [`FileLogging::rust_log`]. When not provided, the default
42///   `DEFAULT_FILE_RUST_LOG` is used.
43/// - create log files with the name `iroh-<ROTATION_BASED_NAME>.log` (ex: iroh-2024-02-02.log)
44pub fn init_terminal_and_file_logging(
45    file_log_config: &FileLogging,
46    logs_root: &Path,
47) -> anyhow::Result<non_blocking::WorkerGuard> {
48    let terminal_layer = fmt::layer()
49        .with_writer(std::io::stderr)
50        .with_filter(tracing_subscriber::EnvFilter::from_default_env());
51    let (file_layer, guard) = {
52        let FileLogging {
53            rust_log,
54            max_files,
55            rotation,
56            dir,
57        } = file_log_config;
58
59        let filter = rust_log.layer();
60
61        let (file_logger, guard) = {
62            let file_appender = if *max_files == 0 || &filter.to_string() == "off" {
63                fmt::writer::OptionalWriter::none()
64            } else {
65                let rotation = match rotation {
66                    Rotation::Hourly => rolling::Rotation::HOURLY,
67                    Rotation::Daily => rolling::Rotation::DAILY,
68                    Rotation::Never => rolling::Rotation::NEVER,
69                };
70
71                // prefer the directory set in the config file over the default
72                let logs_path = dir.clone().unwrap_or_else(|| logs_root.join("logs"));
73
74                let file_appender = rolling::Builder::new()
75                    .rotation(rotation)
76                    .max_log_files(*max_files)
77                    .filename_prefix("iroh")
78                    .filename_suffix("log")
79                    .build(logs_path)?;
80                fmt::writer::OptionalWriter::some(file_appender)
81            };
82            non_blocking(file_appender)
83        };
84
85        let layer = fmt::Layer::new()
86            .with_ansi(false)
87            .with_line_number(true)
88            .with_writer(file_logger)
89            .with_filter(filter);
90        (layer, guard)
91    };
92    tracing_subscriber::registry()
93        .with(file_layer)
94        .with(terminal_layer)
95        .try_init()?;
96    Ok(guard)
97}
98
99/// Initialize logging in the terminal.
100///
101/// This will:
102/// - use the default [`fmt::format::Format`].
103/// - log to [`std::io::Stderr`]
104pub fn init_terminal_logging() -> anyhow::Result<()> {
105    let terminal_layer = fmt::layer()
106        .with_writer(std::io::stderr)
107        .with_filter(tracing_subscriber::EnvFilter::from_default_env());
108    tracing_subscriber::registry()
109        .with(terminal_layer)
110        .try_init()?;
111    Ok(())
112}
113
114/// Configuration for the logfiles.
115// Please note that this is documented in the `iroh.computer` repository under
116// `src/app/docs/reference/config/page.mdx`.  Any changes to this need to be updated there.
117#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
118#[serde(default, deny_unknown_fields)]
119pub struct FileLogging {
120    /// RUST_LOG directive to filter file logs.
121    pub rust_log: EnvFilter,
122    /// Maximum number of files to keep.
123    pub max_files: usize,
124    /// How often should a new log file be produced.
125    pub rotation: Rotation,
126    /// Where to store log files.
127    pub dir: Option<std::path::PathBuf>,
128}
129
130impl Default for FileLogging {
131    fn default() -> Self {
132        Self {
133            rust_log: EnvFilter::default(),
134            max_files: 4,
135            rotation: Rotation::default(),
136            dir: None,
137        }
138    }
139}
140
141/// Wrapper to obtain a [`tracing_subscriber::EnvFilter`] that satisfies required bounds.
142#[derive(
143    Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, derive_more::Display,
144)]
145#[display("{_0}")]
146pub struct EnvFilter(String);
147
148impl FromStr for EnvFilter {
149    type Err = <tracing_subscriber::EnvFilter as FromStr>::Err;
150
151    fn from_str(s: &str) -> Result<Self, Self::Err> {
152        // validate the RUST_LOG statement
153        let _valid_env = tracing_subscriber::EnvFilter::from_str(s)?;
154        Ok(EnvFilter(s.into()))
155    }
156}
157
158impl Default for EnvFilter {
159    fn default() -> Self {
160        Self(DEFAULT_FILE_RUST_LOG.into())
161    }
162}
163
164impl EnvFilter {
165    pub(crate) fn layer(&self) -> tracing_subscriber::EnvFilter {
166        tracing_subscriber::EnvFilter::from_str(&self.0).expect("validated RUST_LOG statement")
167    }
168}
169
170/// How often should a new file be created for file logs.
171///
172/// Akin to [`tracing_appender::rolling::Rotation`].
173#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
174#[serde(rename_all = "lowercase")]
175#[allow(missing_docs)]
176pub enum Rotation {
177    #[default]
178    Hourly,
179    Daily,
180    Never,
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    /// Tests that the default file logging `RUST_LOG` statement produces a valid layer.
188    #[test]
189    fn test_default_file_rust_log() {
190        let _ = EnvFilter::default().layer();
191    }
192}