use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
fn default_listen_addr() -> String {
"127.0.0.1:8080".to_string()
}
fn default_event_bus_capacity() -> usize {
1024
}
fn default_metrics_window_secs() -> u64 {
300 }
fn default_top_n() -> usize {
10
}
fn default_sse_keepalive_secs() -> u64 {
30
}
fn default_max_sse_connections() -> usize {
100
}
fn default_max_ws_connections() -> usize {
50
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_listen_addr")]
pub listen: String,
#[serde(default)]
pub static_dir: Option<String>,
#[serde(default = "default_event_bus_capacity")]
pub event_bus_capacity: usize,
#[serde(default)]
pub metrics: MetricsConfig,
#[serde(default)]
pub sse: SseConfig,
#[serde(default)]
pub websocket: WebSocketConfig,
#[serde(default)]
pub alerts: AlertConfig,
#[serde(default)]
pub cors: CorsConfig,
}
impl Default for WebConfig {
fn default() -> Self {
Self {
enabled: false,
listen: default_listen_addr(),
static_dir: None,
event_bus_capacity: default_event_bus_capacity(),
metrics: MetricsConfig::default(),
sse: SseConfig::default(),
websocket: WebSocketConfig::default(),
alerts: AlertConfig::default(),
cors: CorsConfig::default(),
}
}
}
impl WebConfig {
pub fn socket_addr(&self) -> Result<SocketAddr, std::net::AddrParseError> {
self.listen.parse()
}
pub fn validate(&self) -> Result<(), String> {
self.socket_addr()
.map_err(|e| format!("Invalid listen address '{}': {}", self.listen, e))?;
if self.event_bus_capacity == 0 {
return Err("event_bus_capacity must be greater than 0".to_string());
}
self.metrics.validate()?;
self.sse.validate()?;
self.websocket.validate()?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
#[serde(default = "default_metrics_window_secs")]
pub window_secs: u64,
#[serde(default = "default_top_n")]
pub top_n: usize,
#[serde(default = "default_true")]
pub track_clients: bool,
#[serde(default = "default_true")]
pub track_domains: bool,
#[serde(default = "default_true")]
pub track_latency: bool,
}
fn default_true() -> bool {
true
}
impl Default for MetricsConfig {
fn default() -> Self {
Self {
window_secs: default_metrics_window_secs(),
top_n: default_top_n(),
track_clients: true,
track_domains: true,
track_latency: true,
}
}
}
impl MetricsConfig {
fn validate(&self) -> Result<(), String> {
if self.window_secs == 0 {
return Err("metrics.window_secs must be greater than 0".to_string());
}
if self.top_n == 0 {
return Err("metrics.top_n must be greater than 0".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SseConfig {
#[serde(default = "default_max_sse_connections")]
pub max_connections: usize,
#[serde(default = "default_sse_keepalive_secs")]
pub keepalive_secs: u64,
#[serde(default = "default_sse_buffer")]
pub buffer_size: usize,
}
fn default_sse_buffer() -> usize {
100
}
impl Default for SseConfig {
fn default() -> Self {
Self {
max_connections: default_max_sse_connections(),
keepalive_secs: default_sse_keepalive_secs(),
buffer_size: default_sse_buffer(),
}
}
}
impl SseConfig {
fn validate(&self) -> Result<(), String> {
if self.max_connections == 0 {
return Err("sse.max_connections must be greater than 0".to_string());
}
if self.keepalive_secs == 0 {
return Err("sse.keepalive_secs must be greater than 0".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebSocketConfig {
#[serde(default = "default_max_ws_connections")]
pub max_connections: usize,
#[serde(default = "default_ws_heartbeat")]
pub heartbeat_secs: u64,
#[serde(default = "default_ws_timeout")]
pub timeout_secs: u64,
#[serde(default = "default_ws_max_message_size")]
pub max_message_size: usize,
}
fn default_ws_heartbeat() -> u64 {
30
}
fn default_ws_timeout() -> u64 {
60
}
fn default_ws_max_message_size() -> usize {
64 * 1024 }
impl Default for WebSocketConfig {
fn default() -> Self {
Self {
max_connections: default_max_ws_connections(),
heartbeat_secs: default_ws_heartbeat(),
timeout_secs: default_ws_timeout(),
max_message_size: default_ws_max_message_size(),
}
}
}
impl WebSocketConfig {
fn validate(&self) -> Result<(), String> {
if self.max_connections == 0 {
return Err("websocket.max_connections must be greater than 0".to_string());
}
if self.heartbeat_secs == 0 {
return Err("websocket.heartbeat_secs must be greater than 0".to_string());
}
if self.timeout_secs <= self.heartbeat_secs {
return Err("websocket.timeout_secs must be greater than heartbeat_secs".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub rules: Vec<AlertRule>,
#[serde(default = "default_dedup_window")]
pub dedup_window_secs: u64,
#[serde(default = "default_max_alerts")]
pub max_alerts: usize,
#[serde(default)]
pub webhook: Option<WebhookConfig>,
}
fn default_dedup_window() -> u64 {
300 }
fn default_max_alerts() -> usize {
1000
}
impl Default for AlertConfig {
fn default() -> Self {
Self {
enabled: false,
rules: Vec::new(),
dedup_window_secs: default_dedup_window(),
max_alerts: default_max_alerts(),
webhook: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlertRule {
pub name: String,
pub condition: AlertCondition,
#[serde(default)]
pub severity: AlertSeverity,
#[serde(default)]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AlertCondition {
SecurityEvent { event_type: String },
RateThreshold {
metric: String,
threshold: f64,
window_secs: u64,
},
UpstreamHealth { status: String },
ErrorRate { threshold: f64, window_secs: u64 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum AlertSeverity {
Info,
#[default]
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookConfig {
pub url: String,
#[serde(default)]
pub auth_header: Option<String>,
#[serde(default = "default_webhook_timeout")]
pub timeout_secs: u64,
#[serde(default = "default_webhook_retries")]
pub retries: u32,
}
fn default_webhook_timeout() -> u64 {
10
}
fn default_webhook_retries() -> u32 {
3
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CorsConfig {
#[serde(default)]
pub allowed_origins: Vec<String>,
#[serde(default)]
pub allow_credentials: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = WebConfig::default();
assert!(!config.enabled);
assert_eq!(config.listen, "127.0.0.1:8080");
assert!(config.validate().is_ok());
}
#[test]
fn test_parse_socket_addr() {
let config = WebConfig::default();
let addr = config.socket_addr().unwrap();
assert_eq!(addr.port(), 8080);
}
#[test]
fn test_invalid_listen_addr() {
let config = WebConfig {
listen: "invalid".to_string(),
..Default::default()
};
assert!(config.validate().is_err());
}
#[test]
fn test_invalid_metrics_config() {
let mut config = WebConfig::default();
config.metrics.top_n = 0;
assert!(config.validate().is_err());
}
#[test]
fn test_websocket_timeout_validation() {
let mut config = WebConfig::default();
config.websocket.timeout_secs = 10;
config.websocket.heartbeat_secs = 20;
assert!(config.validate().is_err());
}
#[test]
fn test_alert_rule_deserialization() {
let yaml = r#"
name: high_rate_limit
condition:
type: security_event
event_type: rate_limit_exceeded
severity: warning
"#;
let rule: AlertRule = serde_yaml::from_str(yaml).unwrap();
assert_eq!(rule.name, "high_rate_limit");
assert_eq!(rule.severity, AlertSeverity::Warning);
}
}