Skip to main content

zentinel_proxy/proxy/
mod.rs

1//! Zentinel Proxy Core Implementation
2//!
3//! This module contains the main ZentinelProxy struct and its implementation,
4//! split across several submodules for maintainability:
5//!
6//! - `context`: Request context maintained throughout the request lifecycle
7//! - `handlers`: Helper methods for handling different route types
8//! - `http_trait`: ProxyHttp trait implementation for Pingora
9
10mod context;
11mod fallback;
12mod fallback_metrics;
13pub(crate) mod filters;
14mod handlers;
15mod http_trait;
16mod model_routing;
17mod model_routing_metrics;
18
19pub use context::{FallbackReason, RequestContext};
20pub use fallback::{FallbackDecision, FallbackEvaluator};
21pub use fallback_metrics::{get_fallback_metrics, init_fallback_metrics, FallbackMetrics};
22pub use model_routing::{extract_model_from_headers, find_upstream_for_model, ModelRoutingResult};
23pub use model_routing_metrics::{
24    get_model_routing_metrics, init_model_routing_metrics, ModelRoutingMetrics,
25};
26
27use anyhow::{Context, Result};
28use parking_lot::RwLock;
29use pingora::http::ResponseHeader;
30use pingora::prelude::*;
31use std::collections::HashMap;
32use std::sync::Arc;
33use std::time::Duration;
34use tokio::sync::broadcast;
35use tracing::{debug, error, info, warn};
36use uuid::Uuid;
37
38use zentinel_common::ids::{QualifiedId, Scope};
39use zentinel_common::{Registry, ScopedMetrics, ScopedRegistry};
40
41use crate::agents::AgentManager;
42use crate::app::AppState;
43use crate::builtin_handlers::BuiltinHandlerState;
44use crate::cache::{CacheConfig, CacheManager};
45use crate::errors::ErrorHandler;
46use crate::geo_filter::{GeoDatabaseWatcher, GeoFilterManager};
47use crate::health::PassiveHealthChecker;
48use crate::http_helpers;
49use crate::inference::InferenceRateLimitManager;
50use crate::logging::{LogManager, SharedLogManager};
51use crate::rate_limit::{RateLimitConfig, RateLimitManager};
52use crate::reload::{
53    ConfigManager, GracefulReloadCoordinator, ReloadEvent, RouteValidator, UpstreamValidator,
54};
55use crate::routing::RouteMatcher;
56use crate::scoped_routing::ScopedRouteMatcher;
57use crate::static_files::StaticFileServer;
58use crate::upstream::{ActiveHealthChecker, HealthCheckRunner, UpstreamPool};
59use crate::validation::SchemaValidator;
60
61use zentinel_common::TraceIdFormat;
62use zentinel_config::{Config, FlattenedConfig};
63
64/// Main proxy service implementing Pingora's ProxyHttp trait
65pub struct ZentinelProxy {
66    /// Configuration manager with hot reload
67    pub config_manager: Arc<ConfigManager>,
68    /// Route matcher (global routes only, for backward compatibility)
69    pub(super) route_matcher: Arc<RwLock<RouteMatcher>>,
70    /// Scoped route matcher (namespace/service aware)
71    pub(super) scoped_route_matcher: Arc<tokio::sync::RwLock<ScopedRouteMatcher>>,
72    /// Upstream pools (keyed by upstream ID, global only)
73    pub(super) upstream_pools: Registry<UpstreamPool>,
74    /// Scoped upstream pools (namespace/service aware)
75    pub(super) scoped_upstream_pools: ScopedRegistry<UpstreamPool>,
76    /// Agent manager for external processing
77    pub(super) agent_manager: Arc<AgentManager>,
78    /// Passive health checker
79    pub(super) passive_health: Arc<PassiveHealthChecker>,
80    /// Metrics collector
81    pub(super) metrics: Arc<zentinel_common::observability::RequestMetrics>,
82    /// Scoped metrics collector (with namespace/service labels)
83    pub(super) scoped_metrics: Arc<ScopedMetrics>,
84    /// Application state
85    pub(super) app_state: Arc<AppState>,
86    /// Graceful reload coordinator
87    pub(super) reload_coordinator: Arc<GracefulReloadCoordinator>,
88    /// Error handlers per route (keyed by route ID)
89    pub(super) error_handlers: Registry<ErrorHandler>,
90    /// API schema validators per route (keyed by route ID)
91    pub(super) validators: Registry<SchemaValidator>,
92    /// Static file servers per route (keyed by route ID)
93    pub(super) static_servers: Registry<StaticFileServer>,
94    /// Builtin handler state
95    pub(super) builtin_state: Arc<BuiltinHandlerState>,
96    /// Log manager for file-based logging
97    pub(super) log_manager: SharedLogManager,
98    /// Trace ID format for request tracing
99    pub(super) trace_id_format: TraceIdFormat,
100    /// Active health check runner
101    pub(super) health_check_runner: Arc<HealthCheckRunner>,
102    /// Rate limit manager
103    pub(super) rate_limit_manager: Arc<RateLimitManager>,
104    /// HTTP cache manager
105    pub(super) cache_manager: Arc<CacheManager>,
106    /// GeoIP filter manager
107    pub(super) geo_filter_manager: Arc<GeoFilterManager>,
108    /// Inference rate limit manager (token-based rate limiting for LLM/AI routes)
109    pub(super) inference_rate_limit_manager: Arc<InferenceRateLimitManager>,
110    /// Warmth tracker for cold model detection on inference routes
111    pub(super) warmth_tracker: Arc<crate::health::WarmthTracker>,
112    /// Guardrail processor for semantic inspection (prompt injection, PII detection)
113    pub(super) guardrail_processor: Arc<crate::inference::GuardrailProcessor>,
114    /// ACME challenge manager for HTTP-01 challenge handling
115    /// Present only when ACME is configured for at least one listener
116    pub acme_challenges: Option<Arc<crate::acme::ChallengeManager>>,
117    /// ACME clients for certificate management
118    /// Present only when ACME is configured
119    pub acme_clients: Vec<Arc<crate::acme::AcmeClient>>,
120}
121
122impl ZentinelProxy {
123    /// Create new proxy instance
124    ///
125    /// If config_path is None, uses the embedded default configuration.
126    /// Note: Tracing must be initialized by the caller before calling this function.
127    pub async fn new(config_path: Option<&str>) -> Result<Self> {
128        info!("Starting Zentinel Proxy");
129
130        // Load initial configuration
131        let (config, effective_config_path) = match config_path {
132            Some(path) => {
133                let cfg = Config::from_file(path).context("Failed to load configuration file")?;
134                (cfg, path.to_string())
135            }
136            None => {
137                let cfg = Config::default_embedded()
138                    .context("Failed to load embedded default configuration")?;
139                // Use a zentinel path to indicate embedded config
140                (cfg, "_embedded_".to_string())
141            }
142        };
143
144        config
145            .validate()
146            .context("Initial configuration validation failed")?;
147
148        // Configure global cache storage (must be done before cache is accessed)
149        if let Some(ref cache_config) = config.cache {
150            info!(
151                max_size_mb = cache_config.max_size_bytes / 1024 / 1024,
152                backend = ?cache_config.backend,
153                "Configuring HTTP cache storage"
154            );
155            crate::cache::configure_cache(cache_config.clone());
156            crate::cache::init_disk_cache_state().await;
157        }
158
159        // Create configuration manager
160        let config_manager =
161            Arc::new(ConfigManager::new(&effective_config_path, config.clone()).await?);
162
163        // Add validators
164        config_manager.add_validator(Box::new(RouteValidator)).await;
165        config_manager
166            .add_validator(Box::new(UpstreamValidator))
167            .await;
168
169        // Create route matcher (global routes only)
170        let route_matcher = Arc::new(RwLock::new(RouteMatcher::new(config.routes.clone(), None)?));
171
172        // Flatten config for namespace/service resources
173        let flattened = config.flatten();
174
175        // Create scoped route matcher
176        let scoped_route_matcher = Arc::new(tokio::sync::RwLock::new(
177            ScopedRouteMatcher::from_flattened(&flattened)
178                .await
179                .context("Failed to create scoped route matcher")?,
180        ));
181
182        // Create upstream pools and active health checkers (global only)
183        let mut pools = HashMap::new();
184        let mut health_check_runner = HealthCheckRunner::new();
185
186        for (upstream_id, upstream_config) in &config.upstreams {
187            let mut config_with_id = upstream_config.clone();
188            config_with_id.id = upstream_id.clone();
189            let pool = Arc::new(UpstreamPool::new(config_with_id.clone()).await?);
190            pools.insert(upstream_id.clone(), pool);
191
192            // Create active health checker if health check is configured
193            if let Some(checker) = ActiveHealthChecker::new(&config_with_id) {
194                health_check_runner.add_checker(checker);
195            }
196        }
197        let upstream_pools = Registry::from_map(pools);
198
199        // Create scoped upstream pools from flattened config
200        let scoped_upstream_pools =
201            Self::create_scoped_upstream_pools(&flattened, &mut health_check_runner).await?;
202
203        let health_check_runner = Arc::new(health_check_runner);
204
205        // Create passive health checker
206        let passive_health = Arc::new(PassiveHealthChecker::new(
207            0.5,  // 50% failure rate threshold
208            100,  // Window size
209            None, // Will be linked to active health checkers
210        ));
211
212        // Create agent manager (per-agent queue isolation)
213        let agent_manager = Arc::new(AgentManager::new(config.agents.clone()).await?);
214        agent_manager.initialize().await?;
215
216        // Create metrics collectors
217        let metrics = Arc::new(zentinel_common::observability::RequestMetrics::new()?);
218        let scoped_metrics =
219            Arc::new(ScopedMetrics::new().context("Failed to create scoped metrics collector")?);
220
221        // Create application state
222        let app_state = Arc::new(AppState::new(Uuid::new_v4().to_string()));
223
224        // Create reload coordinator
225        let reload_coordinator = Arc::new(GracefulReloadCoordinator::new(
226            Duration::from_secs(30), // Max drain time
227        ));
228
229        // Setup configuration reload subscription
230        Self::setup_reload_handler(
231            config_manager.clone(),
232            route_matcher.clone(),
233            upstream_pools.clone(),
234            scoped_route_matcher.clone(),
235            scoped_upstream_pools.clone(),
236        )
237        .await;
238
239        // Initialize service type components
240        let (error_handlers, validators, static_servers) =
241            Self::initialize_route_components(&config).await?;
242
243        // Create builtin handler state
244        let builtin_state = Arc::new(BuiltinHandlerState::new(
245            env!("CARGO_PKG_VERSION").to_string(),
246            app_state.instance_id.clone(),
247        ));
248
249        // Create log manager for file-based logging
250        let log_manager = match LogManager::new(&config.observability.logging) {
251            Ok(manager) => {
252                if manager.access_log_enabled() {
253                    info!("Access logging enabled");
254                }
255                if manager.error_log_enabled() {
256                    info!("Error logging enabled");
257                }
258                if manager.audit_log_enabled() {
259                    info!("Audit logging enabled");
260                }
261                Arc::new(manager)
262            }
263            Err(e) => {
264                warn!(
265                    "Failed to initialize log manager, file logging disabled: {}",
266                    e
267                );
268                Arc::new(LogManager::disabled())
269            }
270        };
271
272        // Register audit reload hook to log configuration changes
273        {
274            use crate::reload::AuditReloadHook;
275            let audit_hook = AuditReloadHook::new(log_manager.clone());
276            config_manager.add_hook(Box::new(audit_hook)).await;
277            debug!("Registered audit reload hook");
278        }
279
280        // Start active health check runner in background
281        if health_check_runner.checker_count() > 0 {
282            let runner = health_check_runner.clone();
283            tokio::spawn(async move {
284                runner.run().await;
285            });
286            info!(
287                "Started active health checking for {} upstreams",
288                health_check_runner.checker_count()
289            );
290        }
291
292        // Initialize rate limit manager
293        let rate_limit_manager = Arc::new(Self::initialize_rate_limiters(&config));
294
295        // Initialize inference rate limit manager (for token-based LLM rate limiting)
296        let inference_rate_limit_manager =
297            Arc::new(Self::initialize_inference_rate_limiters(&config));
298
299        // Initialize warmth tracker for cold model detection
300        let warmth_tracker = Arc::new(crate::health::WarmthTracker::with_defaults());
301
302        // Initialize guardrail processor for semantic inspection
303        let guardrail_processor = Arc::new(crate::inference::GuardrailProcessor::new(
304            agent_manager.clone(),
305        ));
306
307        // Initialize geo filter manager
308        let geo_filter_manager = Arc::new(Self::initialize_geo_filters(&config));
309
310        // Start periodic cleanup task for rate limiters and geo caches
311        Self::spawn_cleanup_task(rate_limit_manager.clone(), geo_filter_manager.clone());
312
313        // Start geo database file watcher for hot reload
314        Self::spawn_geo_database_watcher(geo_filter_manager.clone());
315
316        // Mark as ready
317        app_state.set_ready(true);
318
319        // Get trace ID format from config
320        let trace_id_format = config.server.trace_id_format;
321
322        // Initialize cache manager
323        let cache_manager = Arc::new(Self::initialize_cache_manager(&config));
324
325        // Initialize fallback metrics (best-effort, log warning if fails)
326        if let Err(e) = init_fallback_metrics() {
327            warn!("Failed to initialize fallback metrics: {}", e);
328        }
329
330        // Initialize model routing metrics (best-effort, log warning if fails)
331        if let Err(e) = init_model_routing_metrics() {
332            warn!("Failed to initialize model routing metrics: {}", e);
333        }
334
335        // Initialize TLS metrics (best-effort, log warning if fails)
336        if let Err(e) = crate::tls_metrics::init_tls_metrics() {
337            warn!("Failed to initialize TLS metrics: {}", e);
338        }
339
340        Ok(Self {
341            config_manager,
342            route_matcher,
343            scoped_route_matcher,
344            upstream_pools,
345            scoped_upstream_pools,
346            agent_manager,
347            passive_health,
348            metrics,
349            scoped_metrics,
350            app_state,
351            reload_coordinator,
352            error_handlers,
353            validators,
354            static_servers,
355            builtin_state,
356            log_manager,
357            trace_id_format,
358            health_check_runner,
359            rate_limit_manager,
360            cache_manager,
361            geo_filter_manager,
362            inference_rate_limit_manager,
363            warmth_tracker,
364            guardrail_processor,
365            // ACME challenge manager - initialized later if ACME is configured
366            acme_challenges: None,
367            acme_clients: Vec::new(),
368        })
369    }
370
371    /// Setup the configuration reload handler
372    async fn setup_reload_handler(
373        config_manager: Arc<ConfigManager>,
374        route_matcher: Arc<RwLock<RouteMatcher>>,
375        upstream_pools: Registry<UpstreamPool>,
376        scoped_route_matcher: Arc<tokio::sync::RwLock<ScopedRouteMatcher>>,
377        scoped_upstream_pools: ScopedRegistry<UpstreamPool>,
378    ) {
379        let mut reload_rx = config_manager.subscribe();
380        let config_manager_clone = config_manager.clone();
381
382        tokio::spawn(async move {
383            loop {
384                match reload_rx.recv().await {
385                    Ok(ReloadEvent::Applied { .. }) => {}
386                    Ok(_) => continue,
387                    Err(broadcast::error::RecvError::Lagged(n)) => {
388                        warn!("Reload handler lagged by {n} events, applying latest config");
389                        // Fall through to reload with the latest config
390                    }
391                    Err(broadcast::error::RecvError::Closed) => break,
392                };
393                {
394                    // Reload routes and upstreams
395                    let new_config = config_manager_clone.current();
396                    let flattened = new_config.flatten();
397
398                    // Update route matcher FIRST (most critical for traffic)
399                    match RouteMatcher::new(new_config.routes.clone(), None) {
400                        Ok(new_matcher) => {
401                            *route_matcher.write() = new_matcher;
402                            info!(
403                                routes = new_config.routes.len(),
404                                "Global routes reloaded successfully"
405                            );
406                        }
407                        Err(e) => {
408                            error!(error = %e, "Failed to compile route matcher");
409                        }
410                    }
411
412                    // Update scoped route matcher
413                    if let Err(e) = scoped_route_matcher
414                        .write()
415                        .await
416                        .load_from_flattened(&flattened)
417                        .await
418                    {
419                        error!("Failed to reload scoped routes: {}", e);
420                    }
421
422                    // Update upstream pools with timeout to avoid blocking
423                    // the reload handler on DNS resolution / connection attempts
424                    let pool_update = async {
425                        let mut new_pools = HashMap::new();
426                        for (upstream_id, upstream_config) in &new_config.upstreams {
427                            let mut config_with_id = upstream_config.clone();
428                            config_with_id.id = upstream_id.clone();
429                            match UpstreamPool::new(config_with_id).await {
430                                Ok(pool) => {
431                                    new_pools.insert(upstream_id.clone(), Arc::new(pool));
432                                }
433                                Err(e) => {
434                                    error!("Failed to create upstream pool {}: {}", upstream_id, e);
435                                }
436                            }
437                        }
438                        new_pools
439                    };
440
441                    match tokio::time::timeout(Duration::from_secs(10), pool_update).await {
442                        Ok(new_pools) => {
443                            let old_pools = upstream_pools.replace(new_pools).await;
444
445                            // Update scoped upstream pools
446                            let new_scoped_pools = Self::build_scoped_pools_list(&flattened).await;
447                            let old_scoped_pools =
448                                scoped_upstream_pools.replace_all(new_scoped_pools).await;
449
450                            // Track drain lifecycle for old pools
451                            tokio::spawn(async move {
452                                let tracker = crate::upstream::drain::DrainTracker::default();
453                                tracker.track_pools(old_pools).await;
454                                tracker.track_pools(old_scoped_pools).await;
455                            });
456                        }
457                        Err(_) => {
458                            warn!("Upstream pool update timed out after 10s, routes still updated");
459                        }
460                    }
461                }
462            }
463        });
464    }
465
466    /// Create scoped upstream pools from flattened config
467    async fn create_scoped_upstream_pools(
468        flattened: &FlattenedConfig,
469        health_check_runner: &mut HealthCheckRunner,
470    ) -> Result<ScopedRegistry<UpstreamPool>> {
471        let registry = ScopedRegistry::new();
472
473        for (qid, upstream_config) in &flattened.upstreams {
474            let mut config_with_id = upstream_config.clone();
475            config_with_id.id = qid.canonical();
476
477            let pool = Arc::new(
478                UpstreamPool::new(config_with_id.clone())
479                    .await
480                    .with_context(|| {
481                        format!("Failed to create upstream pool '{}'", qid.canonical())
482                    })?,
483            );
484
485            // Track exports
486            let is_exported = flattened
487                .exported_upstreams
488                .contains_key(&upstream_config.id);
489
490            if is_exported {
491                registry.insert_exported(qid.clone(), pool).await;
492            } else {
493                registry.insert(qid.clone(), pool).await;
494            }
495
496            // Create active health checker if configured
497            if let Some(checker) = ActiveHealthChecker::new(&config_with_id) {
498                health_check_runner.add_checker(checker);
499            }
500
501            debug!(
502                upstream_id = %qid.canonical(),
503                scope = ?qid.scope,
504                exported = is_exported,
505                "Created scoped upstream pool"
506            );
507        }
508
509        info!("Created {} scoped upstream pools", registry.len().await);
510
511        Ok(registry)
512    }
513
514    /// Build list of scoped pools for atomic replacement
515    async fn build_scoped_pools_list(
516        flattened: &FlattenedConfig,
517    ) -> Vec<(QualifiedId, Arc<UpstreamPool>, bool)> {
518        let mut result = Vec::new();
519
520        for (qid, upstream_config) in &flattened.upstreams {
521            let mut config_with_id = upstream_config.clone();
522            config_with_id.id = qid.canonical();
523
524            match UpstreamPool::new(config_with_id).await {
525                Ok(pool) => {
526                    let is_exported = flattened
527                        .exported_upstreams
528                        .contains_key(&upstream_config.id);
529                    result.push((qid.clone(), Arc::new(pool), is_exported));
530                }
531                Err(e) => {
532                    error!(
533                        "Failed to create scoped upstream pool {}: {}",
534                        qid.canonical(),
535                        e
536                    );
537                }
538            }
539        }
540
541        result
542    }
543
544    /// Initialize route-specific components (error handlers, validators, static servers)
545    async fn initialize_route_components(
546        config: &Config,
547    ) -> Result<(
548        Registry<ErrorHandler>,
549        Registry<SchemaValidator>,
550        Registry<StaticFileServer>,
551    )> {
552        let mut error_handlers_map = HashMap::new();
553        let mut validators_map = HashMap::new();
554        let mut static_servers_map = HashMap::new();
555
556        for route in &config.routes {
557            info!(
558                "Initializing components for route: {} with service type: {:?}",
559                route.id, route.service_type
560            );
561
562            // Initialize error handler for each route
563            if let Some(ref error_config) = route.error_pages {
564                let handler =
565                    ErrorHandler::new(route.service_type.clone(), Some(error_config.clone()));
566                error_handlers_map.insert(route.id.clone(), Arc::new(handler));
567                debug!("Initialized error handler for route: {}", route.id);
568            } else {
569                // Use default error handler for the service type
570                let handler = ErrorHandler::new(route.service_type.clone(), None);
571                error_handlers_map.insert(route.id.clone(), Arc::new(handler));
572            }
573
574            // Initialize schema validator for API routes
575            if route.service_type == zentinel_config::ServiceType::Api {
576                if let Some(ref api_schema) = route.api_schema {
577                    match SchemaValidator::new(api_schema.clone()) {
578                        Ok(validator) => {
579                            validators_map.insert(route.id.clone(), Arc::new(validator));
580                            info!("Initialized schema validator for route: {}", route.id);
581                        }
582                        Err(e) => {
583                            warn!(
584                                "Failed to initialize schema validator for route {}: {}",
585                                route.id, e
586                            );
587                        }
588                    }
589                }
590            }
591
592            // Initialize static file server for static routes
593            if route.service_type == zentinel_config::ServiceType::Static {
594                if let Some(ref static_config) = route.static_files {
595                    let server = StaticFileServer::new(static_config.clone());
596                    static_servers_map.insert(route.id.clone(), Arc::new(server));
597                    info!("Initialized static file server for route: {}", route.id);
598                } else {
599                    warn!(
600                        "Static route {} has no static_files configuration",
601                        route.id
602                    );
603                }
604            }
605        }
606
607        Ok((
608            Registry::from_map(error_handlers_map),
609            Registry::from_map(validators_map),
610            Registry::from_map(static_servers_map),
611        ))
612    }
613
614    /// Get or generate trace ID from session
615    pub(super) fn get_trace_id(&self, session: &pingora::proxy::Session) -> String {
616        http_helpers::get_or_create_trace_id(session, self.trace_id_format)
617    }
618
619    /// Initialize rate limiters from configuration
620    fn initialize_rate_limiters(config: &Config) -> RateLimitManager {
621        use zentinel_config::RateLimitAction;
622
623        // Create manager with global rate limit if configured
624        let manager = if let Some(ref global) = config.rate_limits.global {
625            info!(
626                max_rps = global.max_rps,
627                burst = global.burst,
628                key = ?global.key,
629                "Initializing global rate limiter"
630            );
631            RateLimitManager::with_global_limit(global.max_rps, global.burst)
632        } else {
633            RateLimitManager::new()
634        };
635
636        for route in &config.routes {
637            // Check for rate limit in route policies
638            if let Some(ref rate_limit) = route.policies.rate_limit {
639                let rl_config = RateLimitConfig {
640                    max_rps: rate_limit.requests_per_second,
641                    burst: rate_limit.burst,
642                    key: rate_limit.key.clone(),
643                    action: RateLimitAction::Reject,
644                    status_code: 429,
645                    message: None,
646                    backend: zentinel_config::RateLimitBackend::Local,
647                    max_delay_ms: 5000, // Default for policy-based rate limits
648                };
649                manager.register_route(&route.id, rl_config);
650                info!(
651                    route_id = %route.id,
652                    max_rps = rate_limit.requests_per_second,
653                    burst = rate_limit.burst,
654                    key = ?rate_limit.key,
655                    "Registered rate limiter for route"
656                );
657            }
658
659            // Also check for rate limit filters in the filter chain
660            for filter_id in &route.filters {
661                if let Some(filter_config) = config.filters.get(filter_id) {
662                    if let zentinel_config::Filter::RateLimit(ref rl_filter) = filter_config.filter
663                    {
664                        let rl_config = RateLimitConfig {
665                            max_rps: rl_filter.max_rps,
666                            burst: rl_filter.burst,
667                            key: rl_filter.key.clone(),
668                            action: rl_filter.on_limit.clone(),
669                            status_code: rl_filter.status_code,
670                            message: rl_filter.limit_message.clone(),
671                            backend: rl_filter.backend.clone(),
672                            max_delay_ms: rl_filter.max_delay_ms,
673                        };
674                        manager.register_route(&route.id, rl_config);
675                        info!(
676                            route_id = %route.id,
677                            filter_id = %filter_id,
678                            max_rps = rl_filter.max_rps,
679                            backend = ?rl_filter.backend,
680                            "Registered rate limiter from filter for route"
681                        );
682                    }
683                }
684            }
685        }
686
687        if manager.route_count() > 0 {
688            info!(
689                route_count = manager.route_count(),
690                "Rate limiting initialized"
691            );
692        }
693
694        manager
695    }
696
697    /// Initialize inference rate limiters from configuration
698    ///
699    /// This creates token-based rate limiters for routes with `service-type "inference"`
700    /// and inference config blocks.
701    fn initialize_inference_rate_limiters(config: &Config) -> InferenceRateLimitManager {
702        let manager = InferenceRateLimitManager::new();
703
704        for route in &config.routes {
705            // Only initialize for inference service type routes with inference config
706            if route.service_type == zentinel_config::ServiceType::Inference {
707                if let Some(ref inference_config) = route.inference {
708                    manager.register_route(&route.id, inference_config);
709                }
710            }
711        }
712
713        if manager.route_count() > 0 {
714            info!(
715                route_count = manager.route_count(),
716                "Inference rate limiting initialized"
717            );
718        }
719
720        manager
721    }
722
723    /// Initialize cache manager from configuration
724    fn initialize_cache_manager(config: &Config) -> CacheManager {
725        let manager = CacheManager::new();
726
727        let mut enabled_count = 0;
728
729        for route in &config.routes {
730            // Use per-route cache config if present, otherwise fall back to service-type defaults
731            let cache_config = if let Some(ref rc) = route.policies.cache {
732                // Pre-compile exclude_paths glob patterns into regex at registration time
733                let exclude_paths = rc
734                    .exclude_paths
735                    .iter()
736                    .filter_map(|pattern| {
737                        let regex_str = crate::cache::compile_glob_to_regex(pattern);
738                        match regex::Regex::new(&regex_str) {
739                            Ok(re) => Some(re),
740                            Err(e) => {
741                                warn!(
742                                    route_id = %route.id,
743                                    pattern = %pattern,
744                                    error = %e,
745                                    "Failed to compile cache exclude-path pattern"
746                                );
747                                None
748                            }
749                        }
750                    })
751                    .collect();
752
753                CacheConfig {
754                    enabled: rc.enabled,
755                    default_ttl_secs: rc.default_ttl_secs,
756                    max_size_bytes: rc.max_size_bytes,
757                    cache_private: rc.cache_private,
758                    stale_while_revalidate_secs: rc.stale_while_revalidate_secs,
759                    stale_if_error_secs: rc.stale_if_error_secs,
760                    cacheable_methods: rc.cacheable_methods.clone(),
761                    cacheable_status_codes: rc.cacheable_status_codes.clone(),
762                    exclude_extensions: rc.exclude_extensions.clone(),
763                    exclude_paths,
764                }
765            } else {
766                match route.service_type {
767                    zentinel_config::ServiceType::Static => CacheConfig {
768                        enabled: true,
769                        default_ttl_secs: 3600,
770                        max_size_bytes: 50 * 1024 * 1024, // 50MB for static
771                        stale_while_revalidate_secs: 60,
772                        stale_if_error_secs: 300,
773                        ..Default::default()
774                    },
775                    zentinel_config::ServiceType::Api => CacheConfig {
776                        enabled: false,
777                        default_ttl_secs: 60,
778                        ..Default::default()
779                    },
780                    zentinel_config::ServiceType::Web => CacheConfig {
781                        enabled: false,
782                        default_ttl_secs: 300,
783                        ..Default::default()
784                    },
785                    _ => CacheConfig::default(),
786                }
787            };
788
789            if cache_config.enabled {
790                enabled_count += 1;
791                info!(
792                    route_id = %route.id,
793                    default_ttl_secs = cache_config.default_ttl_secs,
794                    from_config = route.policies.cache.is_some(),
795                    "HTTP caching enabled for route"
796                );
797            }
798            manager.register_route(&route.id, cache_config);
799        }
800
801        if enabled_count > 0 {
802            info!(enabled_routes = enabled_count, "HTTP caching initialized");
803        } else {
804            debug!("HTTP cache manager initialized (no routes with caching enabled)");
805        }
806
807        manager
808    }
809
810    /// Initialize geo filters from configuration
811    fn initialize_geo_filters(config: &Config) -> GeoFilterManager {
812        let manager = GeoFilterManager::new();
813
814        for (filter_id, filter_config) in &config.filters {
815            if let zentinel_config::Filter::Geo(ref geo_filter) = filter_config.filter {
816                match manager.register_filter(filter_id, geo_filter.clone()) {
817                    Ok(_) => {
818                        info!(
819                            filter_id = %filter_id,
820                            database_path = %geo_filter.database_path,
821                            action = ?geo_filter.action,
822                            countries_count = geo_filter.countries.len(),
823                            "Registered geo filter"
824                        );
825                    }
826                    Err(e) => {
827                        error!(
828                            filter_id = %filter_id,
829                            error = %e,
830                            "Failed to register geo filter"
831                        );
832                    }
833                }
834            }
835        }
836
837        let filter_ids = manager.filter_ids();
838        if !filter_ids.is_empty() {
839            info!(
840                filter_count = filter_ids.len(),
841                filter_ids = ?filter_ids,
842                "GeoIP filtering initialized"
843            );
844        }
845
846        manager
847    }
848
849    /// Spawn background task to periodically clean up idle rate limiters and expired geo caches
850    fn spawn_cleanup_task(
851        rate_limit_manager: Arc<RateLimitManager>,
852        geo_filter_manager: Arc<GeoFilterManager>,
853    ) {
854        // Cleanup interval: 5 minutes
855        const CLEANUP_INTERVAL: Duration = Duration::from_secs(300);
856
857        tokio::spawn(async move {
858            let mut interval = tokio::time::interval(CLEANUP_INTERVAL);
859            // First tick completes immediately; skip it
860            interval.tick().await;
861
862            loop {
863                interval.tick().await;
864
865                // Clean up rate limiters (removes entries when pool exceeds max size)
866                rate_limit_manager.cleanup();
867
868                // Clean up expired geo filter caches
869                geo_filter_manager.clear_expired_caches();
870
871                debug!("Periodic cleanup completed");
872            }
873        });
874
875        info!(
876            interval_secs = CLEANUP_INTERVAL.as_secs(),
877            "Started periodic cleanup task"
878        );
879    }
880
881    /// Spawn background task to watch geo database files for changes
882    fn spawn_geo_database_watcher(geo_filter_manager: Arc<GeoFilterManager>) {
883        let watcher = Arc::new(GeoDatabaseWatcher::new(geo_filter_manager));
884
885        // Try to start watching
886        match watcher.start_watching() {
887            Ok(mut rx) => {
888                let watcher_clone = watcher.clone();
889                tokio::spawn(async move {
890                    // Debounce interval
891                    const DEBOUNCE_MS: u64 = 500;
892
893                    while let Some(path) = rx.recv().await {
894                        // Debounce rapid changes (e.g., temp file then rename)
895                        tokio::time::sleep(Duration::from_millis(DEBOUNCE_MS)).await;
896
897                        // Drain any additional events for the same path during debounce
898                        while rx.try_recv().is_ok() {}
899
900                        // Handle the change
901                        watcher_clone.handle_change(&path);
902                    }
903                });
904
905                info!("Started geo database file watcher");
906            }
907            Err(e) => {
908                warn!(
909                    error = %e,
910                    "Failed to start geo database file watcher, auto-reload disabled"
911                );
912            }
913        }
914    }
915}