Skip to main content

camel_config/
config.rs

1use camel_core::config::TracerConfig;
2use config::{Config, ConfigError};
3use serde::{Deserialize, Serialize};
4use std::env;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Deserialize)]
8pub struct CamelConfig {
9    #[serde(default)]
10    pub routes: Vec<String>,
11
12    /// Enable file-watcher hot-reload. Defaults to false.
13    /// Can be overridden per profile in Camel.toml or via `--watch` / `--no-watch` CLI flags.
14    #[serde(default)]
15    pub watch: bool,
16
17    #[serde(default = "default_log_level")]
18    pub log_level: String,
19
20    #[serde(default = "default_timeout_ms")]
21    pub timeout_ms: u64,
22
23    #[serde(default)]
24    pub components: ComponentsConfig,
25
26    #[serde(default)]
27    pub observability: ObservabilityConfig,
28
29    #[serde(default)]
30    pub supervision: Option<SupervisionCamelConfig>,
31}
32
33#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
34pub struct ComponentsConfig {
35    #[serde(default)]
36    pub timer: Option<TimerConfig>,
37
38    #[serde(default)]
39    pub http: Option<HttpConfig>,
40}
41
42#[derive(Debug, Clone, Deserialize, PartialEq)]
43pub struct TimerConfig {
44    #[serde(default = "default_timer_period")]
45    pub period: u64,
46}
47
48#[derive(Debug, Clone, Deserialize, PartialEq)]
49pub struct HttpConfig {
50    #[serde(default = "default_http_connect_timeout")]
51    pub connect_timeout_ms: u64,
52
53    #[serde(default = "default_http_max_connections")]
54    pub max_connections: usize,
55}
56
57#[derive(Debug, Clone, Deserialize, Default)]
58pub struct ObservabilityConfig {
59    #[serde(default)]
60    pub metrics_enabled: bool,
61
62    #[serde(default = "default_metrics_port")]
63    pub metrics_port: u16,
64
65    #[serde(default)]
66    pub tracer: TracerConfig,
67
68    #[serde(default)]
69    pub otel: Option<OtelCamelConfig>,
70}
71
72/// OpenTelemetry configuration for `[observability.otel]` in Camel.toml.
73#[derive(Debug, Clone, Deserialize)]
74pub struct OtelCamelConfig {
75    /// Enable OTel export (traces, metrics, logs). Default: false.
76    #[serde(default)]
77    pub enabled: bool,
78
79    /// OTLP endpoint. Default: "http://localhost:4317".
80    #[serde(default = "default_otel_endpoint")]
81    pub endpoint: String,
82
83    /// Service name reported to the OTel backend. Default: "rust-camel".
84    #[serde(default = "default_otel_service_name")]
85    pub service_name: String,
86
87    /// Log level filter for the OTel subscriber. Default: "info".
88    #[serde(default = "default_otel_log_level")]
89    pub log_level: String,
90}
91
92#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
93pub struct SupervisionCamelConfig {
94    /// Maximum number of restart attempts. `None` means retry forever.
95    pub max_attempts: Option<u32>,
96
97    /// Delay before the first restart attempt in milliseconds.
98    #[serde(default = "default_initial_delay_ms")]
99    pub initial_delay_ms: u64,
100
101    /// Multiplier applied to the delay after each failed attempt.
102    #[serde(default = "default_backoff_multiplier")]
103    pub backoff_multiplier: f64,
104
105    /// Maximum delay cap between restart attempts in milliseconds.
106    #[serde(default = "default_max_delay_ms")]
107    pub max_delay_ms: u64,
108}
109
110impl Default for SupervisionCamelConfig {
111    fn default() -> Self {
112        Self {
113            max_attempts: Some(5),
114            initial_delay_ms: 1000,
115            backoff_multiplier: 2.0,
116            max_delay_ms: 60000,
117        }
118    }
119}
120
121impl SupervisionCamelConfig {
122    /// Convert to camel_api::SupervisionConfig
123    pub fn into_supervision_config(self) -> camel_api::SupervisionConfig {
124        camel_api::SupervisionConfig {
125            max_attempts: self.max_attempts,
126            initial_delay: Duration::from_millis(self.initial_delay_ms),
127            backoff_multiplier: self.backoff_multiplier,
128            max_delay: Duration::from_millis(self.max_delay_ms),
129        }
130    }
131}
132
133fn default_log_level() -> String {
134    "INFO".to_string()
135}
136fn default_timeout_ms() -> u64 {
137    5000
138}
139fn default_timer_period() -> u64 {
140    1000
141}
142fn default_http_connect_timeout() -> u64 {
143    5000
144}
145fn default_http_max_connections() -> usize {
146    100
147}
148fn default_metrics_port() -> u16 {
149    9090
150}
151
152fn default_otel_endpoint() -> String {
153    "http://localhost:4317".to_string()
154}
155fn default_otel_service_name() -> String {
156    "rust-camel".to_string()
157}
158fn default_otel_log_level() -> String {
159    "info".to_string()
160}
161
162fn default_initial_delay_ms() -> u64 {
163    1000
164}
165
166fn default_backoff_multiplier() -> f64 {
167    2.0
168}
169
170fn default_max_delay_ms() -> u64 {
171    60000
172}
173
174/// Deep merge two TOML values
175/// Tables are merged recursively, with overlay values taking precedence
176fn merge_toml_values(base: &mut toml::Value, overlay: &toml::Value) {
177    match (base, overlay) {
178        (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
179            for (key, value) in overlay_table {
180                if let Some(base_value) = base_table.get_mut(key) {
181                    // Both have this key - merge recursively
182                    merge_toml_values(base_value, value);
183                } else {
184                    // Only overlay has this key - insert it
185                    base_table.insert(key.clone(), value.clone());
186                }
187            }
188        }
189        // For non-table values, overlay replaces base entirely
190        (base, overlay) => {
191            *base = overlay.clone();
192        }
193    }
194}
195
196impl CamelConfig {
197    pub fn from_file(path: &str) -> Result<Self, ConfigError> {
198        Self::from_file_with_profile(path, None)
199    }
200
201    pub fn from_file_with_env(path: &str) -> Result<Self, ConfigError> {
202        Self::from_file_with_profile_and_env(path, None)
203    }
204
205    pub fn from_file_with_profile(path: &str, profile: Option<&str>) -> Result<Self, ConfigError> {
206        // Get profile from parameter or environment variable
207        let env_profile = env::var("CAMEL_PROFILE").ok();
208        let profile = profile.or(env_profile.as_deref());
209
210        // Read the TOML file as a generic value for deep merging
211        let content = std::fs::read_to_string(path)
212            .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
213        let mut config_value: toml::Value = toml::from_str(&content)
214            .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
215
216        // If a profile is specified, merge it with default
217        if let Some(p) = profile {
218            // Extract default config as base
219            let default_value = config_value.get("default").cloned();
220
221            // Extract profile config
222            let profile_value = config_value.get(p).cloned();
223
224            if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
225                // Deep merge profile onto default
226                merge_toml_values(&mut base, &overlay);
227
228                // Replace the entire config with the merged result
229                config_value = base;
230            } else if let Some(profile_val) = config_value.get(p).cloned() {
231                // No default, just use profile
232                config_value = profile_val;
233            } else {
234                return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
235            }
236        } else {
237            // No profile specified, use default section if it exists
238            if let Some(default_val) = config_value.get("default").cloned() {
239                config_value = default_val;
240            }
241        }
242
243        // Deserialize the merged config
244        let merged_toml = toml::to_string(&config_value).map_err(|e| {
245            ConfigError::Message(format!("Failed to serialize merged config: {}", e))
246        })?;
247
248        let config = Config::builder()
249            .add_source(config::File::from_str(
250                &merged_toml,
251                config::FileFormat::Toml,
252            ))
253            .build()?;
254
255        config.try_deserialize()
256    }
257
258    pub fn from_file_with_profile_and_env(
259        path: &str,
260        profile: Option<&str>,
261    ) -> Result<Self, ConfigError> {
262        // Get profile from parameter or environment variable
263        let env_profile = env::var("CAMEL_PROFILE").ok();
264        let profile = profile.or(env_profile.as_deref());
265
266        // Read the TOML file as a generic value for deep merging
267        let content = std::fs::read_to_string(path)
268            .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
269        let mut config_value: toml::Value = toml::from_str(&content)
270            .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
271
272        // If a profile is specified, merge it with default
273        if let Some(p) = profile {
274            // Extract default config as base
275            let default_value = config_value.get("default").cloned();
276
277            // Extract profile config
278            let profile_value = config_value.get(p).cloned();
279
280            if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
281                // Deep merge profile onto default
282                merge_toml_values(&mut base, &overlay);
283
284                // Replace the entire config with the merged result
285                config_value = base;
286            } else if let Some(profile_val) = config_value.get(p).cloned() {
287                // No default, just use profile
288                config_value = profile_val;
289            } else {
290                return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
291            }
292        } else {
293            // No profile specified, use default section if it exists
294            if let Some(default_val) = config_value.get("default").cloned() {
295                config_value = default_val;
296            }
297        }
298
299        // Deserialize the merged config and apply environment variables
300        let merged_toml = toml::to_string(&config_value).map_err(|e| {
301            ConfigError::Message(format!("Failed to serialize merged config: {}", e))
302        })?;
303
304        let config = Config::builder()
305            .add_source(config::File::from_str(
306                &merged_toml,
307                config::FileFormat::Toml,
308            ))
309            .add_source(config::Environment::with_prefix("CAMEL").try_parsing(true))
310            .build()?;
311
312        config.try_deserialize()
313    }
314
315    pub fn from_env_or_default() -> Result<Self, ConfigError> {
316        let path = env::var("CAMEL_CONFIG_FILE").unwrap_or_else(|_| "Camel.toml".to_string());
317
318        Self::from_file(&path)
319    }
320}