1use std::env;
2
3#[derive(Clone, Debug)]
4pub struct Config {
5 pub sqlite_path: String,
6 pub api_key: Option<String>,
7 pub retention_days_errors: i64,
8 pub retention_days_hourly_rollups: i64,
9 pub retention_days_spans: i64,
10 pub slow_request_threshold_ms: f64,
11 pub mini_apm_url: String,
12 pub enable_user_accounts: bool,
13 pub enable_projects: bool,
14 pub session_secret: String,
15}
16
17impl Config {
18 pub fn from_env() -> anyhow::Result<Self> {
19 let enable_user_accounts = env::var("ENABLE_USER_ACCOUNTS")
21 .map(|v| v == "1" || v.to_lowercase() == "true")
22 .unwrap_or(false);
23
24 let session_secret = env::var("SESSION_SECRET").ok();
25
26 if enable_user_accounts && session_secret.is_none() {
27 anyhow::bail!(
28 "SESSION_SECRET environment variable is required when ENABLE_USER_ACCOUNTS=true. \
29 Generate one with: openssl rand -hex 32"
30 );
31 }
32
33 let session_secret = session_secret.unwrap_or_else(|| {
35 if enable_user_accounts {
36 panic!("SESSION_SECRET is required");
38 }
39 use rand::Rng;
41 let bytes: [u8; 32] = rand::thread_rng().r#gen();
42 hex::encode(bytes)
43 });
44
45 Ok(Self {
46 sqlite_path: env::var("SQLITE_PATH")
47 .unwrap_or_else(|_| "./data/miniapm.db".to_string()),
48 api_key: env::var("MINI_APM_API_KEY").ok(),
49 retention_days_errors: env::var("RETENTION_DAYS_ERRORS")
50 .ok()
51 .and_then(|v| v.parse().ok())
52 .filter(|&v| v > 0)
53 .unwrap_or(30),
54 retention_days_hourly_rollups: env::var("RETENTION_DAYS_HOURLY_ROLLUPS")
55 .ok()
56 .and_then(|v| v.parse().ok())
57 .filter(|&v| v > 0)
58 .unwrap_or(90),
59 retention_days_spans: env::var("RETENTION_DAYS_SPANS")
60 .ok()
61 .and_then(|v| v.parse().ok())
62 .filter(|&v| v > 0)
63 .unwrap_or(7),
64 slow_request_threshold_ms: env::var("SLOW_REQUEST_THRESHOLD_MS")
65 .ok()
66 .and_then(|v| v.parse().ok())
67 .filter(|&v| v > 0.0)
68 .unwrap_or(500.0),
69 mini_apm_url: env::var("MINI_APM_URL")
70 .unwrap_or_else(|_| "http://localhost:3000".to_string()),
71 enable_user_accounts,
72 enable_projects: env::var("ENABLE_PROJECTS")
73 .map(|v| v == "1" || v.to_lowercase() == "true")
74 .unwrap_or(false),
75 session_secret,
76 })
77 }
78
79 pub fn api_key_configured(&self) -> bool {
80 self.api_key.as_ref().is_some_and(|k| !k.is_empty())
81 }
82}