403 lines
13 KiB
Rust
403 lines
13 KiB
Rust
#[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<String>,
|
|
pub error: Option<String>,
|
|
pub local_addr: Option<String>,
|
|
pub tls_sni: Option<String>,
|
|
pub sni: Option<String>,
|
|
pub authority: Option<String>,
|
|
pub matched_by: Option<&'static str>,
|
|
pub vhost: Option<String>,
|
|
pub route: Option<String>,
|
|
pub cache_effective_enabled: Option<bool>,
|
|
pub transform_effective_enabled: Option<bool>,
|
|
pub backend: Option<String>,
|
|
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<u16>,
|
|
pub upstream_error: Option<String>,
|
|
pub client_ip: Option<String>,
|
|
pub user_agent: Option<String>,
|
|
pub country: Option<String>,
|
|
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<VecDeque<LogEntry>>,
|
|
pub dropped: AtomicU64,
|
|
settings: Arc<ArcSwap<Settings>>,
|
|
live: broadcast::Sender<LogEntry>,
|
|
sink: mpsc::Sender<LogEntry>,
|
|
sink_dropped: Arc<AtomicU64>,
|
|
sink_sent: Arc<AtomicU64>,
|
|
sink_errors: Arc<AtomicU64>,
|
|
}
|
|
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<String>) -> Option<String> {
|
|
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<Self> {
|
|
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) {
|
|
// String cloning and eviction cleanup do not need the history mutex.
|
|
let history_entry = entry.clone();
|
|
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
|
|
};
|
|
drop(evicted);
|
|
// No live viewers means no reason to allocate/clone a second LogEntry.
|
|
// History retention and external sinks remain unchanged.
|
|
if self.live.receiver_count() != 0 {
|
|
let _ = self.live.send(entry.clone());
|
|
}
|
|
self.push_sink(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<String>,
|
|
) {
|
|
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<String>,
|
|
authority: Option<String>,
|
|
vhost: Option<String>,
|
|
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<LogEntry> {
|
|
self.live.subscribe()
|
|
}
|
|
pub fn recent(&self) -> Vec<LogEntry> {
|
|
self.recent_limit(HISTORY_CAPACITY)
|
|
}
|
|
pub fn recent_limit(&self, limit: usize) -> Vec<LogEntry> {
|
|
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,
|
|
}
|
|
}
|