1use std::path::PathBuf;
2
3use crate::storage::StoreLocation;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum StorageBackend {
10 Duckdb,
11 TaelBackend,
12}
13
14impl StorageBackend {
15 pub fn parse(s: &str) -> Self {
18 match s.trim().to_lowercase().as_str() {
19 "duckdb" | "duck" => StorageBackend::Duckdb,
20 _ => StorageBackend::TaelBackend,
21 }
22 }
23}
24
25pub struct ServerConfig {
26 pub otlp_grpc_addr: String,
27 pub rest_api_addr: String,
28 pub rest_api_socket: Option<String>,
29 pub data_dir: String,
30 pub wal_dir: String,
31 pub storage: StorageBackend,
32 pub query_shards: Vec<String>,
37 pub wal_standbys: Vec<String>,
41 pub wal_required_acks: Option<usize>,
45 pub cluster: Option<ClusterSettings>,
49 pub object_store: ObjectStoreConfig,
53 pub comments: CommentsConfig,
56}
57
58#[derive(Debug, Clone)]
62pub struct ObjectStoreConfig {
63 pub cold: StoreLocation,
65 pub blobs: StoreLocation,
67 pub cold_bucket: Option<String>,
69 pub blob_bucket: Option<String>,
71 pub blob_gc_coordinator: bool,
77}
78
79impl ObjectStoreConfig {
80 fn from_env() -> Self {
81 Self {
82 cold: std::env::var("TAEL_COLD_STORE")
83 .map(|s| StoreLocation::parse(&s))
84 .unwrap_or_default(),
85 blobs: std::env::var("TAEL_BLOB_STORE")
86 .map(|s| StoreLocation::parse(&s))
87 .unwrap_or_default(),
88 cold_bucket: non_empty_env("TAEL_COLD_BUCKET"),
89 blob_bucket: non_empty_env("TAEL_BLOB_BUCKET"),
90 blob_gc_coordinator: std::env::var("TAEL_BLOB_GC_ROLE")
91 .map(|s| s.trim().eq_ignore_ascii_case("coordinator"))
92 .unwrap_or(false),
93 }
94 }
95
96 pub fn blobs_shared(&self) -> bool {
99 self.blobs == StoreLocation::Gcs
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub enum CommentsBackend {
106 #[default]
108 Jsonl,
109 Postgres,
111}
112
113impl CommentsBackend {
114 pub fn parse(s: &str) -> Self {
115 match s.trim().to_lowercase().as_str() {
116 "postgres" | "postgresql" | "pg" => CommentsBackend::Postgres,
117 _ => CommentsBackend::Jsonl,
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct CommentsConfig {
125 pub backend: CommentsBackend,
126 pub database_url: Option<String>,
129}
130
131impl CommentsConfig {
132 fn from_env() -> Self {
133 Self {
134 backend: std::env::var("TAEL_COMMENTS_STORE")
135 .map(|s| CommentsBackend::parse(&s))
136 .unwrap_or_default(),
137 database_url: non_empty_env("TAEL_COMMENTS_DATABASE_URL"),
138 }
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct ClusterSettings {
145 pub node_id: String,
147 pub listen_addr: String,
149 pub advertise_addr: String,
151 pub seeds: Vec<String>,
153 pub cluster_id: String,
155}
156
157impl ServerConfig {
158 pub fn from_env() -> Self {
159 let mut config = Self {
160 otlp_grpc_addr: std::env::var("TAEL_OTLP_GRPC_ADDR")
161 .unwrap_or_else(|_| "127.0.0.1:4317".into()),
162 rest_api_addr: std::env::var("TAEL_REST_API_ADDR")
163 .unwrap_or_else(|_| "127.0.0.1:7701".into()),
164 rest_api_socket: std::env::var("TAEL_REST_API_SOCKET")
165 .ok()
166 .filter(|s| !s.trim().is_empty()),
167 data_dir: std::env::var("TAEL_DATA_DIR").unwrap_or_else(|_| default_data_dir()),
168 wal_dir: std::env::var("TAEL_WAL_DIR")
169 .or_else(|_| std::env::var("WALRUS_DATA_DIR"))
170 .unwrap_or_else(|_| default_wal_dir()),
171 storage: std::env::var("TAEL_STORAGE")
173 .map(|s| StorageBackend::parse(&s))
174 .unwrap_or(StorageBackend::TaelBackend),
175 query_shards: parse_csv_env("TAEL_QUERY_SHARDS"),
176 wal_standbys: parse_csv_env("TAEL_WAL_STANDBYS"),
177 wal_required_acks: std::env::var("TAEL_WAL_REQUIRED_ACKS")
178 .ok()
179 .and_then(|s| s.trim().parse().ok()),
180 cluster: cluster_from_env(),
181 object_store: ObjectStoreConfig::from_env(),
182 comments: CommentsConfig::from_env(),
183 };
184 if let Some(s) = storage_flag() {
187 config.storage = s;
188 }
189 config
190 }
191}
192
193fn default_data_dir() -> String {
194 default_tael_home().join("data").display().to_string()
195}
196
197fn default_wal_dir() -> String {
198 default_tael_home().join("wal_files").display().to_string()
199}
200
201fn default_tael_home() -> PathBuf {
202 home_dir()
203 .map(|home| home.join(".tael"))
204 .unwrap_or_else(|| PathBuf::from(".tael"))
205}
206
207fn home_dir() -> Option<PathBuf> {
208 std::env::var_os("HOME")
209 .map(PathBuf::from)
210 .or_else(|| std::env::var_os("USERPROFILE").map(PathBuf::from))
211 .or_else(
212 || match (std::env::var_os("HOMEDRIVE"), std::env::var_os("HOMEPATH")) {
213 (Some(drive), Some(path)) => {
214 let mut home = PathBuf::from(drive);
215 home.push(path);
216 Some(home)
217 }
218 _ => None,
219 },
220 )
221}
222
223fn cluster_from_env() -> Option<ClusterSettings> {
226 let listen_addr = std::env::var("TAEL_CLUSTER_LISTEN").ok()?;
227 let advertise_addr =
228 std::env::var("TAEL_CLUSTER_ADVERTISE").unwrap_or_else(|_| listen_addr.clone());
229 let node_id = std::env::var("TAEL_NODE_ID").unwrap_or_else(|_| advertise_addr.clone());
231 Some(ClusterSettings {
232 node_id,
233 listen_addr,
234 advertise_addr,
235 seeds: parse_csv_env("TAEL_CLUSTER_SEEDS"),
236 cluster_id: std::env::var("TAEL_CLUSTER_ID").unwrap_or_else(|_| "tael".to_string()),
237 })
238}
239
240fn non_empty_env(var: &str) -> Option<String> {
242 std::env::var(var).ok().filter(|s| !s.trim().is_empty())
243}
244
245fn parse_csv_env(var: &str) -> Vec<String> {
247 std::env::var(var)
248 .ok()
249 .map(|s| {
250 s.split(',')
251 .map(|p| p.trim().to_string())
252 .filter(|p| !p.is_empty())
253 .collect()
254 })
255 .unwrap_or_default()
256}
257
258fn storage_flag() -> Option<StorageBackend> {
260 let mut args = std::env::args().skip(1);
261 while let Some(arg) = args.next() {
262 if arg == "--storage" {
263 return args.next().map(|v| StorageBackend::parse(&v));
264 }
265 if let Some(v) = arg.strip_prefix("--storage=") {
266 return Some(StorageBackend::parse(v));
267 }
268 }
269 None
270}