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