1mod 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
64pub struct ZentinelProxy {
66 pub config_manager: Arc<ConfigManager>,
68 pub(super) route_matcher: Arc<RwLock<RouteMatcher>>,
70 pub(super) listener_matchers: Arc<RwLock<HashMap<String, Arc<RouteMatcher>>>>,
75 pub(super) scoped_route_matcher: Arc<tokio::sync::RwLock<ScopedRouteMatcher>>,
77 pub(super) upstream_pools: Registry<UpstreamPool>,
79 pub(super) scoped_upstream_pools: ScopedRegistry<UpstreamPool>,
81 pub(super) agent_manager: Arc<AgentManager>,
83 pub(super) passive_health: Arc<PassiveHealthChecker>,
85 pub(super) metrics: Arc<zentinel_common::observability::RequestMetrics>,
87 pub(super) scoped_metrics: Arc<ScopedMetrics>,
89 pub(super) app_state: Arc<AppState>,
91 pub(super) reload_coordinator: Arc<GracefulReloadCoordinator>,
93 pub(super) error_handlers: Registry<ErrorHandler>,
95 pub(super) validators: Registry<SchemaValidator>,
97 pub(super) static_servers: Registry<StaticFileServer>,
99 pub(super) builtin_state: Arc<BuiltinHandlerState>,
101 pub(super) log_manager: SharedLogManager,
103 pub(super) trace_id_format: TraceIdFormat,
105 pub(super) health_check_runner: Arc<HealthCheckRunner>,
107 pub(super) rate_limit_manager: Arc<RateLimitManager>,
109 pub(super) cache_manager: Arc<CacheManager>,
111 pub(super) geo_filter_manager: Arc<GeoFilterManager>,
113 pub(super) inference_rate_limit_manager: Arc<InferenceRateLimitManager>,
115 pub(super) warmth_tracker: Arc<crate::health::WarmthTracker>,
117 pub(super) guardrail_processor: Arc<crate::inference::GuardrailProcessor>,
119 pub acme_challenges: Option<Arc<crate::acme::ChallengeManager>>,
122 pub acme_clients: Vec<Arc<crate::acme::AcmeClient>>,
125}
126
127impl ZentinelProxy {
128 pub async fn new(config_path: Option<&str>) -> Result<Self> {
133 info!("Starting Zentinel Proxy");
134
135 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 (cfg, "_embedded_".to_string())
146 }
147 };
148
149 config
150 .validate()
151 .context("Initial configuration validation failed")?;
152
153 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 let config_manager =
166 Arc::new(ConfigManager::new(&effective_config_path, config.clone()).await?);
167
168 config_manager.add_validator(Box::new(RouteValidator)).await;
170 config_manager
171 .add_validator(Box::new(UpstreamValidator))
172 .await;
173
174 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 let listener_matchers = Arc::new(RwLock::new(Self::build_listener_matchers(&config)));
184
185 let flattened = config.flatten();
187
188 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 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 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 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 let passive_health = Arc::new(PassiveHealthChecker::new(
220 0.5, 100, None, ));
224
225 let agent_manager = Arc::new(AgentManager::new(config.agents.clone()).await?);
227 agent_manager.initialize().await?;
228
229 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 let app_state = Arc::new(AppState::new(Uuid::new_v4().to_string()));
236
237 let reload_coordinator = Arc::new(GracefulReloadCoordinator::new(
239 Duration::from_secs(30), ));
241
242 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 let (error_handlers, validators, static_servers) =
255 Self::initialize_route_components(&config).await?;
256
257 let builtin_state = Arc::new(BuiltinHandlerState::new(
259 env!("CARGO_PKG_VERSION").to_string(),
260 app_state.instance_id.clone(),
261 ));
262
263 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 {
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 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 let rate_limit_manager = Arc::new(Self::initialize_rate_limiters(&config));
308
309 let inference_rate_limit_manager =
311 Arc::new(Self::initialize_inference_rate_limiters(&config));
312
313 let warmth_tracker = Arc::new(crate::health::WarmthTracker::with_defaults());
315
316 let guardrail_processor = Arc::new(crate::inference::GuardrailProcessor::new(
318 agent_manager.clone(),
319 ));
320
321 let geo_filter_manager = Arc::new(Self::initialize_geo_filters(&config));
323
324 Self::spawn_cleanup_task(rate_limit_manager.clone(), geo_filter_manager.clone());
326
327 Self::spawn_geo_database_watcher(geo_filter_manager.clone());
329
330 app_state.set_ready(true);
332
333 let trace_id_format = config.server.trace_id_format;
335
336 let cache_manager = Arc::new(Self::initialize_cache_manager(&config));
338
339 if let Err(e) = init_fallback_metrics() {
341 warn!("Failed to initialize fallback metrics: {}", e);
342 }
343
344 if let Err(e) = init_model_routing_metrics() {
346 warn!("Failed to initialize model routing metrics: {}", e);
347 }
348
349 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_challenges: None,
382 acme_clients: Vec::new(),
383 })
384 }
385
386 pub fn http_cache_stats(&self) -> Arc<crate::cache::HttpCacheStats> {
391 self.cache_manager.stats()
392 }
393
394 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 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 }
466 Err(broadcast::error::RecvError::Closed) => break,
467 };
468 {
469 let new_config = config_manager_clone.current();
471 let flattened = new_config.flatten();
472
473 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 *listener_matchers.write() = Self::build_listener_matchers(&new_config);
489
490 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 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 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 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 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 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 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 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 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 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 let handler = ErrorHandler::new(route.service_type.clone(), None);
649 error_handlers_map.insert(route.id.clone(), Arc::new(handler));
650 }
651
652 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 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 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 fn initialize_rate_limiters(config: &Config) -> RateLimitManager {
699 use zentinel_config::RateLimitAction;
700
701 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 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, 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 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 fn initialize_inference_rate_limiters(config: &Config) -> InferenceRateLimitManager {
782 let manager = InferenceRateLimitManager::new();
783
784 for route in &config.routes {
785 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 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 let cache_config = if let Some(ref rc) = route.policies.cache {
812 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(®ex_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, 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 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 fn spawn_cleanup_task(
931 rate_limit_manager: Arc<RateLimitManager>,
932 geo_filter_manager: Arc<GeoFilterManager>,
933 ) {
934 const CLEANUP_INTERVAL: Duration = Duration::from_secs(300);
936
937 tokio::spawn(async move {
938 let mut interval = tokio::time::interval(CLEANUP_INTERVAL);
939 interval.tick().await;
941
942 loop {
943 interval.tick().await;
944
945 rate_limit_manager.cleanup();
947
948 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 fn spawn_geo_database_watcher(geo_filter_manager: Arc<GeoFilterManager>) {
963 let watcher = Arc::new(GeoDatabaseWatcher::new(geo_filter_manager));
964
965 match watcher.start_watching() {
967 Ok(mut rx) => {
968 let watcher_clone = watcher.clone();
969 tokio::spawn(async move {
970 const DEBOUNCE_MS: u64 = 500;
972
973 while let Some(path) = rx.recv().await {
974 tokio::time::sleep(Duration::from_millis(DEBOUNCE_MS)).await;
976
977 while rx.try_recv().is_ok() {}
979
980 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 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 assert!(admin
1046 .match_request(&RequestInfo::new("GET", "/metrics", "x"))
1047 .is_some());
1048 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}