Skip to main content

elph_agent/
builder.rs

1use std::path::PathBuf;
2
3/// How often log files are rotated.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum LogRotation {
6    Hourly,
7    Daily,
8    Weekly,
9}
10
11impl LogRotation {
12    pub fn from_env(prefix: &str) -> Self {
13        let key = format!("{prefix}_LOG_ROTATION");
14        Self::parse(std::env::var(&key).ok().as_deref())
15    }
16
17    pub fn parse(value: Option<&str>) -> Self {
18        match value {
19            Some("hourly") => Self::Hourly,
20            Some("weekly") => Self::Weekly,
21            Some("daily") | None => Self::Daily,
22            _ => Self::Daily,
23        }
24    }
25}
26
27/// Resolved logging configuration for an application to initialize its subscriber.
28#[derive(Debug, Clone)]
29pub struct LoggingOptions {
30    pub app_name: &'static str,
31    pub logs_dir: PathBuf,
32    pub level: String,
33    pub rotation: LogRotation,
34    pub max_files: Option<usize>,
35    pub file_enabled: bool,
36    pub console_enabled: bool,
37}
38
39impl LoggingOptions {
40    pub fn level_from_env(prefix: &str) -> String {
41        let key = format!("{prefix}_LOG_LEVEL");
42        match std::env::var(&key) {
43            Ok(value) if matches!(value.as_str(), "trace" | "debug" | "info" | "warn" | "error") => value,
44            _ => "info".to_string(),
45        }
46    }
47
48    pub fn max_files_from_env(prefix: &str) -> Option<usize> {
49        let key = format!("{prefix}_LOG_MAX_FILES");
50        std::env::var(&key).ok().and_then(|value| value.parse().ok())
51    }
52
53    pub fn file_logging_enabled(prefix: &str) -> bool {
54        let key = format!("{prefix}_LOG_FILE");
55        std::env::var(&key).map(|value| value != "0").unwrap_or(true)
56    }
57
58    fn resolve(env_prefix: &str, app_name: &'static str, logs_dir: Option<PathBuf>, console_enabled: bool) -> Self {
59        let file_enabled = logs_dir.is_some() && Self::file_logging_enabled(env_prefix);
60        Self {
61            app_name,
62            logs_dir: logs_dir.unwrap_or_default(),
63            level: Self::level_from_env(env_prefix),
64            rotation: LogRotation::from_env(env_prefix),
65            max_files: Self::max_files_from_env(env_prefix),
66            file_enabled,
67            console_enabled,
68        }
69    }
70}
71
72/// Output of [`AgentBuilder::build`].
73#[derive(Debug, Clone)]
74pub struct AgentInit {
75    pub app_version: &'static str,
76    pub quiet_env: Option<&'static str>,
77    pub logging: LoggingOptions,
78}
79
80/// Builder for application initialization settings shared across Elph apps.
81#[derive(Debug, Clone)]
82pub struct AgentBuilder {
83    app_version: &'static str,
84    env_prefix: &'static str,
85    app_name: &'static str,
86    quiet_env: Option<&'static str>,
87    logs_dir: Option<PathBuf>,
88    console_enabled: bool,
89}
90
91impl AgentBuilder {
92    pub fn new(app_version: &'static str) -> Self {
93        Self {
94            app_version,
95            env_prefix: "",
96            app_name: "",
97            quiet_env: None,
98            logs_dir: None,
99            console_enabled: true,
100        }
101    }
102
103    pub fn env_prefix(mut self, prefix: &'static str) -> Self {
104        self.env_prefix = prefix;
105        self
106    }
107
108    pub fn app_name(mut self, name: &'static str) -> Self {
109        self.app_name = name;
110        self
111    }
112
113    pub fn quiet_env(mut self, env: &'static str) -> Self {
114        self.quiet_env = Some(env);
115        self
116    }
117
118    pub fn logs_dir(mut self, dir: PathBuf) -> Self {
119        self.logs_dir = Some(dir);
120        self
121    }
122
123    pub fn console_enabled(mut self, enabled: bool) -> Self {
124        self.console_enabled = enabled;
125        self
126    }
127
128    pub fn build(self) -> AgentInit {
129        AgentInit {
130            app_version: self.app_version,
131            quiet_env: self.quiet_env,
132            logging: LoggingOptions::resolve(self.env_prefix, self.app_name, self.logs_dir, self.console_enabled),
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn defaults_to_daily_rotation() {
143        assert_eq!(LogRotation::parse(None), LogRotation::Daily);
144        assert_eq!(LogRotation::parse(Some("daily")), LogRotation::Daily);
145    }
146
147    #[test]
148    fn parses_rotation_values() {
149        assert_eq!(LogRotation::parse(Some("hourly")), LogRotation::Hourly);
150        assert_eq!(LogRotation::parse(Some("weekly")), LogRotation::Weekly);
151        assert_eq!(LogRotation::parse(Some("monthly")), LogRotation::Daily);
152    }
153
154    #[test]
155    fn builder_resolves_logging_without_logs_dir() {
156        let init = AgentBuilder::new("0.0.12-test")
157            .env_prefix("ELPH")
158            .app_name("elph")
159            .console_enabled(false)
160            .build();
161
162        assert_eq!(init.app_version, "0.0.12-test");
163        assert!(!init.logging.file_enabled);
164        assert!(!init.logging.console_enabled);
165        assert_eq!(init.logging.level, "info");
166    }
167}