Skip to main content

autopulse_utils/
logs.rs

1use anyhow::{Context, Ok};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use tracing_appender::non_blocking::WorkerGuard;
5use tracing_appender::rolling::RollingFileAppender;
6pub use tracing_appender::rolling::Rotation;
7use tracing_subscriber::util::SubscriberInitExt;
8use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter};
9
10#[derive(Clone, Serialize, Deserialize, Default)]
11#[serde(rename_all = "lowercase")]
12pub enum LogLevel {
13    Trace,
14    Debug,
15    #[default]
16    Info,
17    Warn,
18    Error,
19}
20
21impl std::fmt::Display for LogLevel {
22    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
23        match self {
24            Self::Trace => write!(f, "trace"),
25            Self::Debug => write!(f, "debug"),
26            Self::Info => write!(f, "info"),
27            Self::Warn => write!(f, "warn"),
28            Self::Error => write!(f, "error"),
29        }
30    }
31}
32
33impl std::str::FromStr for LogLevel {
34    type Err = anyhow::Error;
35
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        match s {
38            "trace" => Ok(Self::Trace),
39            "debug" => Ok(Self::Debug),
40            "info" => Ok(Self::Info),
41            "warn" => Ok(Self::Warn),
42            "error" => Ok(Self::Error),
43            _ => Err(anyhow::anyhow!("Invalid log level")),
44        }
45    }
46}
47
48pub fn setup_logs(
49    log_level: &LogLevel,
50    log_file: &Option<PathBuf>,
51    log_file_rollover: &Rotation,
52    api_logging: bool,
53) -> anyhow::Result<Option<WorkerGuard>> {
54    let timer = tracing_subscriber::fmt::time::OffsetTime::local_rfc_3339()
55        .context("failed to initialize the timer")?;
56
57    let mut file_guard = None;
58
59    let mut filter =
60        EnvFilter::from_default_env().add_directive(format!("autopulse={log_level}").parse()?);
61
62    if api_logging {
63        filter = filter
64            .add_directive("actix_web=info".parse()?)
65            .add_directive("actix_server::builder=info".parse()?);
66    }
67
68    let registry = tracing_subscriber::registry().with(filter);
69
70    if let Some(log_file) = log_file {
71        // `tracing-appender` is pinned to a fork (see workspace Cargo.toml)
72        // that enables a `local-time` feature, making the rolled filename
73        // suffix and rotation boundary track the local timezone instead of
74        // UTC. Without that feature, log filenames would be a day off the
75        // record timestamps inside them (#208).
76        let writer = RollingFileAppender::new(
77            log_file_rollover.clone(),
78            log_file
79                .parent()
80                .ok_or_else(|| anyhow::anyhow!("failed to get parent directory of log file"))?,
81            log_file
82                .file_name()
83                .ok_or_else(|| anyhow::anyhow!("failed to get file name of log file"))?,
84        );
85
86        let (non_blocking, guard) = tracing_appender::non_blocking(writer);
87        file_guard = Some(guard);
88
89        let file_layer = fmt::layer()
90            .with_writer(non_blocking)
91            .with_ansi(false)
92            .with_timer(timer.clone());
93
94        let registry = registry.with(file_layer);
95
96        let console_layer = fmt::layer()
97            .with_writer(std::io::stdout)
98            .with_ansi(true)
99            .with_timer(timer);
100
101        registry.with(console_layer).init();
102    } else {
103        let console_layer = fmt::layer()
104            .with_writer(std::io::stdout)
105            .with_ansi(true)
106            .with_timer(timer);
107
108        registry.with(console_layer).init();
109    }
110    Ok(file_guard)
111}