hyprstream_core/
config.rs

1//! Configuration management for Hyprstream service.
2//!
3//! This module provides configuration handling through multiple sources:
4//! 1. Default configuration (embedded in binary)
5//! 2. System-wide configuration file (`/etc/hyprstream/config.toml`)
6//! 3. User-specified configuration file
7//! 4. Environment variables (prefixed with `HYPRSTREAM_`)
8//! 5. Command-line arguments
9//!
10//! Configuration options are loaded in order of precedence, with later sources
11//! overriding earlier ones.
12//!
13//! # Environment Variables
14//!
15//! Backend-specific credentials should be provided via environment variables:
16//! - `HYPRSTREAM_ENGINE_USERNAME` - Primary storage backend username
17//! - `HYPRSTREAM_ENGINE_PASSWORD` - Primary storage backend password
18//! - `HYPRSTREAM_CACHE_USERNAME` - Cache backend username (if needed)
19//! - `HYPRSTREAM_CACHE_PASSWORD` - Cache backend password (if needed)
20
21use clap::Parser;
22use config::{Config, ConfigError};
23use serde::Deserialize;
24use std::env;
25use std::path::PathBuf;
26use std::collections::HashMap;
27
28const DEFAULT_CONFIG: &str = include_str!("../config/default.toml");
29const DEFAULT_CONFIG_PATH: &str = "/etc/hyprstream/config.toml";
30
31/// Command-line arguments parser.
32///
33/// This structure defines all available command-line options and their
34/// corresponding environment variables. It uses clap for parsing and
35/// supports both short and long option forms.
36#[derive(Parser, Debug)]
37#[command(author, version, about)]
38pub struct CliArgs {
39    /// Path to the configuration file
40    #[arg(short, long, value_name = "FILE")]
41    config: Option<PathBuf>,
42
43    /// Server host address
44    #[arg(long, env = "HYPRSTREAM_SERVER_HOST")]
45    host: Option<String>,
46
47    /// Server port
48    #[arg(long, env = "HYPRSTREAM_SERVER_PORT")]
49    port: Option<u16>,
50
51    /// Log level (trace, debug, info, warn, error)
52    #[arg(long, env = "HYPRSTREAM_LOG_LEVEL")]
53    log_level: Option<String>,
54
55    /// Primary storage engine type
56    #[arg(long, env = "HYPRSTREAM_ENGINE")]
57    engine: Option<String>,
58
59    /// Primary storage engine connection string
60    #[arg(long, env = "HYPRSTREAM_ENGINE_CONNECTION")]
61    engine_connection: Option<String>,
62
63    /// Primary storage engine options (key=value pairs)
64    #[arg(long, env = "HYPRSTREAM_ENGINE_OPTIONS")]
65    engine_options: Option<Vec<String>>,
66
67    /// Enable caching
68    #[arg(long, env = "HYPRSTREAM_ENABLE_CACHE")]
69    enable_cache: Option<bool>,
70
71    /// Cache engine type
72    #[arg(long, env = "HYPRSTREAM_CACHE_ENGINE")]
73    cache_engine: Option<String>,
74
75    /// Cache engine connection string
76    #[arg(long, env = "HYPRSTREAM_CACHE_CONNECTION")]
77    cache_connection: Option<String>,
78
79    /// Cache engine options (key=value pairs)
80    #[arg(long, env = "HYPRSTREAM_CACHE_OPTIONS")]
81    cache_options: Option<Vec<String>>,
82
83    /// Cache maximum duration in seconds
84    #[arg(long, env = "HYPRSTREAM_CACHE_MAX_DURATION")]
85    cache_max_duration: Option<u64>,
86
87    /// Primary storage engine username
88    #[arg(long, env = "HYPRSTREAM_ENGINE_USERNAME")]
89    engine_username: Option<String>,
90
91    /// Primary storage engine password
92    #[arg(long, env = "HYPRSTREAM_ENGINE_PASSWORD")]
93    engine_password: Option<String>,
94
95    /// Cache engine username
96    #[arg(long, env = "HYPRSTREAM_CACHE_USERNAME")]
97    cache_username: Option<String>,
98
99    /// Cache engine password
100    #[arg(long, env = "HYPRSTREAM_CACHE_PASSWORD")]
101    cache_password: Option<String>,
102}
103
104/// Complete service configuration.
105///
106/// This structure holds all configuration options for the service,
107/// including server settings, storage backend configuration, and
108/// cache settings.
109#[derive(Debug, Deserialize)]
110pub struct Settings {
111    /// Server configuration
112    pub server: ServerConfig,
113    /// Engine configuration
114    pub engine: EngineConfig,
115    /// Cache configuration
116    pub cache: CacheConfig,
117}
118
119/// Server configuration options.
120///
121/// Defines the network interface and port for the Flight SQL service.
122#[derive(Debug, Deserialize)]
123pub struct ServerConfig {
124    /// Host address to bind to
125    pub host: String,
126    /// Port number to listen on
127    pub port: u16,
128    /// Log level (trace, debug, info, warn, error)
129    #[serde(default = "default_log_level")]
130    pub log_level: String,
131}
132
133fn default_log_level() -> String {
134    "info".to_string()
135}
136
137/// Engine configuration.
138///
139/// Specifies the primary storage engine to use for metric data.
140#[derive(Debug, Deserialize)]
141pub struct EngineConfig {
142    /// Engine type ("duckdb" or "adbc")
143    pub engine: String,
144    /// Connection string for the engine
145    pub connection: String,
146    /// Engine-specific options
147    #[serde(default)]
148    pub options: std::collections::HashMap<String, String>,
149    /// Authentication credentials (not serialized)
150    #[serde(skip)]
151    pub credentials: Option<Credentials>,
152}
153
154/// Authentication credentials for storage backends.
155#[derive(Debug, Clone, Deserialize)]
156pub struct Credentials {
157    /// Username for authentication
158    pub username: String,
159    /// Password for authentication
160    pub password: String,
161}
162
163/// Cache configuration for the storage backend.
164#[derive(Debug, Deserialize)]
165pub struct CacheConfig {
166    /// Whether caching is enabled
167    pub enabled: bool,
168    /// Cache storage engine type (e.g., "duckdb", "adbc")
169    pub engine: String,
170    /// Cache connection string
171    pub connection: String,
172    /// Cache engine options
173    pub options: HashMap<String, String>,
174    /// Cache credentials (optional)
175    #[serde(default)]
176    pub credentials: Option<Credentials>,
177    /// Maximum duration to keep entries in cache (in seconds)
178    #[serde(default = "default_ttl")]
179    pub ttl: Option<u64>,
180}
181
182fn default_ttl() -> Option<u64> {
183    Some(3600) // Default 1 hour TTL
184}
185
186impl Default for CacheConfig {
187    fn default() -> Self {
188        Self {
189            enabled: false,
190            engine: "duckdb".to_string(),
191            connection: ":memory:".to_string(),
192            options: HashMap::new(),
193            credentials: None,
194            ttl: default_ttl(),
195        }
196    }
197}
198
199fn default_cache_engine() -> String {
200    "duckdb".to_string()
201}
202
203fn default_cache_connection() -> String {
204    ":memory:".to_string()
205}
206
207fn default_cache_duration() -> u64 {
208    3600
209}
210
211impl Settings {
212    /// Loads configuration from all available sources.
213    pub fn new(cli: CliArgs) -> Result<Self, ConfigError> {
214        let mut builder = Config::builder();
215
216        // Load default configuration
217        builder = builder.add_source(config::File::from_str(
218            DEFAULT_CONFIG,
219            config::FileFormat::Toml,
220        ));
221
222        // Load system configuration if it exists
223        if let Ok(metadata) = std::fs::metadata(DEFAULT_CONFIG_PATH) {
224            if metadata.is_file() {
225                builder = builder.add_source(config::File::from(PathBuf::from(DEFAULT_CONFIG_PATH)));
226            }
227        }
228
229        // Load user configuration if specified
230        if let Some(ref config_path) = cli.config {
231            builder = builder.add_source(config::File::from(config_path.clone()));
232        }
233
234        // Add environment variables (prefixed with HYPRSTREAM_)
235        builder = builder.add_source(config::Environment::with_prefix("HYPRSTREAM"));
236
237        // Override with command line arguments
238        if let Some(ref host) = cli.host {
239            builder = builder.set_override("server.host", host.as_str())?;
240        }
241        if let Some(port) = cli.port {
242            builder = builder.set_override("server.port", port)?;
243        }
244
245        // Set log level if provided
246        if let Some(ref log_level) = cli.log_level {
247            builder = builder.set_override("server.log_level", log_level.as_str())?;
248        }
249
250        // Engine settings
251        if let Some(ref engine) = cli.engine {
252            builder = builder.set_override("engine.engine", engine.as_str())?;
253        }
254        if let Some(ref connection) = cli.engine_connection {
255            builder = builder.set_override("engine.connection", connection.as_str())?;
256        }
257        if let Some(ref options) = cli.engine_options {
258            let options: std::collections::HashMap<String, String> = options
259                .iter()
260                .filter_map(|opt| {
261                    let parts: Vec<&str> = opt.split('=').collect();
262                    if parts.len() == 2 {
263                        Some((parts[0].to_string(), parts[1].to_string()))
264                    } else {
265                        None
266                    }
267                })
268                .collect();
269            builder = builder.set_override("engine.options", options)?;
270        }
271
272        // Cache settings
273        if let Some(enabled) = cli.enable_cache {
274            builder = builder.set_override("cache.enabled", enabled)?;
275        }
276        if let Some(ref engine) = cli.cache_engine {
277            builder = builder.set_override("cache.engine", engine.as_str())?;
278        }
279        if let Some(ref connection) = cli.cache_connection {
280            builder = builder.set_override("cache.connection", connection.as_str())?;
281        }
282        if let Some(ref options) = cli.cache_options {
283            let options: std::collections::HashMap<String, String> = options
284                .iter()
285                .filter_map(|opt| {
286                    let parts: Vec<&str> = opt.split('=').collect();
287                    if parts.len() == 2 {
288                        Some((parts[0].to_string(), parts[1].to_string()))
289                    } else {
290                        None
291                    }
292                })
293                .collect();
294            builder = builder.set_override("cache.options", options)?;
295        }
296        if let Some(duration) = cli.cache_max_duration {
297            builder = builder.set_override("cache.max_duration_secs", duration)?;
298        }
299
300        // Build initial settings
301        let mut settings: Settings = builder.build()?.try_deserialize()?;
302
303        // Load credentials from environment variables and CLI args
304        settings.engine.credentials = Self::load_engine_credentials(&cli);
305        settings.cache.credentials = Self::load_cache_credentials(&cli);
306
307        Ok(settings)
308    }
309
310    /// Load engine credentials from all available sources.
311    /// Priority order (highest to lowest):
312    /// 1. Environment variables
313    /// 2. Command line arguments
314    fn load_engine_credentials(cli: &CliArgs) -> Option<Credentials> {
315        // Try environment variables first
316        if let (Some(username), Some(password)) = (
317            env::var("HYPRSTREAM_ENGINE_USERNAME").ok(),
318            env::var("HYPRSTREAM_ENGINE_PASSWORD").ok(),
319        ) {
320            return Some(Credentials { username, password });
321        }
322
323        // Try command line arguments
324        if let (Some(username), Some(password)) = (&cli.engine_username, &cli.engine_password) {
325            return Some(Credentials {
326                username: username.clone(),
327                password: password.clone(),
328            });
329        }
330
331        None
332    }
333
334    /// Load cache credentials from all available sources.
335    /// Priority order (highest to lowest):
336    /// 1. Environment variables
337    /// 2. Command line arguments
338    fn load_cache_credentials(cli: &CliArgs) -> Option<Credentials> {
339        // Try environment variables first
340        if let (Some(username), Some(password)) = (
341            env::var("HYPRSTREAM_CACHE_USERNAME").ok(),
342            env::var("HYPRSTREAM_CACHE_PASSWORD").ok(),
343        ) {
344            return Some(Credentials { username, password });
345        }
346
347        // Try command line arguments
348        if let (Some(username), Some(password)) = (&cli.cache_username, &cli.cache_password) {
349            return Some(Credentials {
350                username: username.clone(),
351                password: password.clone(),
352            });
353        }
354
355        None
356    }
357}