#[derive(Debug, Clone, Serialize)] pub struct LogEntry { pub event: &'static str, pub protocol_error: Option<&'static str>, pub reason: Option<&'static str>, pub error_detail: Option, pub error: Option, pub local_addr: Option, pub tls_sni: Option, pub sni: Option, pub authority: Option, pub matched_by: Option<&'static str>, pub vhost: Option, pub route: Option, pub cache_effective_enabled: Option, pub transform_effective_enabled: Option, pub backend: Option, pub id: u64, pub request_id: String, pub time_ms: u64, pub method: String, pub host: String, pub path: String, pub http_version: &'static str, pub transport: &'static str, pub websocket_bootstrap: Option<&'static str>, pub websocket_phase: Option<&'static str>, pub websocket_error: Option<&'static str>, pub h3_reset_code: Option<&'static str>, pub upstream_status: Option, pub upstream_error: Option, pub client_ip: Option, pub user_agent: Option, pub country: Option, pub status: u16, pub duration_ms: u64, pub request_bytes: u64, pub response_bytes: u64, pub outcome: &'static str, pub cache: &'static str, pub cache_reason: &'static str, } #[derive(Debug, Clone, Serialize)] pub struct SinkStats { pub queue_capacity: usize, pub queue_dropped: u64, pub sent: u64, pub errors: u64, } pub struct Logs { entries: Mutex>, pub dropped: AtomicU64, settings: Arc>, live: broadcast::Sender, sink: mpsc::Sender, sink_dropped: Arc, sink_sent: Arc, sink_errors: Arc, } impl Default for Logs { fn default() -> Self { let (sink, receiver) = mpsc::channel(SINK_QUEUE); drop(receiver); let (live, _) = broadcast::channel(LIVE_STREAM_CAPACITY); Self { entries: Mutex::new(VecDeque::new()), dropped: AtomicU64::new(0), settings: Arc::new(ArcSwap::from_pointee(Settings::default())), live, sink, sink_dropped: Arc::new(AtomicU64::new(0)), sink_sent: Arc::new(AtomicU64::new(0)), sink_errors: Arc::new(AtomicU64::new(0)), } } } fn protocol_error_detail(detail: impl std::fmt::Display) -> String { let mut detail = redact_text(&detail.to_string()); if detail.len() > 512 { let mut end = 512; while !detail.is_char_boundary(end) { end -= 1; } detail.truncate(end); detail.push_str("..."); } detail } fn protocol_identity_field(value: Option) -> Option { value.map(|value| { let mut value: String = value.chars().filter(|ch| !ch.is_control()).collect(); if value.len() > 512 { let mut end = 512; while !value.is_char_boundary(end) { end -= 1; } value.truncate(end); value.push_str("..."); } value }) } impl Logs { pub fn start(settings: Settings, shutdown: CancellationToken) -> Arc { let (tx, rx) = mpsc::channel(SINK_QUEUE); let (live, _) = broadcast::channel(LIVE_STREAM_CAPACITY); let settings = Arc::new(ArcSwap::from_pointee(settings)); let sink_dropped = Arc::new(AtomicU64::new(0)); let sink_sent = Arc::new(AtomicU64::new(0)); let sink_errors = Arc::new(AtomicU64::new(0)); let logs = Arc::new(Self { entries: Mutex::new(VecDeque::new()), dropped: AtomicU64::new(0), settings: settings.clone(), live, sink: tx, sink_dropped: sink_dropped.clone(), sink_sent: sink_sent.clone(), sink_errors: sink_errors.clone(), }); tokio::spawn(sink_loop(rx, settings, sink_sent, sink_errors, shutdown)); logs } pub fn configure(&self, settings: Settings) { self.settings.store(Arc::new(settings)); } pub fn capture_user_agent(&self) -> bool { self.settings.load().capture_user_agent } pub fn capture_country(&self) -> bool { self.settings.load().capture_country } pub fn push(&self, entry: LogEntry) { let has_viewers = self.live.receiver_count() != 0; let has_sink = self.settings.load().enabled(); // Reserve the bounded sink slot BEFORE cloning a log entry. Under // saturation, the failed enqueue must not copy 30+ owned fields. let sink_permit = if has_sink { match self.sink.try_reserve() { Ok(permit) => Some(permit), Err(_) => { self.sink_dropped.fetch_add(1, Ordering::Relaxed); None } } } else { None }; let mut external = Some(entry); let history_entry = if has_viewers || sink_permit.is_some() { external.as_ref().expect("entry").clone() } else { external.take().expect("entry") }; let evicted = if let Ok(mut entries) = self.entries.try_lock() { let evicted = if entries.len() == HISTORY_CAPACITY { entries.pop_front() } else { None }; entries.push_back(history_entry); evicted } else { self.dropped.fetch_add(1, Ordering::Relaxed); None }; // Destroy evicted strings outside of the history lock. drop(evicted); if let Some(entry) = external { if has_viewers { let _ = self.live.send(entry.clone()); } if let Some(permit) = sink_permit { permit.send(entry); } } } fn push_sink(&self, entry: LogEntry) { if self.settings.load().enabled() && self.sink.try_send(entry).is_err() { self.sink_dropped.fetch_add(1, Ordering::Relaxed); } } /// Persist transport/protocol failures without polluting the bounded Live Logs buffer. pub fn push_protocol_error( &self, kind: &'static str, detail: impl std::fmt::Display, client: std::net::SocketAddr, local: std::net::SocketAddr, transport: &'static str, http_version: &'static str, status: u16, tls_sni: Option, ) { self.push_protocol_error_attributed( kind, detail, client, local, transport, http_version, status, tls_sni, None, None, "none", ); } #[allow(clippy::too_many_arguments)] pub fn push_protocol_error_attributed( &self, kind: &'static str, detail: impl std::fmt::Display, client: std::net::SocketAddr, local: std::net::SocketAddr, transport: &'static str, http_version: &'static str, status: u16, tls_sni: Option, authority: Option, vhost: Option, matched_by: &'static str, ) { if !self.settings.load().enabled() { return; } let detail = protocol_error_detail(detail); let tls_sni = protocol_identity_field(tls_sni); let authority = protocol_identity_field(authority); let sni = tls_sni.clone(); self.push_sink(LogEntry { event: "protocol_error", protocol_error: Some(kind), reason: Some(kind), error_detail: Some(detail.clone()), error: Some(detail), local_addr: Some(local.to_string()), tls_sni, sni, authority, matched_by: Some(matched_by), vhost, route: None, cache_effective_enabled: None, transform_effective_enabled: None, backend: None, id: 0, request_id: String::new(), time_ms: now_ms(), method: String::new(), host: String::new(), path: String::new(), http_version, transport, websocket_bootstrap: None, websocket_phase: None, websocket_error: None, h3_reset_code: None, upstream_status: None, upstream_error: None, client_ip: Some(client.ip().to_string()), user_agent: None, country: None, status, duration_ms: 0, request_bytes: 0, response_bytes: 0, outcome: "protocol_error", cache: "-", cache_reason: "protocol_error", }); } pub fn subscribe(&self) -> broadcast::Receiver { self.live.subscribe() } pub fn recent(&self) -> Vec { self.recent_limit(HISTORY_CAPACITY) } pub fn recent_limit(&self, limit: usize) -> Vec { self.entries .lock() .map(|entries| { entries .iter() .rev() .take(limit.min(HISTORY_CAPACITY)) .cloned() .collect() }) .unwrap_or_default() } pub fn stats(&self) -> SinkStats { SinkStats { queue_capacity: SINK_QUEUE, queue_dropped: self.sink_dropped.load(Ordering::Relaxed), sent: self.sink_sent.load(Ordering::Relaxed), errors: self.sink_errors.load(Ordering::Relaxed), } } } fn value_for(entry: &LogEntry, name: &str) -> Value { match name { "event" => json!(entry.event), "protocol_error" => entry .protocol_error .map(|v| Value::String(v.into())) .unwrap_or(Value::Null), "reason" => entry .reason .map(|v| Value::String(v.into())) .unwrap_or(Value::Null), "error_detail" => entry .error_detail .clone() .map(Value::String) .unwrap_or(Value::Null), "error" => entry .error .clone() .map(Value::String) .unwrap_or(Value::Null), "local_addr" => entry .local_addr .clone() .map(Value::String) .unwrap_or(Value::Null), "tls_sni" => entry .tls_sni .clone() .map(Value::String) .unwrap_or(Value::Null), "sni" => entry .sni .clone() .map(Value::String) .unwrap_or(Value::Null), "authority" => entry .authority .clone() .map(Value::String) .unwrap_or(Value::Null), "matched_by" => entry .matched_by .map(|v| Value::String(v.into())) .unwrap_or(Value::Null), "vhost" => entry .vhost .clone() .map(Value::String) .unwrap_or(Value::Null), "route" => entry .route .clone() .map(Value::String) .unwrap_or(Value::Null), "cache_effective_enabled" => entry .cache_effective_enabled .map(Value::Bool) .unwrap_or(Value::Null), "transform_effective_enabled" => entry .transform_effective_enabled .map(Value::Bool) .unwrap_or(Value::Null), "backend" => entry .backend .clone() .map(Value::String) .unwrap_or(Value::Null), "id" => json!(entry.id), "request_id" => json!(&entry.request_id), "time_ms" => json!(entry.time_ms), "method" => json!(&entry.method), "host" => json!(&entry.host), "path" => json!(&entry.path), "http_version" => json!(entry.http_version), "transport" => json!(entry.transport), "websocket_bootstrap" => entry .websocket_bootstrap .map(|v| Value::String(v.into())) .unwrap_or(Value::Null), "websocket_phase" => entry .websocket_phase .map(|v| Value::String(v.into())) .unwrap_or(Value::Null), "websocket_error" => entry .websocket_error .map(|v| Value::String(v.into())) .unwrap_or(Value::Null), "h3_reset_code" => entry .h3_reset_code .map(|v| Value::String(v.into())) .unwrap_or(Value::Null), "upstream_status" => entry .upstream_status .map(|v| json!(v)) .unwrap_or(Value::Null), "upstream_error" => entry .upstream_error .clone() .map(Value::String) .unwrap_or(Value::Null), "client_ip" => entry .client_ip .clone() .map(Value::String) .unwrap_or(Value::Null), "user_agent" => entry .user_agent .clone() .map(Value::String) .unwrap_or(Value::Null), "country" => entry .country .clone() .map(Value::String) .unwrap_or(Value::Null), "status" => json!(entry.status), "duration_ms" => json!(entry.duration_ms), "request_bytes" => json!(entry.request_bytes), "response_bytes" => json!(entry.response_bytes), "outcome" => json!(entry.outcome), "cache" => json!(entry.cache), "cache_reason" => json!(entry.cache_reason), _ => Value::Null, } }