1#![allow(clippy::result_large_err)]
29
30mod acme;
31mod admin;
32mod balancer;
33mod batch;
34mod config;
35mod config_store;
36mod consumers;
37mod context;
38mod debug;
39mod graph;
40mod hot_reload;
41mod mcp;
42mod metrics;
43mod outbound;
44mod plugins;
45mod ratelimit;
46mod routing;
47mod server;
48mod sessions;
49mod state;
50mod stores;
51mod stream;
52#[cfg(test)]
53mod test_log;
54mod traffic;
55mod vars;
56
57use std::path::PathBuf;
58use std::sync::Arc;
59
60use clap::Parser;
61use tracing::{error, info};
62
63use crate::config::{ConfigSourceKind, GatewayConfig, SystemConfig};
64use crate::config_store::{ConfigStore, FileConfigStore};
65use crate::state::SharedState;
66
67#[derive(Parser)]
69#[command(name = "featherbit", about = "A lightweight API gateway")]
70struct Cli {
71 #[arg(long, default_value = "config/system.yaml")]
73 system_config: PathBuf,
74
75 #[arg(long, default_value = "config/gateway.yaml")]
77 gateway_config: PathBuf,
78}
79
80#[tokio::main]
81async fn main() {
82 let cli = Cli::parse();
83
84 let system: SystemConfig = match config::load_yaml_with_env(&cli.system_config) {
86 Ok(c) => c,
87 Err(e) => {
88 eprintln!("Failed to load system config: {}", e);
89 std::process::exit(1);
90 }
91 };
92
93 if let Err(e) = system.validate() {
94 eprintln!("Invalid system config: {}", e);
95 std::process::exit(1);
96 }
97
98 init_logging(&system.logging);
100
101 info!("Starting featherbit v{}", env!("CARGO_PKG_VERSION"));
102
103 let (config_store, gateway, config_path): (
105 Arc<dyn ConfigStore>,
106 GatewayConfig,
107 Option<PathBuf>,
108 ) = match system.config.source {
109 ConfigSourceKind::File => {
110 let store = Arc::new(FileConfigStore::new(cli.gateway_config.clone()));
111 let gw = match store.load_all().await {
112 Ok(c) => c,
113 Err(e) => {
114 error!("Failed to load gateway config: {}", e);
115 std::process::exit(1);
116 }
117 };
118 (store, gw, Some(cli.gateway_config.clone()))
119 }
120 ConfigSourceKind::Etcd => match build_etcd_source(&system, &cli.gateway_config).await {
121 Ok(v) => v,
122 Err(e) => {
123 error!("etcd config source: {}", e);
124 std::process::exit(1);
125 }
126 },
127 };
128
129 if let Err(e) = system.validate_against_gateway(&gateway) {
130 eprintln!("Invalid config: {}", e);
131 std::process::exit(1);
132 }
133
134 let state = match SharedState::new(system.clone(), gateway, config_path, config_store) {
136 Ok(s) => Arc::new(s),
137 Err(e) => {
138 error!("Failed to initialize gateway: {}", e);
139 std::process::exit(1);
140 }
141 };
142
143 state
144 .resources
145 .traffic
146 .cache
147 .set_capacity(system.cache.max_entries);
148
149 {
150 let routes = state.routes.read().await;
151 for (route, _) in routes.iter() {
152 info!(
153 "Route '{}' -> policy '{}' (match: {:?})",
154 route.name, route.policy, route.match_rule.path
155 );
156 }
157 }
158
159 let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false);
162 tokio::spawn(async move {
163 shutdown_signal().await;
164 info!("Shutdown signal received; draining…");
165 let _ = shutdown_tx.send(true);
166 });
167
168 match system.config.source {
170 ConfigSourceKind::File => {
171 let reload_state = state.clone();
172 let watch_path = cli.gateway_config.clone();
173 tokio::spawn(async move {
174 hot_reload::watch_config(reload_state, watch_path).await;
175 });
176 }
177 ConfigSourceKind::Etcd => {
178 spawn_etcd_watch(state.clone(), &system);
179 }
180 }
181
182 let drain_timeout = std::time::Duration::from_secs(system.timeouts.shutdown_timeout_seconds);
183
184 let admin_handle = system.admin.as_ref().map(|admin_config| {
187 let admin_state = state.clone();
188 let admin_cfg = admin_config.clone();
189 let admin_shutdown = shutdown_rx.clone();
190 tokio::spawn(async move {
191 if let Err(e) =
192 admin::start_admin_server(&admin_cfg, admin_state, admin_shutdown, drain_timeout)
193 .await
194 {
195 error!("Admin API error: {}", e);
196 }
197 })
198 });
199
200 if !system.stream.is_empty() {
203 if let Err(e) =
204 stream::start_all(&system.stream, &system.timeouts, shutdown_rx.clone()).await
205 {
206 error!("Failed to start stream listeners: {}", e);
207 std::process::exit(1);
208 }
209 }
210
211 if let Err(e) = server::start_server(&system, state, shutdown_rx).await {
214 error!("Server error: {}", e);
215 std::process::exit(1);
216 }
217
218 if let Some(handle) = admin_handle {
220 let _ = handle.await;
221 }
222 info!("Shutdown complete");
223}
224
225async fn shutdown_signal() {
228 let ctrl_c = async {
229 let _ = tokio::signal::ctrl_c().await;
230 };
231
232 #[cfg(unix)]
233 let terminate = async {
234 match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
235 Ok(mut sig) => {
236 sig.recv().await;
237 }
238 Err(_) => std::future::pending::<()>().await,
239 }
240 };
241 #[cfg(not(unix))]
242 let terminate = std::future::pending::<()>();
243
244 tokio::select! {
245 _ = ctrl_c => {},
246 _ = terminate => {},
247 }
248}
249
250async fn build_etcd_source(
253 system: &SystemConfig,
254 seed_path: &std::path::Path,
255) -> Result<(Arc<dyn ConfigStore>, GatewayConfig, Option<PathBuf>), String> {
256 config_store::etcd::build_source(system, seed_path).await
257}
258
259fn spawn_etcd_watch(state: Arc<SharedState>, system: &SystemConfig) {
261 config_store::etcd::spawn_watch(state, system);
262}
263
264fn init_logging(config: &config::LoggingConfig) {
269 let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
270 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(&config.level));
271
272 match config.format.as_str() {
273 "json" => {
274 tracing_subscriber::fmt()
275 .json()
276 .with_env_filter(env_filter)
277 .init();
278 }
279 _ => {
280 tracing_subscriber::fmt().with_env_filter(env_filter).init();
281 }
282 }
283}