This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 19:05:55 +02:00
1 parent ddc5c6593f
commit b9d137eee1
63 files changed
+1232 -445

No files matched your search

+74 -2
View File
@@ -2,9 +2,14 @@
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>,
@@ -85,6 +90,21 @@ fn protocol_error_detail(detail: impl std::fmt::Display) -> String {
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);
@@ -143,18 +163,47 @@ impl Logs {
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),
error_detail: Some(detail),
reason: Some(kind),
error_detail: Some(detail.clone()),
error: Some(detail),
local_addr: Some(local.to_string()),
tls_sni,
vhost: None,
sni,
authority,
matched_by: Some(matched_by),
vhost,
route: None,
cache_effective_enabled: None,
transform_effective_enabled: None,
@@ -221,11 +270,20 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
.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()
@@ -236,6 +294,20 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
.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()
+5
View File
@@ -231,9 +231,14 @@ fn source_value_name(name: &str) -> bool {
name,
"event"
| "protocol_error"
| "reason"
| "error_detail"
| "error"
| "local_addr"
| "tls_sni"
| "sni"
| "authority"
| "matched_by"
| "vhost"
| "route"
| "cache_effective_enabled"
+10 -18
View File
@@ -19,11 +19,17 @@ fn format_entry(entry: &LogEntry, format: &JsonFormat) -> Value {
for name in [
"event",
"protocol_error",
"reason",
"error_detail",
"error",
"time_ms",
"client_ip",
"local_addr",
"tls_sni",
"sni",
"authority",
"matched_by",
"vhost",
"transport",
"http_version",
"status",
@@ -63,24 +69,10 @@ struct FileState {
const PER_VHOST_OPEN_FILES: usize = 64;
fn vhost_log_name(vhost: Option<&str>) -> String {
let raw = vhost
.filter(|value| !value.is_empty())
.unwrap_or("_unmatched");
let mut safe = String::with_capacity(raw.len().min(96));
for ch in raw.chars().take(96) {
safe.push(
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
ch
} else {
'_'
},
);
}
if safe.is_empty() {
"_unmatched".into()
} else {
safe
}
vhost
.filter(|value| crate::config::valid_id(value))
.unwrap_or("_unmatched")
.to_owned()
}
struct LogstashState {
address: String,
+25 -1
View File
@@ -5,9 +5,14 @@ mod tests {
LogEntry {
event: "request",
protocol_error: None,
reason: None,
error_detail: None,
error: None,
local_addr: Some("192.0.2.1:443".into()),
tls_sni: Some("app.example".into()),
sni: Some("app.example".into()),
authority: None,
matched_by: None,
vhost: Some("app".into()),
route: Some("root".into()),
cache_effective_enabled: Some(true),
@@ -95,7 +100,7 @@ mod tests {
fn per_vhost_file_mode_has_safe_names_and_requires_directory() {
assert_eq!(vhost_log_name(Some("app-1")), "app-1");
assert_eq!(vhost_log_name(None), "_unmatched");
assert_eq!(vhost_log_name(Some("bad/name")), "bad_name");
assert_eq!(vhost_log_name(Some("bad/name")), "_unmatched");
let mut settings = Settings::default();
settings.file.enabled = true;
settings.file.per_vhost = true;
@@ -124,7 +129,12 @@ mod tests {
let mut item = entry();
item.event = "protocol_error";
item.protocol_error = Some("tls_handshake_error");
item.reason = Some("tls_handshake_error");
item.error_detail = Some("invalid handshake".into());
item.error = Some("invalid handshake".into());
item.sni = Some("app.example".into());
item.authority = Some("app.example:443".into());
item.matched_by = Some("sni");
item.outcome = "protocol_error";
item.status = 0;
item.transport = "tls";
@@ -135,7 +145,12 @@ mod tests {
assert_eq!(value["site"], "app");
assert_eq!(value["event"], "protocol_error");
assert_eq!(value["protocol_error"], "tls_handshake_error");
assert_eq!(value["reason"], "tls_handshake_error");
assert_eq!(value["error_detail"], "invalid handshake");
assert_eq!(value["error"], "invalid handshake");
assert_eq!(value["sni"], "app.example");
assert_eq!(value["authority"], "app.example:443");
assert_eq!(value["matched_by"], "sni");
assert_eq!(value["client_ip"], "192.0.2.10");
assert_eq!(value["transport"], "tls");
assert_eq!(value["outcome"], "protocol_error");
@@ -170,6 +185,15 @@ mod tests {
assert!(detail.ends_with("..."));
}
#[test]
fn protocol_identity_fields_are_bounded_and_strip_controls() {
let value = protocol_identity_field(Some(format!("host\n{}", "€".repeat(300)))).unwrap();
assert!(!value.contains('\n'));
assert!(value.is_char_boundary(value.len()));
assert!(value.len() <= 515);
assert!(value.ends_with("..."));
}
#[test]
fn error_log_defaults_off_and_redacts_common_secret_shapes() {
let settings = ErrorLogSettings::default();