foxy/loader/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! High-level entry-point – "turn the key and go".
6//!
7//! The [`FoxyLoader`] consumes configuration, builds the predicate-router,
8//! wires up the filter graph and returns a single [`ProxyCore`] ready to be
9//! passed into [`ProxyServer::serve`].
10
11#[cfg(test)]
12#[path = "../../tests/unit/loader/tests.rs"]
13mod tests;
14
15use log::LevelFilter;
16use std::env;
17use std::sync::Arc;
18use thiserror::Error;
19
20use crate::config::{Config, ConfigError, ConfigProvider, EnvConfigProvider, FileConfigProvider};
21use crate::core::ProxyCore;
22use crate::logging::config::LoggingConfig;
23use crate::router::{FilterConfig, PredicateRouter};
24use crate::{
25    Filter, FilterFactory, ProxyError, ProxyServer, ServerConfig, info_fmt, init_with_config,
26};
27
28/// Errors that can occur during Foxy initialization.
29#[derive(Error, Debug)]
30pub enum LoaderError {
31    /// Configuration error
32    #[error("configuration error: {0}")]
33    ConfigError(#[from] ConfigError),
34
35    /// Proxy error
36    #[error("proxy error: {0}")]
37    ProxyError(#[from] ProxyError),
38
39    /// IO error
40    #[error("IO error: {0}")]
41    IoError(#[from] std::io::Error),
42
43    /// Generic error
44    #[error("{0}")]
45    Other(String),
46}
47
48/// Builder for initializing and configuring Foxy.
49#[derive(Debug, Default)]
50pub struct FoxyLoader {
51    config_builder: Option<Config>,
52    config_file_path: Option<String>,
53    use_env_vars: bool,
54    env_prefix: Option<String>,
55    custom_filters: Vec<Arc<dyn Filter>>,
56}
57
58impl FoxyLoader {
59    /// Create a new Foxy loader with default settings.
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Set a custom configuration to use.
65    pub fn with_config(mut self, config: Config) -> Self {
66        self.config_builder = Some(config);
67        self
68    }
69
70    /// Set a configuration file to load.
71    pub fn with_config_file(mut self, file_path: &str) -> Self {
72        self.config_file_path = Some(file_path.to_string());
73        self
74    }
75
76    /// Enable environment variable configuration.
77    pub fn with_env_vars(mut self) -> Self {
78        self.use_env_vars = true;
79        self
80    }
81
82    /// Set a custom prefix for environment variables (default is "FOXY_").
83    pub fn with_env_prefix(mut self, prefix: &str) -> Self {
84        self.env_prefix = Some(prefix.to_string());
85        self.use_env_vars = true;
86        self
87    }
88
89    /// Add a custom configuration provider.
90    pub fn with_provider<P: ConfigProvider + 'static>(self, provider: P) -> Self {
91        let config_builder = match self.config_builder {
92            Some(_) => Config::builder().with_provider(provider),
93            None => Config::builder().with_provider(provider),
94        };
95
96        Self {
97            config_builder: Some(config_builder.build()),
98            ..self
99        }
100    }
101
102    /// Add a custom filter.
103    pub fn with_filter<F: Filter + 'static>(mut self, filter: F) -> Self {
104        self.custom_filters.push(Arc::new(filter));
105        self
106    }
107
108    /// Build and initialize Foxy.
109    pub async fn build(self) -> Result<Foxy, LoaderError> {
110        // Build the configuration
111        let config = if let Some(config) = self.config_builder {
112            config
113        } else {
114            let mut config_builder = Config::builder();
115
116            // Add environment variable provider if enabled
117            if self.use_env_vars {
118                let env_provider = match self.env_prefix {
119                    Some(prefix) => EnvConfigProvider::new(&prefix),
120                    None => EnvConfigProvider::default(),
121                };
122                config_builder = config_builder.with_provider(env_provider);
123            }
124
125            // Add file configuration provider if specified
126            if let Some(file_path) = self.config_file_path {
127                match FileConfigProvider::new(&file_path) {
128                    Ok(file_provider) => {
129                        config_builder = config_builder.with_provider(file_provider);
130                    }
131                    Err(e) => {
132                        return Err(LoaderError::ConfigError(e));
133                    }
134                }
135            }
136
137            config_builder.build()
138        };
139
140        let config_arc = Arc::new(config);
141
142        // Get the full logging config from the file, or use a default if it's missing.
143        let mut logging_config: LoggingConfig = config_arc
144            .get("proxy.logging")
145            .unwrap_or(None)
146            .unwrap_or_default();
147
148        // Determine the final log level, giving precedence to the RUST_LOG environment variable.
149        let level_str_from_env = env::var("RUST_LOG").ok();
150        let final_level_str = level_str_from_env
151            .as_deref()
152            .unwrap_or(&logging_config.level);
153        let final_level_filter = final_level_str
154            .parse::<LevelFilter>()
155            .unwrap_or(LevelFilter::Info);
156
157        // Update the config object with the final, resolved level.
158        // This ensures to_logger_config() gets the correct string later.
159        logging_config.level = final_level_filter.to_string();
160
161        // Initialize all logging with this single, consistent configuration.
162        init_with_config(final_level_filter, &logging_config);
163
164        info_fmt!("Loader", "Foxy starting up");
165
166        #[cfg(feature = "opentelemetry")]
167        {
168            if let Ok(Some(otel_config)) =
169                config_arc.get::<crate::opentelemetry::OpenTelemetryConfig>("proxy.opentelemetry")
170            {
171                info_fmt!(
172                    "Loader",
173                    "OpenTelemetry initialized with endpoint: {} and service name: {}",
174                    otel_config.endpoint,
175                    otel_config.service_name
176                );
177            }
178        }
179
180        // Create the router
181        let router = PredicateRouter::new(config_arc.clone()).await?;
182
183        // Create the proxy core
184        let proxy_core = ProxyCore::new(config_arc.clone(), Arc::new(router)).await?;
185
186        // Load global filters from configuration
187        let global_filters_config: Option<Vec<FilterConfig>> =
188            config_arc.get("proxy.global_filters")?;
189
190        if let Some(global_filters) = global_filters_config {
191            for filter_config in global_filters {
192                let filter = FilterFactory::create_filter(
193                    &filter_config.type_,
194                    filter_config.config.clone(),
195                )?;
196                proxy_core.add_global_filter(filter).await;
197
198                info_fmt!("Loader", "Added global filter: {}", filter_config.type_);
199            }
200        }
201
202        // Add custom filters
203        for filter in self.custom_filters {
204            proxy_core.add_global_filter(filter).await;
205        }
206
207        // Get server configuration
208        let server_config: ServerConfig =
209            config_arc.get_or_default("server", ServerConfig::default())?;
210
211        // Create the proxy server
212        let proxy_server = ProxyServer::new(server_config, Arc::new(proxy_core));
213
214        // Create the Foxy instance
215        Ok(Foxy {
216            config: config_arc,
217            server: proxy_server,
218        })
219    }
220}
221
222/// Main Foxy struct that holds the initialized proxy.
223#[derive(Debug, Clone)]
224pub struct Foxy {
225    config: Arc<Config>,
226    server: ProxyServer,
227}
228
229impl Foxy {
230    /// Create a new loader for initializing Foxy.
231    pub fn loader() -> FoxyLoader {
232        FoxyLoader::new()
233    }
234
235    /// Get the configuration.
236    pub fn config(&self) -> &Config {
237        &self.config
238    }
239
240    /// Get access to the proxy core for adding security providers and global filters.
241    pub fn core(&self) -> &Arc<ProxyCore> {
242        self.server.core()
243    }
244
245    /// Start the proxy server.
246    pub async fn start(&self) -> Result<(), LoaderError> {
247        self.server.start().await.map_err(LoaderError::ProxyError)
248    }
249}