v0.29.0
This commit is contained in:
1 parent
65e082b9d1
commit
76cb4acef0
72 files changed
+6134
-4709
No files matched your search
@@ -0,0 +1,106 @@
|
||||
// Bounded file-log inventory helpers and gzip tail reader.
|
||||
fn rotated_gzip_sources(
|
||||
active: &std::path::Path,
|
||||
id_prefix: &str,
|
||||
label_prefix: &str,
|
||||
kind: &'static str,
|
||||
enabled: bool,
|
||||
format: &'static str,
|
||||
) -> Vec<FileLogSource> {
|
||||
let Some(parent) = active.parent() else { return Vec::new(); };
|
||||
let Some(base) = active.file_name().and_then(|name| name.to_str()) else { return Vec::new(); };
|
||||
let safe = std::fs::symlink_metadata(parent).ok().is_some_and(|meta| meta.file_type().is_dir() && !meta.file_type().is_symlink());
|
||||
if !safe { return Vec::new(); }
|
||||
let prefix = format!("{base}.");
|
||||
let mut entries = std::fs::read_dir(parent).ok().into_iter().flat_map(|iter| iter.filter_map(|entry| entry.ok())).filter_map(|entry| {
|
||||
let name = entry.file_name().to_string_lossy().into_owned();
|
||||
if name.len() > 180 || !name.starts_with(&prefix) || !name.ends_with(".gz") { return None; }
|
||||
let file_type = entry.file_type().ok()?;
|
||||
if !file_type.is_file() || file_type.is_symlink() { return None; }
|
||||
let meta = entry.metadata().ok()?;
|
||||
Some((name, entry.path(), file_modified_ms(&meta)))
|
||||
}).collect::<Vec<_>>();
|
||||
entries.sort_by(|a,b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
|
||||
entries.truncate(64);
|
||||
entries.into_iter().map(|(name,path,_)| source_from_path(
|
||||
format!("{id_prefix}:gz:{name}"),
|
||||
format!("{label_prefix} · {name}"),
|
||||
kind, path, enabled, format
|
||||
)).collect()
|
||||
}
|
||||
|
||||
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();
|
||||
if source.compressed {
|
||||
// gzip streams are not seekable. Read a complete bounded rotation, then
|
||||
// keep only the same 2 MiB decompressed tail used for plain files.
|
||||
if length > budget || length > FILE_LOG_GZIP_INPUT_MAX {
|
||||
return Ok((Vec::new(), length.min(budget), true));
|
||||
}
|
||||
let mut decoder = flate2::read::GzDecoder::new(file);
|
||||
let mut decoded = Vec::new();
|
||||
decoder
|
||||
.by_ref()
|
||||
.take(FILE_LOG_GZIP_OUTPUT_MAX + 1)
|
||||
.read_to_end(&mut decoded)?;
|
||||
if decoded.len() as u64 > FILE_LOG_GZIP_OUTPUT_MAX {
|
||||
return Ok((Vec::new(), length, true));
|
||||
}
|
||||
let keep = decoded.len().min(FILE_LOG_SCAN_BYTES_PER_SOURCE as usize);
|
||||
let start = decoded.len().saturating_sub(keep);
|
||||
let mut content = decoded[start..].to_vec();
|
||||
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<_>>();
|
||||
return Ok((lines, length, truncated || iter.next().is_some()));
|
||||
}
|
||||
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()))
|
||||
}
|
||||
Reference in new issue
Block a user