This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 15:59:37 +02:00
1 parent 65e082b9d1
commit 76cb4acef0
72 files changed
+6134 -4709

No files matched your search

+30 -56
View File
@@ -4,6 +4,8 @@ const FILE_LOG_SCAN_LINES: usize = 20_000;
const FILE_LOG_MAX_SOURCES: usize = 32;
const FILE_LOG_MAX_LIMIT: usize = 500;
const FILE_LOG_MAX_OFFSET: usize = 10_000;
const FILE_LOG_GZIP_INPUT_MAX: u64 = 8 * 1024 * 1024;
const FILE_LOG_GZIP_OUTPUT_MAX: u64 = 64 * 1024 * 1024;
#[derive(Clone)]
struct FileLogSource {
id: String,
@@ -15,6 +17,7 @@ struct FileLogSource {
exists: bool,
size: u64,
modified_ms: u64,
compressed: bool,
}
#[derive(Default, Deserialize)]
#[serde(default, deny_unknown_fields)]
@@ -48,6 +51,10 @@ fn source_from_path(
let safe_regular = metadata
.as_ref()
.is_some_and(|meta| meta.file_type().is_file() && !meta.file_type().is_symlink());
let compressed = path
.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("gz"));
FileLogSource {
id,
label,
@@ -64,8 +71,11 @@ fn source_from_path(
.as_ref()
.filter(|_| safe_regular)
.map_or(0, file_modified_ms),
compressed,
}
}
include!("file_logs_io.rs");
fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSource> {
let mut sources = Vec::new();
if config.logging.file.per_vhost {
@@ -81,7 +91,9 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
.filter_map(|entry| {
let path = entry.path();
let name = entry.file_name().to_string_lossy().into_owned();
if !name.ends_with(".jsonl") || name.len() > 160 {
let plain = name.ends_with(".jsonl");
let rotated = name.contains(".jsonl.") && name.ends_with(".gz");
if (!plain && !rotated) || name.len() > 180 {
return None;
}
let file_type = entry.file_type().ok()?;
@@ -95,7 +107,11 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
entries.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
entries.truncate(256);
for (name, path, _) in entries {
let id = format!("vhost:{}", name.trim_end_matches(".jsonl"));
let id = if name.ends_with(".jsonl") {
format!("vhost:{}", name.trim_end_matches(".jsonl"))
} else {
format!("vhost-gz:{name}")
};
sources.push(source_from_path(
id,
name,
@@ -107,26 +123,31 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSou
}
}
} else {
let access_path = std::path::PathBuf::from(&config.logging.file.path);
sources.push(source_from_path(
"access".into(),
"Access log".into(),
"access",
std::path::PathBuf::from(&config.logging.file.path),
access_path.clone(),
config.logging.file.enabled,
"JSONL",
));
sources.extend(rotated_gzip_sources(&access_path, "access", "Access log", "access", config.logging.file.enabled, "JSONL"));
}
let error_format = match config.error_log.format {
crate::logging::ErrorLogFormat::Json => "JSONL",
crate::logging::ErrorLogFormat::Raw => "RAW",
};
let error_path = std::path::PathBuf::from(&config.error_log.path);
sources.push(source_from_path(
"error".into(),
"Error log".into(),
"error",
std::path::PathBuf::from(&config.error_log.path),
error_path.clone(),
config.error_log.enabled,
match config.error_log.format {
crate::logging::ErrorLogFormat::Json => "JSONL",
crate::logging::ErrorLogFormat::Raw => "RAW",
},
error_format,
));
sources.extend(rotated_gzip_sources(&error_path, "error", "Error log", "error", config.error_log.enabled, error_format));
sources
}
fn file_log_sources_json(sources: &[FileLogSource]) -> Value {
@@ -144,6 +165,7 @@ fn file_log_sources_json(sources: &[FileLogSource]) -> Value {
"exists": source.exists,
"size": source.size,
"modified_ms": source.modified_ms,
"compressed": source.compressed,
})
})
.collect(),
@@ -171,54 +193,6 @@ async fn file_logs(State(state): State<Arc<App>>) -> Result<Json<Value>, ApiErro
}
})))
}
fn open_file_log(path: &std::path::Path) -> std::io::Result<std::fs::File> {
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW);
}
let file = options.open(path)?;
let metadata = file.metadata()?;
if !metadata.file_type().is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"configured log path is not a regular file",
));
}
Ok(file)
}
fn read_tail(
source: &FileLogSource,
budget: u64,
line_budget: usize,
) -> std::io::Result<(Vec<String>, u64, bool)> {
use std::io::{Read, Seek, SeekFrom};
let mut file = open_file_log(&source.path)?;
let length = file.metadata()?.len();
let bytes = length.min(budget).min(FILE_LOG_SCAN_BYTES_PER_SOURCE);
let start = length.saturating_sub(bytes);
file.seek(SeekFrom::Start(start))?;
let mut content = Vec::with_capacity(bytes as usize);
file.take(bytes).read_to_end(&mut content)?;
let truncated = start > 0;
if truncated {
if let Some(index) = content.iter().position(|byte| *byte == b'\n') {
content.drain(..=index);
} else {
content.clear();
}
}
let text = String::from_utf8_lossy(&content);
let mut iter = text.lines().rev();
let lines = iter
.by_ref()
.take(line_budget)
.map(str::to_owned)
.collect::<Vec<_>>();
Ok((lines, bytes, truncated || iter.next().is_some()))
}
fn copy_mapped_field(
object: &mut serde_json::Map<String, Value>,
format: &crate::logging::JsonFormat,