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    /// Per-listener route matchers for listeners bound to a namespace route set,
71    /// keyed by the listener's bound address. A request arriving on one of these
72    /// listeners is matched only against its namespace's routes (isolated from
73    /// the global set). Listeners absent from this map use [`Self::route_matcher`].
74    pub(super) listener_matchers: Arc<RwLock<HashMap<String, Arc<RouteMatcher>>>>,
75    /// Scoped route matcher (namespace/service aware)
76    pub(super) scoped_route_matcher: Arc<tokio::sync::RwLock<ScopedRouteMatcher>>,
77    /// Upstream pools (keyed by upstream ID, global only)
78    pub(super) upstream_pools: Registry<UpstreamPool>,
79    /// Scoped upstream pools (namespace/service aware)
80    pub(super) scoped_upstream_pools: ScopedRegistry<UpstreamPool>,
81    /// Agent manager for external processing
82    pub(super) agent_manager: Arc<AgentManager>,
83    /// Passive health checker
84    pub(super) passive_health: Arc<PassiveHealthChecker>,
85    /// Metrics collector
86    pub(super) metrics: Arc<zentinel_common::observability::RequestMetrics>,
87    /// Scoped metrics collector (with namespace/service labels)
88    pub(super) scoped_metrics: Arc<ScopedMetrics>,
89    /// Application state
90    pub(super) app_state: Arc<AppState>,
91    /// Graceful reload coordinator
92    pub(super) reload_coordinator: Arc<GracefulReloadCoordinator>,
93    /// Error handlers per route (keyed by route ID)
94    pub(super) error_handlers: Registry<ErrorHandler>,
95    /// API schema validators per route (keyed by route ID)
96    pub(super) validators: Registry<SchemaValidator>,
97    /// Static file servers per route (keyed by route ID)
98    pub(super) static_servers: Registry<StaticFileServer>,
99    /// Builtin handler state
100    pub(super) builtin_state: Arc<BuiltinHandlerState>,
101    /// Log manager for file-based logging
102    pub(super) log_manager: SharedLogManager,
103    /// Trace ID format for request tracing
104    pub(super) trace_id_format: TraceIdFormat,
105    /// Active health check runner
106    pub(super) health_check_runner: Arc<HealthCheckRunner>,
107    /// Rate limit manager
108    pub(super) rate_limit_manager: Arc<RateLimitManager>,
109    /// HTTP cache manager
110    pub(super) cache_manager: Arc<CacheManager>,
111    /// GeoIP filter manager
112    pub(super) geo_filter_manager: Arc<GeoFilterManager>,
113    /// Inference rate limit manager (token-based rate limiting for LLM/AI routes)
114    pub(super) inference_rate_limit_manager: Arc<InferenceRateLimitManager>,
115    /// Warmth tracker for cold model detection on inference routes
116    pub(super) warmth_tracker: Arc<crate::health::WarmthTracker>,
117    /// Guardrail processor for semantic inspection (prompt injection, PII detection)
118    pub(super) guardrail_processor: Arc<crate::inference::GuardrailProcessor>,
119    /// ACME challenge manager for HTTP-01 challenge handling
120    /// Present only when ACME is configured for at least one listener
121    pub acme_challenges: Option<Arc<crate::acme::ChallengeManager>>,
122    /// ACME clients for certificate management
123    /// Present only when ACME is configured
124    pub acme_clients: Vec<Arc<crate::acme::AcmeClient>>,
125}
126
127impl ZentinelProxy {
128    /// Create new proxy instance
129    ///
130    /// If config_path is None, uses the embedded default configuration.
131    /// Note: Tracing must be initialized by the caller before calling this function.
132    pub async fn new(config_path: Option<&str>) -> Result<Self> {
133        info!("Starting Zentinel Proxy");
134
135        // Load initial configuration
136        let (config, effective_config_path) = match config_path {
137            Some(path) => {
138                let cfg = Config::from_file(path).context("Failed to load configuration file")?;
139                (cfg, path.to_string())
140            }
141            None => {
142                let cfg = Config::default_embedded()
143                    .context("Failed to load embedded default configuration")?;
144                // Use a zentinel path to indicate embedded config
145                (cfg, "_embedded_".to_string())
146            }
147        };
148
149        config
150            .validate()
151            .context("Initial configuration validation failed")?;
152
153        // Configure global cache storage (must be done before cache is accessed)
154        if let Some(ref cache_config) = config.cache {
155            info!(
156                max_size_mb = cache_config.max_size_bytes / 1024 / 1024,
157                backend = ?cache_config.backend,
158                "Configuring HTTP cache storage"
159            );
160            crate::cache::configure_cache(cache_config.clone());
161            crate::cache::init_disk_cache_state().await;
162        }
163
164        // Create configuration manager
165        let config_manager =
166            Arc::new(ConfigManager::new(&effective_config_path, config.clone()).await?);
167
168        // Add validators
169        config_manager.add_validator(Box::new(RouteValidator)).await;
170        config_manager
171            .add_validator(Box::new(UpstreamValidator))
172            .await;
173
174        // Create route matcher (global routes only)
175        let route_matcher = Arc::new(RwLock::new(RouteMatcher::with_cache_size(
176            config.routes.clone(),
177            None,
178            config.server.route_cache_size,
179        )?));
180
181        // Build per-listener route matchers for listeners bound to a namespace
182        // route set (empty unless any listener references a namespace).
183        let listener_matchers = Arc::new(RwLock::new(Self::build_listener_matchers(&config)));
184
185        // Flatten config for namespace/service resources
186        let flattened = config.flatten();
187
188        // Create scoped route matcher
189        let scoped_route_matcher = Arc::new(tokio::sync::RwLock::new(
190            ScopedRouteMatcher::from_flattened(&flattened)
191                .await
192                .context("Failed to create scoped route matcher")?,
193        ));
194
195        // Create upstream pools and active health checkers (global only)
196        let mut pools = HashMap::new();
197        let mut health_check_runner = HealthCheckRunner::new();
198
199        for (upstream_id, upstream_config) in &config.upstreams {
200            let mut config_with_id = upstream_config.clone();
201            config_with_id.id = upstream_id.clone();
202            let pool = Arc::new(UpstreamPool::new(config_with_id.clone()).await?);
203            pools.insert(upstream_id.clone(), pool);
204
205            // Create active health checker if health check is configured
206            if let Some(checker) = ActiveHealthChecker::new(&config_with_id) {
207                health_check_runner.add_checker(checker);
208            }
209        }
210        let upstream_pools = Registry::from_map(pools);
211
212        // Create scoped upstream pools from flattened config
213        let scoped_upstream_pools =
214            Self::create_scoped_upstream_pools(&flattened, &mut health_check_runner).await?;
215
216        let health_check_runner = Arc::new(health_check_runner);
217
218        // Create passive health checker
219        let passive_health = Arc::new(PassiveHealthChecker::new(
220            0.5,  // 50% failure rate threshold
221            100,  // Window size
222            None, // Will be linked to active health checkers
223        ));
224
225        // Create agent manager (per-agent queue isolation)
226        let agent_manager = Arc::new(AgentManager::new(config.agents.clone()).await?);
227        agent_manager.initialize().await?;
228
229        // Create metrics collectors
230        let metrics = Arc::new(zentinel_common::observability::RequestMetrics::new()?);
231        let scoped_metrics =
232            Arc::new(ScopedMetrics::new().context("Failed to create scoped metrics collector")?);
233
234        // Create application state
235        let app_state = Arc::new(AppState::new(Uuid::new_v4().to_string()));
236
237        // Create reload coordinator
238        let reload_coordinator = Arc::new(GracefulReloadCoordinator::new(
239            Duration::from_secs(30), // Max drain time
240        ));
241
242        // Setup configuration reload subscription
243        Self::setup_reload_handler(
244            config_manager.clone(),
245            route_matcher.clone(),
246            listener_matchers.clone(),
247            upstream_pools.clone(),
248            scoped_route_matcher.clone(),
249            scoped_upstream_pools.clone(),
250        )
251        .await;
252
253        // Initialize service type components
254        let (error_handlers, validators, static_servers) =
255            Self::initialize_route_components(&config).await?;
256
257        // Create builtin handler state
258        let builtin_state = Arc::new(BuiltinHandlerState::new(
259            env!("CARGO_PKG_VERSION").to_string(),
260            app_state.instance_id.clone(),
261        ));
262
263        // Create log manager for file-based logging
264        let log_manager = match LogManager::new(&config.observability.logging) {
265            Ok(manager) => {
266                if manager.access_log_enabled() {
267                    info!("Access logging enabled");
268                }
269                if manager.error_log_enabled() {
270                    info!("Error logging enabled");
271                }
272                if manager.audit_log_enabled() {
273                    info!("Audit logging enabled");
274                }
275                Arc::new(manager)
276            }
277            Err(e) => {
278                warn!(
279                    "Failed to initialize log manager, file logging disabled: {}",
280                    e
281                );
282                Arc::new(LogManager::disabled())
283            }
284        };
285
286        // Register audit reload hook to log configuration changes
287        {
288            use crate::reload::AuditReloadHook;
289            let audit_hook = AuditReloadHook::new(log_manager.clone());
290            config_manager.add_hook(Box::new(audit_hook)).await;
291            debug!("Registered audit reload hook");
292        }
293
294        // Start active health check runner in background
295        if health_check_runner.checker_count() > 0 {
296            let runner = health_check_runner.clone();
297            tokio::spawn(async move {
298                runner.run().await;
299            });
300            info!(
301                "Started active health checking for {} upstreams",
302                health_check_runner.checker_count()
303            );
304        }
305
306        // Initialize rate limit manager
307        let rate_limit_manager = Arc::new(Self::initialize_rate_limiters(&config));
308
309        // Initialize inference rate limit manager (for token-based LLM rate limiting)
310        let inference_rate_limit_manager =
311            Arc::new(Self::initialize_inference_rate_limiters(&config));
312
313        // Initialize warmth tracker for cold model detection
314        let warmth_tracker = Arc::new(crate::health::WarmthTracker::with_defaults());
315
316        // Initialize guardrail processor for semantic inspection
317        let guardrail_processor = Arc::new(crate::inference::GuardrailProcessor::new(
318            agent_manager.clone(),
319        ));
320
321        // Initialize geo filter manager
322        let geo_filter_manager = Arc::new(Self::initialize_geo_filters(&config));
323
324        // Start periodic cleanup task for rate limiters and geo caches
325        Self::spawn_cleanup_task(rate_limit_manager.clone(), geo_filter_manager.clone());
326
327        // Start geo database file watcher for hot reload
328        Self::spawn_geo_database_watcher(geo_filter_manager.clone());
329
330        // Mark as ready
331        app_state.set_ready(true);
332
333        // Get trace ID format from config
334        let trace_id_format = config.server.trace_id_format;
335
336        // Initialize cache manager
337        let cache_manager = Arc::new(Self::initialize_cache_manager(&config));
338
339        // Initialize fallback metrics (best-effort, log warning if fails)
340        if let Err(e) = init_fallback_metrics() {
341            warn!("Failed to initialize fallback metrics: {}", e);
342        }
343
344        // Initialize model routing metrics (best-effort, log warning if fails)
345        if let Err(e) = init_model_routing_metrics() {
346            warn!("Failed to initialize model routing metrics: {}", e);
347        }
348
349        // Initialize TLS metrics (best-effort, log warning if fails)
350        if let Err(e) = crate::tls_metrics::init_tls_metrics() {
351            warn!("Failed to initialize TLS metrics: {}", e);
352        }
353
354        Ok(Self {
355            config_manager,
356            route_matcher,
357            listener_matchers,
358            scoped_route_matcher,
359            upstream_pools,
360            scoped_upstream_pools,
361            agent_manager,
362            passive_health,
363            metrics,
364            scoped_metrics,
365            app_state,
366            reload_coordinator,
367            error_handlers,
368            validators,
369            static_servers,
370            builtin_state,
371            log_manager,
372            trace_id_format,
373            health_check_runner,
374            rate_limit_manager,
375            cache_manager,
376            geo_filter_manager,
377            inference_rate_limit_manager,
378            warmth_tracker,
379            guardrail_processor,
380            // ACME challenge manager - initialized later if ACME is configured
381            acme_challenges: None,
382            acme_clients: Vec::new(),
383        })
384    }
385
386    /// Shared HTTP cache statistics.
387    ///
388    /// Exposed so the standalone metrics server can include cache counters in
389    /// its Prometheus output, matching the builtin `/metrics` route handler.
390    pub fn http_cache_stats(&self) -> Arc<crate::cache::HttpCacheStats> {
391        self.cache_manager.stats()
392    }
393
394    /// Build per-listener route matchers for listeners that reference a
395    /// namespace route set.
396    ///
397    /// Listeners without a `namespace` reference are omitted (they fall back to
398    /// the global matcher at request time). Unknown namespace references are
399    /// skipped with a warning — config validation rejects them before startup,
400    /// so this only guards against a reload racing a bad config.
401    fn build_listener_matchers(
402        config: &zentinel_config::Config,
403    ) -> HashMap<String, Arc<RouteMatcher>> {
404        let mut matchers = HashMap::new();
405        for listener in &config.listeners {
406            let Some(ns_id) = listener.namespace.as_ref() else {
407                continue;
408            };
409            let Some(ns) = config.namespaces.iter().find(|n| &n.id == ns_id) else {
410                warn!(
411                    listener_id = %listener.id,
412                    namespace = %ns_id,
413                    "Listener references unknown namespace; no routes will match on this listener"
414                );
415                continue;
416            };
417            match RouteMatcher::with_cache_size(
418                ns.routes.clone(),
419                None,
420                config.server.route_cache_size,
421            ) {
422                Ok(matcher) => {
423                    info!(
424                        listener_id = %listener.id,
425                        address = %listener.address,
426                        namespace = %ns_id,
427                        routes = ns.routes.len(),
428                        "Listener bound to namespace route set"
429                    );
430                    matchers.insert(listener.address.clone(), Arc::new(matcher));
431                }
432                Err(e) => {
433                    error!(
434                        listener_id = %listener.id,
435                        namespace = %ns_id,
436                        error = %e,
437                        "Failed to compile route matcher for listener namespace"
438                    );
439                }
440            }
441        }
442        matchers
443    }
444
445    /// Setup the configuration reload handler
446    async fn setup_reload_handler(
447        config_manager: Arc<ConfigManager>,
448        route_matcher: Arc<RwLock<RouteMatcher>>,
449        listener_matchers: Arc<RwLock<HashMap<String, Arc<RouteMatcher>>>>,
450        upstream_pools: Registry<UpstreamPool>,
451        scoped_route_matcher: Arc<tokio::sync::RwLock<ScopedRouteMatcher>>,
452        scoped_upstream_pools: ScopedRegistry<UpstreamPool>,
453    ) {
454        let mut reload_rx = config_manager.subscribe();
455        let config_manager_clone = config_manager.clone();
456
457        tokio::spawn(async move {
458            loop {
459                match reload_rx.recv().await {
460                    Ok(ReloadEvent::Applied { .. }) => {}
461                    Ok(_) => continue,
462                    Err(broadcast::error::RecvError::Lagged(n)) => {
463                        warn!("Reload handler lagged by {n} events, applying latest config");
464                        // Fall through to reload with the latest config
465                    }
466                    Err(broadcast::error::RecvError::Closed) => break,
467                };
468                {
469                    // Reload routes and upstreams
470                    let new_config = config_manager_clone.current();
471                    let flattened = new_config.flatten();
472
473                    // Update route matcher FIRST (most critical for traffic)
474                    match RouteMatcher::new(new_config.routes.clone(), None) {
475                        Ok(new_matcher) => {
476                            *route_matcher.write() = new_matcher;
477                            info!(
478                                routes = new_config.routes.len(),
479                                "Global routes reloaded successfully"
480                            );
481                        }
482                        Err(e) => {
483                            error!(error = %e, "Failed to compile route matcher");
484                        }
485                    }
486
487                    // Rebuild per-listener (namespace-bound) route matchers
488                    *listener_matchers.write() = Self::build_listener_matchers(&new_config);
489
490                    // Update scoped route matcher
491                    if let Err(e) = scoped_route_matcher
492                        .write()
493                        .await
494                        .load_from_flattened(&flattened)
495                        .await
496                    {
497                        error!("Failed to reload scoped routes: {}", e);
498                    }
499
500                    // Update upstream pools with timeout to avoid blocking
501                    // the reload handler on DNS resolution / connection attempts
502                    let pool_update = async {
503                        let mut new_pools = HashMap::new();
504                        for (upstream_id, upstream_config) in &new_config.upstreams {
505                            let mut config_with_id = upstream_config.clone();
506                            config_with_id.id = upstream_id.clone();
507                            match UpstreamPool::new(config_with_id).await {
508                                Ok(pool) => {
509                                    new_pools.insert(upstream_id.clone(), Arc::new(pool));
510                                }
511                                Err(e) => {
512                                    error!("Failed to create upstream pool {}: {}", upstream_id, e);
513                                }
514                            }
515                        }
516                        new_pools
517                    };
518
519                    match tokio::time::timeout(Duration::from_secs(10), pool_update).await {
520                        Ok(new_pools) => {
521                            let old_pools = upstream_pools.replace(new_pools).await;
522
523                            // Update scoped upstream pools
524                            let new_scoped_pools = Self::build_scoped_pools_list(&flattened).await;
525                            let old_scoped_pools =
526                                scoped_upstream_pools.replace_all(new_scoped_pools).await;
527
528                            // Track drain lifecycle for old pools
529                            tokio::spawn(async move {
530                                let tracker = crate::upstream::drain::DrainTracker::default();
531                                tracker.track_pools(old_pools).await;
532                                tracker.track_pools(old_scoped_pools).await;
533                            });
534                        }
535                        Err(_) => {
536                            warn!("Upstream pool update timed out after 10s, routes still updated");
537                        }
538                    }
539                }
540            }
541        });
542    }
543
544    /// Create scoped upstream pools from flattened config
545    async fn create_scoped_upstream_pools(
546        flattened: &FlattenedConfig,
547        health_check_runner: &mut HealthCheckRunner,
548    ) -> Result<ScopedRegistry<UpstreamPool>> {
549        let registry = ScopedRegistry::new();
550
551        for (qid, upstream_config) in &flattened.upstreams {
552            let mut config_with_id = upstream_config.clone();
553            config_with_id.id = qid.canonical();
554
555            let pool = Arc::new(
556                UpstreamPool::new(config_with_id.clone())
557                    .await
558                    .with_context(|| {
559                        format!("Failed to create upstream pool '{}'", qid.canonical())
560                    })?,
561            );
562
563            // Track exports
564            let is_exported = flattened
565                .exported_upstreams
566                .contains_key(&upstream_config.id);
567
568            if is_exported {
569                registry.insert_exported(qid.clone(), pool).await;
570            } else {
571                registry.insert(qid.clone(), pool).await;
572            }
573
574            // Create active health checker if configured
575            if let Some(checker) = ActiveHealthChecker::new(&config_with_id) {
576                health_check_runner.add_checker(checker);
577            }
578
579            debug!(
580                upstream_id = %qid.canonical(),
581                scope = ?qid.scope,
582                exported = is_exported,
583                "Created scoped upstream pool"
584            );
585        }
586
587        info!("Created {} scoped upstream pools", registry.len().await);
588
589        Ok(registry)
590    }
591
592    /// Build list of scoped pools for atomic replacement
593    async fn build_scoped_pools_list(
594        flattened: &FlattenedConfig,
595    ) -> Vec<(QualifiedId, Arc<UpstreamPool>, bool)> {
596        let mut result = Vec::new();
597
598        for (qid, upstream_config) in &flattened.upstreams {
599            let mut config_with_id = upstream_config.clone();
600            config_with_id.id = qid.canonical();
601
602            match UpstreamPool::new(config_with_id).await {
603                Ok(pool) => {
604                    let is_exported = flattened
605                        .exported_upstreams
606                        .contains_key(&upstream_config.id);
607                    result.push((qid.clone(), Arc::new(pool), is_exported));
608                }
609                Err(e) => {
610                    error!(
611                        "Failed to create scoped upstream pool {}: {}",
612                        qid.canonical(),
613                        e
614                    );
615                }
616            }
617        }
618
619        result
620    }
621
622    /// Initialize route-specific components (error handlers, validators, static servers)
623    async fn initialize_route_components(
624        config: &Config,
625    ) -> Result<(
626        Registry<ErrorHandler>,
627        Registry<SchemaValidator>,
628        Registry<StaticFileServer>,
629    )> {
630        let mut error_handlers_map = HashMap::new();
631        let mut validators_map = HashMap::new();
632        let mut static_servers_map = HashMap::new();
633
634        for route in &config.routes {
635            info!(
636                "Initializing components for route: {} with service type: {:?}",
637                route.id, route.service_type
638            );
639
640            // Initialize error handler for each route
641            if let Some(ref error_config) = route.error_pages {
642                let handler =
643                    ErrorHandler::new(route.service_type.clone(), Some(error_config.clone()));
644                error_handlers_map.insert(route.id.clone(), Arc::new(handler));
645                debug!("Initialized error handler for route: {}", route.id);
646            } else {
647                // Use default error handler for the service type
648                let handler = ErrorHandler::new(route.service_type.clone(), None);
649                error_handlers_map.insert(route.id.clone(), Arc::new(handler));
650            }
651
652            // Initialize schema validator for API routes
653            if route.service_type == zentinel_config::ServiceType::Api {
654                if let Some(ref api_schema) = route.api_schema {
655                    match SchemaValidator::new(api_schema.clone()) {
656                        Ok(validator) => {
657                            validators_map.insert(route.id.clone(), Arc::new(validator));
658                            info!("Initialized schema validator for route: {}", route.id);
659                        }
660                        Err(e) => {
661                            warn!(
662                                "Failed to initialize schema validator for route {}: {}",
663                                route.id, e
664                            );
665                        }
666                    }
667                }
668            }
669
670            // Initialize static file server for static routes
671            if route.service_type == zentinel_config::ServiceType::Static {
672                if let Some(ref static_config) = route.static_files {
673                    let server = StaticFileServer::new(static_config.clone());
674                    static_servers_map.insert(route.id.clone(), Arc::new(server));
675                    info!("Initialized static file server for route: {}", route.id);
676                } else {
677                    warn!(
678                        "Static route {} has no static_files configuration",
679                        route.id
680                    );
681                }
682            }
683        }
684
685        Ok((
686            Registry::from_map(error_handlers_map),
687            Registry::from_map(validators_map),
688            Registry::from_map(static_servers_map),
689        ))
690    }
691
692    /// Get or generate trace ID from session
693    pub(super) fn get_trace_id(&self, session: &pingora::proxy::Session) -> String {
694        http_helpers::get_or_create_trace_id(session, self.trace_id_format)
695    }
696
697    /// Initialize rate limiters from configuration
698    fn initialize_rate_limiters(config: &Config) -> RateLimitManager {
699        use zentinel_config::RateLimitAction;
700
701        // Create manager with global rate limit if configured
702        let manager = if let Some(ref global) = config.rate_limits.global {
703            info!(
704                max_rps = global.max_rps,
705                burst = global.burst,
706                key = ?global.key,
707                "Initializing global rate limiter"
708            );
709            RateLimitManager::with_global_limit(global.max_rps, global.burst)
710        } else {
711            RateLimitManager::new()
712        };
713
714        for route in &config.routes {
715            // Check for rate limit in route policies
716            if let Some(ref rate_limit) = route.policies.rate_limit {
717                let rl_config = RateLimitConfig {
718                    max_rps: rate_limit.requests_per_second,
719                    burst: rate_limit.burst,
720                    key: rate_limit.key.clone(),
721                    action: RateLimitAction::Reject,
722                    status_code: 429,
723                    message: None,
724                    backend: zentinel_config::RateLimitBackend::Local,
725                    max_delay_ms: 5000, // Default for policy-based rate limits
726                    max_keys: crate::rate_limit::DEFAULT_MAX_RATE_LIMIT_KEYS,
727                };
728                manager.register_route(&route.id, rl_config);
729                info!(
730                    route_id = %route.id,
731                    max_rps = rate_limit.requests_per_second,
732                    burst = rate_limit.burst,
733                    key = ?rate_limit.key,
734                    "Registered rate limiter for route"
735                );
736            }
737
738            // Also check for rate limit filters in the filter chain
739            for filter_id in &route.filters {
740                if let Some(filter_config) = config.filters.get(filter_id) {
741                    if let zentinel_config::Filter::RateLimit(ref rl_filter) = filter_config.filter
742                    {
743                        let rl_config = RateLimitConfig {
744                            max_rps: rl_filter.max_rps,
745                            burst: rl_filter.burst,
746                            key: rl_filter.key.clone(),
747                            action: rl_filter.on_limit.clone(),
748                            status_code: rl_filter.status_code,
749                            message: rl_filter.limit_message.clone(),
750                            backend: rl_filter.backend.clone(),
751                            max_delay_ms: rl_filter.max_delay_ms,
752                            max_keys: rl_filter.max_keys,
753                        };
754                        manager.register_route(&route.id, rl_config);
755                        info!(
756                            route_id = %route.id,
757                            filter_id = %filter_id,
758                            max_rps = rl_filter.max_rps,
759                            backend = ?rl_filter.backend,
760                            "Registered rate limiter from filter for route"
761                        );
762                    }
763                }
764            }
765        }
766
767        if manager.route_count() > 0 {
768            info!(
769                route_count = manager.route_count(),
770                "Rate limiting initialized"
771            );
772        }
773
774        manager
775    }
776
777    /// Initialize inference rate limiters from configuration
778    ///
779    /// This creates token-based rate limiters for routes with `service-type "inference"`
780    /// and inference config blocks.
781    fn initialize_inference_rate_limiters(config: &Config) -> InferenceRateLimitManager {
782        let manager = InferenceRateLimitManager::new();
783
784        for route in &config.routes {
785            // Only initialize for inference service type routes with inference config
786            if route.service_type == zentinel_config::ServiceType::Inference {
787                if let Some(ref inference_config) = route.inference {
788                    manager.register_route(&route.id, inference_config);
789                }
790            }
791        }
792
793        if manager.route_count() > 0 {
794            info!(
795                route_count = manager.route_count(),
796                "Inference rate limiting initialized"
797            );
798        }
799
800        manager
801    }
802
803    /// Initialize cache manager from configuration
804    fn initialize_cache_manager(config: &Config) -> CacheManager {
805        let manager = CacheManager::new();
806
807        let mut enabled_count = 0;
808
809        for route in &config.routes {
810            // Use per-route cache config if present, otherwise fall back to service-type defaults
811            let cache_config = if let Some(ref rc) = route.policies.cache {
812                // Pre-compile exclude_paths glob patterns into regex at registration time
813                let exclude_paths = rc
814                    .exclude_paths
815                    .iter()
816                    .filter_map(|pattern| {
817                        let regex_str = crate::cache::compile_glob_to_regex(pattern);
818                        match regex::Regex::new(&regex_str) {
819                            Ok(re) => Some(re),
820                            Err(e) => {
821                                warn!(
822                                    route_id = %route.id,
823                                    pattern = %pattern,
824                                    error = %e,
825                                    "Failed to compile cache exclude-path pattern"
826                                );
827                                None
828                            }
829                        }
830                    })
831                    .collect();
832
833                CacheConfig {
834                    enabled: rc.enabled,
835                    default_ttl_secs: rc.default_ttl_secs,
836                    max_size_bytes: rc.max_size_bytes,
837                    cache_private: rc.cache_private,
838                    stale_while_revalidate_secs: rc.stale_while_revalidate_secs,
839                    stale_if_error_secs: rc.stale_if_error_secs,
840                    cacheable_methods: rc.cacheable_methods.clone(),
841                    cacheable_status_codes: rc.cacheable_status_codes.clone(),
842                    exclude_extensions: rc.exclude_extensions.clone(),
843                    exclude_paths,
844                }
845            } else {
846                match route.service_type {
847                    zentinel_config::ServiceType::Static => CacheConfig {
848                        enabled: true,
849                        default_ttl_secs: 3600,
850                        max_size_bytes: 50 * 1024 * 1024, // 50MB for static
851                        stale_while_revalidate_secs: 60,
852                        stale_if_error_secs: 300,
853                        ..Default::default()
854                    },
855                    zentinel_config::ServiceType::Api => CacheConfig {
856                        enabled: false,
857                        default_ttl_secs: 60,
858                        ..Default::default()
859                    },
860                    zentinel_config::ServiceType::Web => CacheConfig {
861                        enabled: false,
862                        default_ttl_secs: 300,
863                        ..Default::default()
864                    },
865                    _ => CacheConfig::default(),
866                }
867            };
868
869            if cache_config.enabled {
870                enabled_count += 1;
871                info!(
872                    route_id = %route.id,
873                    default_ttl_secs = cache_config.default_ttl_secs,
874                    from_config = route.policies.cache.is_some(),
875                    "HTTP caching enabled for route"
876                );
877            }
878            manager.register_route(&route.id, cache_config);
879        }
880
881        if enabled_count > 0 {
882            info!(enabled_routes = enabled_count, "HTTP caching initialized");
883        } else {
884            debug!("HTTP cache manager initialized (no routes with caching enabled)");
885        }
886
887        manager
888    }
889
890    /// Initialize geo filters from configuration
891    fn initialize_geo_filters(config: &Config) -> GeoFilterManager {
892        let manager = GeoFilterManager::new();
893
894        for (filter_id, filter_config) in &config.filters {
895            if let zentinel_config::Filter::Geo(ref geo_filter) = filter_config.filter {
896                match manager.register_filter(filter_id, geo_filter.clone()) {
897                    Ok(_) => {
898                        info!(
899                            filter_id = %filter_id,
900                            database_path = %geo_filter.database_path,
901                            action = ?geo_filter.action,
902                            countries_count = geo_filter.countries.len(),
903                            "Registered geo filter"
904                        );
905                    }
906                    Err(e) => {
907                        error!(
908                            filter_id = %filter_id,
909                            error = %e,
910                            "Failed to register geo filter"
911                        );
912                    }
913                }
914            }
915        }
916
917        let filter_ids = manager.filter_ids();
918        if !filter_ids.is_empty() {
919            info!(
920                filter_count = filter_ids.len(),
921                filter_ids = ?filter_ids,
922                "GeoIP filtering initialized"
923            );
924        }
925
926        manager
927    }
928
929    /// Spawn background task to periodically clean up idle rate limiters and expired geo caches
930    fn spawn_cleanup_task(
931        rate_limit_manager: Arc<RateLimitManager>,
932        geo_filter_manager: Arc<GeoFilterManager>,
933    ) {
934        // Cleanup interval: 5 minutes
935        const CLEANUP_INTERVAL: Duration = Duration::from_secs(300);
936
937        tokio::spawn(async move {
938            let mut interval = tokio::time::interval(CLEANUP_INTERVAL);
939            // First tick completes immediately; skip it
940            interval.tick().await;
941
942            loop {
943                interval.tick().await;
944
945                // Clean up rate limiters (removes entries when pool exceeds max size)
946                rate_limit_manager.cleanup();
947
948                // Clean up expired geo filter caches
949                geo_filter_manager.clear_expired_caches();
950
951                debug!("Periodic cleanup completed");
952            }
953        });
954
955        info!(
956            interval_secs = CLEANUP_INTERVAL.as_secs(),
957            "Started periodic cleanup task"
958        );
959    }
960
961    /// Spawn background task to watch geo database files for changes
962    fn spawn_geo_database_watcher(geo_filter_manager: Arc<GeoFilterManager>) {
963        let watcher = Arc::new(GeoDatabaseWatcher::new(geo_filter_manager));
964
965        // Try to start watching
966        match watcher.start_watching() {
967            Ok(mut rx) => {
968                let watcher_clone = watcher.clone();
969                tokio::spawn(async move {
970                    // Debounce interval
971                    const DEBOUNCE_MS: u64 = 500;
972
973                    while let Some(path) = rx.recv().await {
974                        // Debounce rapid changes (e.g., temp file then rename)
975                        tokio::time::sleep(Duration::from_millis(DEBOUNCE_MS)).await;
976
977                        // Drain any additional events for the same path during debounce
978                        while rx.try_recv().is_ok() {}
979
980                        // Handle the change
981                        watcher_clone.handle_change(&path);
982                    }
983                });
984
985                info!("Started geo database file watcher");
986            }
987            Err(e) => {
988                warn!(
989                    error = %e,
990                    "Failed to start geo database file watcher, auto-reload disabled"
991                );
992            }
993        }
994    }
995}
996
997#[cfg(test)]
998mod listener_matcher_tests {
999    use super::*;
1000    use crate::routing::RequestInfo;
1001
1002    const KDL: &str = r#"
1003        schema-version "1.0"
1004        system { worker-threads 0 }
1005        listeners {
1006            listener "public" { address "0.0.0.0:8080" }
1007            listener "admin" {
1008                address "127.0.0.1:9000"
1009                namespace "ops"
1010            }
1011        }
1012        routes {
1013            route "api" {
1014                matches { path-prefix "/api" }
1015                upstream "backend"
1016            }
1017        }
1018        upstreams {
1019            upstream "backend" { target "127.0.0.1:3000" }
1020        }
1021        namespace "ops" {
1022            routes {
1023                route "metrics" {
1024                    matches { path "/metrics" }
1025                    service-type "builtin"
1026                    builtin-handler "metrics"
1027                }
1028            }
1029        }
1030    "#;
1031
1032    #[test]
1033    fn namespace_listener_matches_only_its_own_routes() {
1034        let config = zentinel_config::Config::from_kdl(KDL).expect("config parses");
1035        let matchers = ZentinelProxy::build_listener_matchers(&config);
1036
1037        // Only the namespace-bound listener gets a dedicated matcher.
1038        assert_eq!(matchers.len(), 1);
1039        assert!(!matchers.contains_key("0.0.0.0:8080"));
1040        let admin = matchers
1041            .get("127.0.0.1:9000")
1042            .expect("admin listener bound to namespace");
1043
1044        // Isolated: the admin listener serves the namespace route...
1045        assert!(admin
1046            .match_request(&RequestInfo::new("GET", "/metrics", "x"))
1047            .is_some());
1048        // ...but NOT the global route.
1049        assert!(admin
1050            .match_request(&RequestInfo::new("GET", "/api/users", "x"))
1051            .is_none());
1052    }
1053
1054    #[test]
1055    fn no_namespace_listeners_yields_empty_map() {
1056        let kdl = r#"
1057            schema-version "1.0"
1058            system { worker-threads 0 }
1059            listeners {
1060                listener "public" {
1061                    address "0.0.0.0:8080"
1062                }
1063            }
1064            routes {
1065                route "api" {
1066                    matches { path-prefix "/api" }
1067                    upstream "backend"
1068                }
1069            }
1070            upstreams {
1071                upstream "backend" {
1072                    target "127.0.0.1:3000"
1073                }
1074            }
1075        "#;
1076        let config = zentinel_config::Config::from_kdl(kdl).expect("config parses");
1077        let matchers = ZentinelProxy::build_listener_matchers(&config);
1078        assert!(matchers.is_empty());
1079    }
1080}