#[derive(Debug, Clone, Default)]
pub struct ServerLogMetrics {
pub prompt_tokens: Option<u64>,
pub prompt_progress: Option<f64>,
pub prompt_elapsed_ms: Option<f64>,
pub prompt_tps_eval: Option<f64>,
pub ctx_used: Option<u32>,
pub decoded_tokens: Option<u64>,
pub gen_tps: Option<f64>,
}
pub fn parse_log_line(line: &str, prev_line: Option<&str>) -> (ServerLogMetrics, bool) {
let mut metrics = ServerLogMetrics::default();
let mut is_generation = false;
let is_prompt_processing = line.contains("prompt processing");
let is_generation_line = line.contains("n_decoded =");
if is_generation_line {
is_generation = true;
}
if is_prompt_processing {
if let Some(tokens_part) = line.split("n_tokens =").last() {
let val_str = tokens_part.split(',').next().unwrap_or(tokens_part).trim();
if let Ok(tokens) = val_str.parse::<u64>() {
metrics.prompt_tokens = Some(tokens);
}
}
if let Some(progress_part) = line.split("progress =").last() {
let val_str = progress_part
.split(',')
.next()
.unwrap_or(progress_part)
.trim();
if let Ok(progress) = val_str.parse::<f64>() {
metrics.prompt_progress = Some(progress);
}
}
let mut parsed = false;
if let Some(t_part) = line.split("t =").last() {
let val_str = t_part
.split_whitespace()
.find(|s| !s.is_empty())
.unwrap_or("")
.trim();
if let Ok(t) = val_str.parse::<f64>() {
metrics.prompt_elapsed_ms = Some(t * 1000.0);
parsed = true;
}
}
if !parsed {
if let Some(t_part) = line.split("t=").last() {
let val_str = t_part
.split(|c: char| c.is_whitespace() || c == '\u{2560}')
.find(|s| !s.is_empty())
.unwrap_or("")
.trim();
if let Ok(t) = val_str.parse::<f64>() {
metrics.prompt_elapsed_ms = Some(t * 1000.0);
parsed = true;
}
}
}
if !parsed {
if let Some(slash_part) = line.rsplit(" s /").next() {
let val_str = slash_part
.split_whitespace()
.rfind(|s| !s.is_empty())
.unwrap_or("")
.trim();
if let Ok(t) = val_str.parse::<f64>() {
metrics.prompt_elapsed_ms = Some(t * 1000.0);
}
}
}
let tps_str = line
.contains("tokens per second")
.then(|| {
line.split("tokens per second")
.next()
.and_then(|p| p.split('/').next_back())
.map(|s| s.trim())
})
.flatten()
.or(prev_line.and_then(|prev| {
prev.split("tokens per second")
.next()
.and_then(|p| p.split('/').next_back())
.map(|s| s.trim())
}));
if let Some(tps_str) = tps_str {
if let Ok(tps) = tps_str.parse::<f64>() {
metrics.prompt_tps_eval = Some(tps);
}
}
}
if !is_prompt_processing && line.contains("n_tokens =") {
if let Some(tokens_part) = line.split("n_tokens =").last() {
let val_str = tokens_part.split(',').next().unwrap_or(tokens_part).trim();
if let Ok(tokens) = val_str.parse::<u32>() {
metrics.ctx_used = Some(tokens);
}
}
}
if line.contains("n_decoded =") {
if let Some(decoded_part) = line.split("n_decoded =").last() {
let val_str = decoded_part
.split(',')
.next()
.unwrap_or(decoded_part)
.trim();
if let Ok(tokens) = val_str.parse::<u64>() {
metrics.decoded_tokens = Some(tokens);
}
}
}
if line.contains("tg =") {
if let Some(tg_part) = line.split("tg =").last() {
let val_str = tg_part.trim().split(' ').next().unwrap_or(tg_part).trim();
if let Ok(tg) = val_str.parse::<f64>() {
metrics.gen_tps = Some(tg);
}
}
}
(metrics, is_generation)
}