This commit is contained in:
Mateusz Gruszczyński committed 2026-10-12 11:22:42 +02:00
1 parent 68ada6680d
commit 180a0e4877
56 files changed
+9866 -205

No files matched your search

+88 -11
View File
@@ -50,6 +50,91 @@ fn record_matches(value: &Value, query: &FileLogQuery, search: &[FileLogSearchCl
true
}
// Order the entire bounded matching scan before applying offset/limit. Sorting
// only the current browser page would give misleading navigation results.
#[derive(Debug)]
enum FileLogSortKey {
Number(serde_json::Number),
Text(String),
}
fn stored_log_summary(value: &Value) -> String {
let field = |name| value.get(name).and_then(Value::as_str).filter(|text| !text.is_empty());
if let Some(reason) = field("reason").or_else(|| field("protocol_error")) {
return reason.to_owned();
}
if field("method").is_some() || field("path").is_some() {
return format!("{} {}{}", field("method").unwrap_or(""),
field("host").or_else(|| field("authority")).unwrap_or(""),
field("path").unwrap_or("")).trim().to_owned();
}
for key in ["error", "error_detail", "message", "raw", "outcome"] {
if let Some(text) = field(key) { return text.to_owned(); }
}
String::new()
}
fn stored_log_sort_key(record: &Value, field: &str) -> Option<FileLogSortKey> {
if field == "record" {
return Some(FileLogSortKey::Text(stored_log_summary(record).to_lowercase()));
}
// Prefer millisecond timestamps for numeric chronological sorting. RAW
// file timestamps use the existing ISO/text fallback.
let value = if field == "time" {
record.get("time_ms").filter(|value| value.is_number())
.or_else(|| record.get("timestamp"))
} else {
file_log_field(record, field)
}?;
match value {
Value::Null => None,
Value::Number(number) => Some(FileLogSortKey::Number(number.clone())),
Value::String(text) if !text.is_empty() => Some(FileLogSortKey::Text(text.to_lowercase())),
Value::Bool(boolean) => Some(FileLogSortKey::Text(boolean.to_string())),
Value::Array(_) | Value::Object(_) => Some(FileLogSortKey::Text(value.to_string().to_lowercase())),
_ => None,
}
}
fn file_log_timestamp_cmp_desc(left: &Value, right: &Value) -> std::cmp::Ordering {
match (left.get("time_ms").and_then(Value::as_u64), right.get("time_ms").and_then(Value::as_u64)) {
(Some(a), Some(b)) => b.cmp(&a),
_ => right.get("timestamp").and_then(Value::as_str).unwrap_or("")
.cmp(left.get("timestamp").and_then(Value::as_str).unwrap_or("")),
}
}
fn sort_file_log_records(records: &mut Vec<Value>, field: &str, descending: bool) {
use std::cmp::Ordering;
// Build one key per record; do not repeatedly allocate/parse strings in
// the O(n log n) comparison loop. The scan itself is limited to 20k lines.
let mut keyed = std::mem::take(records).into_iter().enumerate()
.map(|(ordinal, record)| (stored_log_sort_key(&record, field), ordinal, record))
.collect::<Vec<_>>();
keyed.sort_by(|left, right| {
let order = match (&left.0, &right.0) {
(None, None) => Ordering::Equal,
(None, Some(_)) => return Ordering::Greater, // missing values always last
(Some(_), None) => return Ordering::Less,
(Some(FileLogSortKey::Number(a)), Some(FileLogSortKey::Number(b))) =>
match (a.as_i64(), b.as_i64()) {
(Some(left), Some(right)) => left.cmp(&right),
_ => match (a.as_u64(), b.as_u64()) {
(Some(left), Some(right)) => left.cmp(&right),
_ => a.as_f64().partial_cmp(&b.as_f64()).unwrap_or(Ordering::Equal),
},
},
(Some(FileLogSortKey::Text(a)), Some(FileLogSortKey::Text(b))) => a.cmp(b),
(Some(FileLogSortKey::Number(_)), Some(FileLogSortKey::Text(_))) => Ordering::Less,
(Some(FileLogSortKey::Text(_)), Some(FileLogSortKey::Number(_))) => Ordering::Greater,
};
(if descending { order.reverse() } else { order })
.then_with(|| file_log_timestamp_cmp_desc(&left.2, &right.2))
.then_with(|| left.1.cmp(&right.1))
});
records.extend(keyed.into_iter().map(|(_, _, record)| record));
}
fn validate_file_log_query(query: &FileLogQuery) -> Result<(), ApiError> {
let text_ok = |value: &Option<String>, max: usize| {
value
@@ -61,6 +146,7 @@ fn validate_file_log_query(query: &FileLogQuery) -> Result<(), ApiError> {
|| !text_ok(&query.client_ip, 128)
|| !text_ok(&query.vhost, 256)
|| query.aggregate_field.as_ref().is_some_and(|field| !valid_raw_field_key(field))
|| query.sort_by.as_ref().is_some_and(|field| !valid_raw_field_key(field))
|| query.aggregate_limit.is_some_and(|value| value == 0 || value > FILE_LOG_MAX_AGGREGATE_RESULTS)
|| query.aggregate_min_count.is_some_and(|value| value < 2 || value > FILE_LOG_SCAN_LINES)
|| query.offset.unwrap_or(0) > FILE_LOG_MAX_OFFSET
@@ -171,17 +257,8 @@ async fn file_logs_query(
Err(_) => truncated = true,
}
}
records.sort_by(|a, b| {
let numeric = |value: &Value| value.get("time_ms").and_then(Value::as_u64);
match (numeric(a), numeric(b)) {
(Some(left), Some(right)) => right.cmp(&left),
_ => b
.get("timestamp")
.and_then(Value::as_str)
.unwrap_or("")
.cmp(a.get("timestamp").and_then(Value::as_str).unwrap_or("")),
}
});
sort_file_log_records(&mut records, query.sort_by.as_deref().unwrap_or("time"),
query.sort_descending.unwrap_or(true));
let mut aggregation = aggregate_counts.into_iter()
.filter(|(_, count)| *count >= aggregate_min_count)
.map(|(value, count)| json!({"value": value, "count": count}))