v0.29.0
This commit is contained in:
1 parent
65e082b9d1
commit
76cb4acef0
72 files changed
+6134
-4709
No files matched your search
+30
-56
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user