logger: cleanup

This commit is contained in:
Mikaël Cluseau
2026-04-14 10:52:36 +02:00
parent a5026b884d
commit f3b3a9b9c7
4 changed files with 115 additions and 72 deletions

1
Cargo.lock generated
View File

@@ -353,6 +353,7 @@ dependencies = [
"human-units", "human-units",
"log", "log",
"lz4", "lz4",
"memchr",
"nix", "nix",
"openssl", "openssl",
"page_size", "page_size",

View File

@@ -28,6 +28,7 @@ hex = "0.4.3"
human-units = "0.5.3" human-units = "0.5.3"
log = "0.4.27" log = "0.4.27"
lz4 = "1.28.1" lz4 = "1.28.1"
memchr = "2.8.0"
nix = { version = "0.31.2", features = ["process", "signal", "user"] } nix = { version = "0.31.2", features = ["process", "signal", "user"] }
openssl = "0.10.73" openssl = "0.10.73"
page_size = "0.6.0" page_size = "0.6.0"

View File

@@ -176,6 +176,15 @@ impl Cgroup {
dir.push(name); dir.push(name);
Self::read(self.path.child(name), &dir).await Self::read(self.path.child(name), &dir).await
} }
pub async fn write_param(
&self,
name: impl AsRef<StdPath>,
value: impl AsRef<[u8]>,
) -> fs::Result<()> {
let cg_dir = PathBuf::from(self.path.as_ref());
fs::write(cg_dir.join(name), value).await
}
} }
impl PartialEq for Cgroup { impl PartialEq for Cgroup {

View File

@@ -9,7 +9,6 @@ use tokio::{
io::{self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}, io::{self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, BufWriter},
process, process,
sync::mpsc, sync::mpsc,
task::spawn_blocking,
time::{sleep, Duration}, time::{sleep, Duration},
}; };
@@ -52,48 +51,33 @@ impl<'t> Logger<'t> {
cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped()); cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
if let Some(cgroup) = cgroup.as_deref() { if let Some(cgroup) = cgroup.as_deref() {
let mut cg_path = PathBuf::from(cgroup::ROOT); let mut cg_path = PathBuf::from(cgroup::ROOT);
cg_path.push(cgroup);
cg_path.push(self.log_name);
let mut parts = cgroup.split('/').chain([self.log_name].into_iter()); use std::io::ErrorKind as K;
let mut part = parts.next().unwrap(); // 1 element guaranteed match tokio::fs::create_dir(&cg_path).await {
Ok(_) => debug!("created dir {}", cg_path.display()),
loop { Err(e) if e.kind() == K::AlreadyExists => {
cg_path.push(part); debug!("existing dir {}", cg_path.display())
use std::io::ErrorKind as K;
match tokio::fs::create_dir(&cg_path).await {
Ok(_) => log::debug!("created dir {}", cg_path.display()),
Err(e) if e.kind() == K::AlreadyExists => {
log::debug!("existing dir {}", cg_path.display())
}
Err(e) => Err(e)?,
} }
Err(e) => return Err(fs::Error::CreateDir(cg_path, e).into()),
let Some(next_part) = parts.next() else {
break;
};
let control_file = &cg_path.join("cgroup.subtree_control");
log::debug!("control file {}", control_file.display());
tokio::fs::write(control_file, b"+cpu +memory +pids +io").await?;
part = next_part;
} }
let procs_file = cg_path.join("cgroup.procs"); let procs_file = cg_path.join("cgroup.procs");
log::debug!("procs file {}", procs_file.display()); debug!("procs file {}", procs_file.display());
fs::write(&procs_file, b"0").await?; fs::write(&procs_file, b"0").await?;
} }
let mut child = cmd.spawn().map_err(|e| format_err!("exec failed: {e}"))?; let mut child = cmd.spawn().map_err(|e| format_err!("exec failed: {e}"))?;
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = mpsc::channel(8);
tokio::spawn(copy("stdout", child.stdout.take().unwrap(), tx.clone())); tokio::spawn(copy("stdout", child.stdout.take().unwrap(), tx.clone()));
tokio::spawn(copy("stderr", child.stderr.take().unwrap(), tx)); tokio::spawn(copy("stderr", child.stderr.take().unwrap(), tx));
// forward signals // forward signals
if let Some(child_pid) = child.id() { if let Some(child_pid) = child.id() {
spawn_blocking(move || forward_signals_to(child_pid as i32)).await?; forward_signals_to(child_pid as i32);
} }
// handle output // handle output
@@ -153,6 +137,9 @@ impl<'t> Logger<'t> {
} }
out.write_all(&log.line).await?; out.write_all(&log.line).await?;
if log.line.last() != Some(&b'\n') {
out.write("\n".as_bytes()).await?;
}
Ok(()) Ok(())
} }
@@ -166,29 +153,28 @@ impl<'t> Logger<'t> {
.open(log_file) .open(log_file)
.await?; .await?;
let link_src = &format!( let link_src = &PathBuf::from(self.log_path)
"{path}/{name}.log", .join(self.log_name)
path = self.log_path, .with_added_extension("log");
name = self.log_name let link_tgt = &self.archive_rel_path(ts);
);
let link_tgt = log_file
.strip_prefix(&format!("{}/", self.log_path))
.unwrap_or(log_file);
let _ = fs::remove_file(link_src).await; let _ = fs::remove_file(link_src).await;
fs::symlink(link_tgt, link_src) fs::symlink(link_tgt, link_src).await.map_err(|e| {
.await format_err!(
.map_err(|e| format_err!("symlink {link_src} -> {link_tgt} failed: {e}",))?; "symlink {s} -> {t} failed: {e}",
s = link_src.display(),
t = link_tgt.display()
)
})?;
Ok(file) Ok(file)
} }
fn archive_path(&self, ts: Timestamp) -> String { fn archive_path(&self, ts: Timestamp) -> PathBuf {
format!( PathBuf::from(self.log_path).join(self.archive_rel_path(ts))
"{path}/archives/{file}", }
path = self.log_path, fn archive_rel_path(&self, ts: Timestamp) -> PathBuf {
file = self.archive_file(ts) PathBuf::from("archives").join(self.archive_file(ts))
)
} }
fn archive_file(&self, ts: Timestamp) -> String { fn archive_file(&self, ts: Timestamp) -> String {
format!( format!(
@@ -206,7 +192,7 @@ fn forward_signals_to(pid: i32) {
}; };
use signal_hook::{consts::*, low_level::register}; use signal_hook::{consts::*, low_level::register};
log::debug!("forwarding signals to pid {pid}"); debug!("forwarding signals to pid {pid}");
let pid = Pid::from_raw(pid); let pid = Pid::from_raw(pid);
let signals = [ let signals = [
@@ -219,7 +205,7 @@ fn forward_signals_to(pid: i32) {
}; };
unsafe { unsafe {
register(sig, move || { register(sig, move || {
log::debug!("forwarding {signal} to {pid}"); debug!("forwarding {signal} to {pid}");
let _ = kill(pid, signal); let _ = kill(pid, signal);
}) })
.ok(); .ok();
@@ -233,33 +219,67 @@ struct LogItem {
line: Vec<u8>, line: Vec<u8>,
} }
async fn copy( async fn copy(stream_name: &'static str, out: impl AsyncRead + Unpin, tx: mpsc::Sender<LogItem>) {
stream_name: &'static str,
out: impl AsyncRead + Unpin,
tx: mpsc::UnboundedSender<LogItem>,
) {
let mut out = BufReader::new(out);
let buf_size = page_size::get(); let buf_size = page_size::get();
let mut out = BufReader::with_capacity(buf_size, out);
let mut line = Vec::with_capacity(buf_size);
macro_rules! send_line {
() => {
let log = LogItem {
stream_name,
ts: chrono::Utc::now(),
line: line.clone(),
};
if let Err(e) = tx.send(log).await {
warn!("send line failed: {e}");
return;
}
line.clear();
};
}
loop { loop {
let mut line = Vec::with_capacity(buf_size); let Ok(buf) = (out.fill_buf())
if let Err(e) = out.read_until(b'\n', &mut line).await { .await
warn!("read {stream_name} failed: {e}"); .inspect_err(|e| warn!("read {stream_name} failed: {e}"))
return; else {
} break;
if line.is_empty() { };
if buf.is_empty() {
break; break;
} }
let log = LogItem { let remaining = buf_size - line.len();
stream_name,
ts: chrono::Utc::now(), if let Some(pos) = memchr::memchr(b'\n', buf) {
line, let len = pos + 1;
}; let mut buf = &buf[..len];
if let Err(e) = tx.send(log) {
warn!("send line failed: {e}"); if len > remaining {
return; line.extend_from_slice(&buf[..remaining]);
send_line!();
buf = &buf[remaining..];
}
line.extend_from_slice(buf);
out.consume(len);
send_line!();
} else if buf.len() > remaining {
line.extend_from_slice(&buf[..remaining]);
out.consume(remaining);
send_line!();
} else {
line.extend_from_slice(buf);
let len = buf.len();
out.consume(len);
} }
} }
if !line.is_empty() {
send_line!();
}
} }
pub fn trunc_ts(ts: Timestamp) -> Timestamp { pub fn trunc_ts(ts: Timestamp) -> Timestamp {
@@ -319,14 +339,14 @@ async fn compress(path: impl AsRef<Path>) {
let mut out = ZstdEncoder::new(out); let mut out = ZstdEncoder::new(out);
async { async {
tokio::io::copy(&mut input, &mut out).await?; tokio::io::copy(&mut input, &mut out).await?;
out.flush().await out.shutdown().await
} }
.await .await
.map_err(|e| format_err!("compression of {path_str} failed: {e}"))?; .map_err(|e| format_err!("compression of {path_str} failed: {e}"))?;
fs::remove_file(path) fs::remove_file(path)
.await .await
.map_err(|e| format_err!("remove {path_str} failed: {e}")) .map_err(|e| format_err!("remove {path_str} failed: {e}"))
} }
.await; .await;
@@ -336,8 +356,9 @@ async fn compress(path: impl AsRef<Path>) {
} }
pub fn parse_ts(ts: &str) -> std::result::Result<Timestamp, chrono::ParseError> { pub fn parse_ts(ts: &str) -> std::result::Result<Timestamp, chrono::ParseError> {
let dt = let format = &format!("{TS_FORMAT}%M%S");
chrono::NaiveDateTime::parse_from_str(&format!("{ts}0000"), &format!("{TS_FORMAT}%M%S"))?; let full_ts = &format!("{ts}0000");
let dt = chrono::NaiveDateTime::parse_from_str(full_ts, format)?;
Ok(Timestamp::from_naive_utc_and_offset(dt, Utc)) Ok(Timestamp::from_naive_utc_and_offset(dt, Utc))
} }
@@ -389,16 +410,27 @@ pub async fn log_files(log_path: &str, log_name: &str) -> fs::Result<Vec<LogFile
Ok(entries) Ok(entries)
} }
#[derive(Debug, PartialEq, Eq, Ord)] #[derive(Debug)]
pub struct LogFile { pub struct LogFile {
pub path: PathBuf, pub path: PathBuf,
pub compressed: bool, pub compressed: bool,
pub timestamp: Timestamp, pub timestamp: Timestamp,
} }
impl Eq for LogFile {}
impl PartialEq for LogFile {
fn eq(&self, other: &Self) -> bool {
self.timestamp == other.timestamp
}
}
impl Ord for LogFile {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.timestamp.cmp(&other.timestamp)
}
}
impl PartialOrd for LogFile { impl PartialOrd for LogFile {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.timestamp.partial_cmp(&other.timestamp) Some(self.cmp(other))
} }
} }