main: Create a format string parser and printer for log output

Introduces a custom format string parser for use for log entries. For
now only the existing format string entries are covered and the default
format string matches the existing behaviour.

The format string is tokenized once and then that token stream is used
for each log entry.

Assisted-by: Claude:Opus-4.7
Signed-off-by: Rob Bradford <rbradford@meta.com>
This commit is contained in:
Rob Bradford
2026-04-28 14:43:37 +01:00
parent 661faf51fe
commit a440ea0b44
2 changed files with 128 additions and 30 deletions
+122 -23
View File
@@ -3,11 +3,110 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// //
use std::io::Write;
use std::str::FromStr;
use std::sync::Mutex; use std::sync::Mutex;
use std::time::Instant;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unterminated '{{' in format string")]
UnterminatedBrace,
#[error("Unmatched '}}' in format string")]
UnmatchedBrace,
#[error("Unknown format token '{{{0}}}'")]
UnknownToken(String),
}
enum Token {
Literal(String),
BootTime,
Thread,
Level,
Location,
Msg,
}
impl FromStr for Token {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"boottime" => Ok(Self::BootTime),
"thread" => Ok(Self::Thread),
"level" => Ok(Self::Level),
"location" => Ok(Self::Location),
"msg" => Ok(Self::Msg),
_ => Err(Error::UnknownToken(s.to_string())),
}
}
}
fn parse_format(fmt: &str) -> Result<Vec<Token>, Error> {
let mut tokens = Vec::new();
let mut literal = String::new();
let mut chars = fmt.chars().peekable();
while let Some(c) = chars.next() {
match c {
'{' => {
if chars.peek() == Some(&'{') {
chars.next();
literal.push('{');
continue;
}
if !literal.is_empty() {
tokens.push(Token::Literal(std::mem::take(&mut literal)));
}
let mut name = String::new();
loop {
match chars.next() {
Some('}') => break,
Some(ch) => name.push(ch),
None => return Err(Error::UnterminatedBrace),
}
}
tokens.push(name.parse()?);
}
'}' => {
if chars.peek() == Some(&'}') {
chars.next();
literal.push('}');
} else {
return Err(Error::UnmatchedBrace);
}
}
_ => literal.push(c),
}
}
if !literal.is_empty() {
tokens.push(Token::Literal(literal));
}
Ok(tokens)
}
const DEFAULT_FORMAT: &str =
"cloud-hypervisor: {boottime}s: <{thread}> {level}:{location} -- {msg}";
pub struct Logger { pub struct Logger {
pub output: Mutex<Box<dyn std::io::Write + Send>>, output: Mutex<Box<dyn Write + Send>>,
pub start: std::time::Instant, start: Instant,
tokens: Vec<Token>,
}
impl Logger {
pub fn new(output: Box<dyn Write + Send>) -> Result<Self, Error> {
Ok(Self {
output: Mutex::new(output),
start: Instant::now(),
tokens: parse_format(DEFAULT_FORMAT)?,
})
}
} }
impl log::Log for Logger { impl log::Log for Logger {
@@ -20,28 +119,28 @@ impl log::Log for Logger {
return; return;
} }
let now = std::time::Instant::now(); let duration_s = Instant::now().duration_since(self.start).as_secs_f32();
let duration = now.duration_since(self.start);
let duration_s = duration.as_secs_f32();
let location = if let (Some(file), Some(line)) = (record.file(), record.line()) {
format!("{file}:{line}")
} else {
record.target().to_string()
};
let mut out = self.output.lock().unwrap(); let mut out = self.output.lock().unwrap();
write!( for token in &self.tokens {
&mut *out, let _ = match token {
// 10: 6 decimal places + sep => whole seconds in range `0..=999` properly aligned Token::Literal(s) => out.write_all(s.as_bytes()),
"cloud-hypervisor: {:>10.6?}s: <{}> {}:{} -- {}\r\n", // 10: 6 decimal places + sep => whole seconds in range `0..=999` properly aligned
duration_s, Token::BootTime => write!(&mut *out, "{duration_s:>10.6?}"),
std::thread::current().name().unwrap_or("anonymous"), Token::Thread => write!(
record.level(), &mut *out,
location, "{}",
record.args(), std::thread::current().name().unwrap_or("anonymous")
) ),
.ok(); Token::Level => write!(&mut *out, "{}", record.level()),
Token::Location => match (record.file(), record.line()) {
(Some(file), Some(line)) => write!(&mut *out, "{file}:{line}"),
_ => write!(&mut *out, "{}", record.target()),
},
Token::Msg => write!(&mut *out, "{}", record.args()),
};
}
let _ = out.write_all(b"\r\n");
} }
fn flush(&self) {} fn flush(&self) {}
} }
+6 -7
View File
@@ -9,7 +9,6 @@ mod test_util;
use std::fs::File; use std::fs::File;
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
use std::sync::Mutex;
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
use std::{env, io}; use std::{env, io};
@@ -97,6 +96,8 @@ enum Error {
BareGdb, BareGdb,
#[error("Error creating log file")] #[error("Error creating log file")]
LogFileCreation(#[source] std::io::Error), LogFileCreation(#[source] std::io::Error),
#[error("Error parsing logger format")]
LoggerFormat(#[source] logger::Error),
#[error("Error setting up logger")] #[error("Error setting up logger")]
LoggerSetup(#[source] log::SetLoggerError), LoggerSetup(#[source] log::SetLoggerError),
#[error("Failed to gracefully shutdown http api")] #[error("Failed to gracefully shutdown http api")]
@@ -514,12 +515,10 @@ fn start_vmm(
Box::new(std::io::stderr()) Box::new(std::io::stderr())
}; };
log::set_boxed_logger(Box::new(Logger { let logger = Logger::new(log_file).map_err(Error::LoggerFormat)?;
output: Mutex::new(log_file), log::set_boxed_logger(Box::new(logger))
start: std::time::Instant::now(), .map(|()| log::set_max_level(log_level))
})) .map_err(Error::LoggerSetup)?;
.map(|()| log::set_max_level(log_level))
.map_err(Error::LoggerSetup)?;
let (api_request_sender, api_request_receiver) = channel(); let (api_request_sender, api_request_receiver) = channel();
let api_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateApiEventFd)?; let api_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::CreateApiEventFd)?;