1use wowlab_common::{EnvLoader, ensure_query_param};
4use wowlab_parsers::parse_csv_list;
5
6#[rustfmt::skip]
7pub(crate) use secrecy::{ExposeSecret, SecretString};
8
9const DEFAULT_HTTP_PORT: u16 = 8080;
10const DEFAULT_CLEANUP_JOBS_DAYS: f64 = 14.0;
11const DEFAULT_RANKING_TOP_N: usize = 100;
12const DEFAULT_RETENTION_SNAPSHOTS_DAYS: f64 = 90.0;
13const DEFAULT_RETENTION_ROLLING_DAYS: f64 = 90.0;
14const DEFAULT_DB_ACQUIRE_TIMEOUT_SECS: u64 = 5;
15const DEFAULT_DB_IDLE_TIMEOUT_SECS: u64 = 600;
16const DEFAULT_SCHEDULER_BATCH_SIZE: i64 = 100;
17const DEFAULT_SCHEDULER_POLL_TIMEOUT_SECS: u64 = 30;
18const DEFAULT_SCHEDULER_RECONNECT_DELAY_SECS: u64 = 5;
19const DEFAULT_RECLAIM_TIMEOUT_MINUTES: i64 = 5;
20const DEFAULT_RECLAIM_MAX_ATTEMPTS: i32 = 3;
21const DEFAULT_BURST_MAX_NODES: usize = 5;
22
23#[derive(Clone, Debug)]
24pub(crate) struct Config {
25 pub database_url: SecretString,
26 pub centrifugo_url: String,
27 pub centrifugo_key: SecretString,
28 pub centrifugo_token_secret: SecretString,
29 pub http_port: u16,
30 pub deploy_webhook_secret: Option<SecretString>,
31 pub deploy_branch_filter: Vec<String>,
32 pub mcp_enabled: bool,
33 pub cron_presence: String,
35 pub cron_reclaim: String,
36 pub cron_uptime: String,
37 pub cron_cleanup: String,
38 pub cron_cleanup_jobs_days: f64,
39 pub cron_rollup: String,
40 pub cron_ranking: String,
41 pub ranking_top_n: usize,
42 pub cron_integrity: String,
43 pub cron_retention: String,
44 pub cron_docs: String,
45 pub cron_queue_depth: String,
46 pub docs_base_url: String,
47 pub retention_snapshots_days: f64,
48 pub retention_rolling_days: f64,
49 pub db_acquire_timeout_secs: u64,
50 pub db_idle_timeout_secs: u64,
51 pub scheduler_batch_size: i64,
52 pub scheduler_poll_timeout_secs: u64,
53 pub scheduler_reconnect_delay_secs: u64,
54 pub reclaim_timeout_minutes: i64,
55 pub reclaim_max_attempts: i32,
56 pub cors_origins: Vec<String>,
57 pub latitude_api_key: Option<SecretString>,
58 pub latitude_base_url: String,
59 pub latitude_project: Option<String>,
60 pub latitude_site: Option<String>,
61 pub latitude_burst_tag_id: Option<String>,
62 pub burst_enabled: bool,
63 pub burst_floor_capacity: i64,
64 pub burst_per_node_throughput: i64,
65 pub burst_max_nodes: usize,
66 pub burst_node_claim_token: Option<SecretString>,
67 pub burst_ts_authkey: Option<SecretString>,
68 pub cron_burst_scheduler: String,
69}
70
71impl Config {
72 pub(crate) fn from_env() -> Result<Self, wowlab_common::EnvError> {
73 let env = EnvLoader::new("SENTINEL");
74
75 Ok(Self {
76 database_url: ensure_query_param(&env.require("DATABASE_URL")?, "sslmode", "require")
77 .into(),
78 centrifugo_url: env.require("CENTRIFUGO_URL")?,
79 centrifugo_key: env.require_secret("CENTRIFUGO_KEY")?,
80 centrifugo_token_secret: env.require_secret("CENTRIFUGO_TOKEN_SECRET")?,
81 http_port: env.parse_or("HTTP_PORT", DEFAULT_HTTP_PORT),
82 deploy_webhook_secret: env.get_secret("DEPLOY_WEBHOOK_SECRET"),
83 deploy_branch_filter: parse_csv_list(
84 &env.get_or("DEPLOY_BRANCH_FILTER", "main,master"),
85 ),
86 mcp_enabled: env.parse_bool("MCP_ENABLED", false),
87 cron_presence: env.get_or("CRON_PRESENCE", "*/30 * * * * *"),
88 cron_reclaim: env.get_or("CRON_RECLAIM", "0 * * * * *"),
89 cron_uptime: env.get_or("CRON_UPTIME", "*/30 * * * * *"),
90 cron_cleanup: env.get_or("CRON_CLEANUP", "0 0 3 * * *"),
91
92 cron_cleanup_jobs_days: env
93 .parse_or("CRON_CLEANUP_JOBS_DAYS", DEFAULT_CLEANUP_JOBS_DAYS),
94 cron_rollup: env.get_or("CRON_ROLLUP", "0 0 4 * * *"),
95 cron_ranking: env.get_or("CRON_RANKING", "0 0 * * * *"),
96 ranking_top_n: env.parse_or("RANKING_TOP_N", DEFAULT_RANKING_TOP_N),
97 cron_integrity: env.get_or("CRON_INTEGRITY", "0 30 4 * * *"),
98 cron_retention: env.get_or("CRON_RETENTION", "0 0 5 * * *"),
99 cron_docs: env.get_or("CRON_DOCS", "0 */30 * * * *"),
100 cron_queue_depth: env.get_or("CRON_QUEUE_DEPTH", "*/30 * * * * *"),
101 docs_base_url: env.get_or("DOCS_BASE_URL", "https://wowlab.gg"),
102 retention_snapshots_days: env
103 .parse_or("RETENTION_SNAPSHOTS_DAYS", DEFAULT_RETENTION_SNAPSHOTS_DAYS),
104 retention_rolling_days: env
105 .parse_or("RETENTION_ROLLING_DAYS", DEFAULT_RETENTION_ROLLING_DAYS),
106 db_acquire_timeout_secs: env
107 .parse_or("DB_ACQUIRE_TIMEOUT_SECS", DEFAULT_DB_ACQUIRE_TIMEOUT_SECS),
108 db_idle_timeout_secs: env
109 .parse_or("DB_IDLE_TIMEOUT_SECS", DEFAULT_DB_IDLE_TIMEOUT_SECS),
110 scheduler_batch_size: env
111 .parse_or("SCHEDULER_BATCH_SIZE", DEFAULT_SCHEDULER_BATCH_SIZE),
112 scheduler_poll_timeout_secs: env.parse_or(
113 "SCHEDULER_POLL_TIMEOUT_SECS",
114 DEFAULT_SCHEDULER_POLL_TIMEOUT_SECS,
115 ),
116 scheduler_reconnect_delay_secs: env.parse_or(
117 "SCHEDULER_RECONNECT_DELAY_SECS",
118 DEFAULT_SCHEDULER_RECONNECT_DELAY_SECS,
119 ),
120 reclaim_timeout_minutes: env
121 .parse_or("RECLAIM_TIMEOUT_MINUTES", DEFAULT_RECLAIM_TIMEOUT_MINUTES),
122 reclaim_max_attempts: env
123 .parse_or("RECLAIM_MAX_ATTEMPTS", DEFAULT_RECLAIM_MAX_ATTEMPTS),
124 cors_origins: parse_csv_list(&env.get_or(
125 "CORS_ORIGINS",
126 "https://wowlab.gg,https://*.wowlab.gg,http://localhost:*",
127 )),
128 latitude_api_key: env.get_secret("LATITUDE_API_KEY"),
129 latitude_base_url: env.get_or("LATITUDE_BASE_URL", "https://api.latitude.sh"),
130 latitude_project: env.get("LATITUDE_PROJECT"),
131 latitude_site: env.get("LATITUDE_SITE"),
132 latitude_burst_tag_id: env.get("LATITUDE_BURST_TAG_ID"),
133 burst_enabled: env.parse_bool("BURST_ENABLED", false),
134 burst_floor_capacity: env.parse_or("BURST_FLOOR_CAPACITY", 0),
135 burst_per_node_throughput: env.parse_or("BURST_PER_NODE_THROUGHPUT", 1),
136 burst_max_nodes: env.parse_or("BURST_MAX_NODES", DEFAULT_BURST_MAX_NODES),
137 burst_node_claim_token: env.get_secret("BURST_NODE_CLAIM_TOKEN"),
138 burst_ts_authkey: env.get_secret("BURST_TS_AUTHKEY"),
139 cron_burst_scheduler: env.get_or("CRON_BURST_SCHEDULER", "0 * * * * *"),
140 })
141 }
142
143 pub(crate) fn log(&self) {
144 tracing::info!(config = ?self, "Configuration loaded");
145 }
146}