Skip to content

Commit ab17d6e

Browse files
committed
feat(vmm): preserve serial logs across VM restarts
QEMU truncates serial.log on each boot, losing previous crash/panic logs that are critical for debugging restart loops. - Append serial.log to serial.history.log with boot timestamp separator before each VM restart, capped at a configurable max size (default 4MB) - Add boot separator timestamps to stdout.log and stderr.log so individual boot sessions are clearly delimited - Add `serial_history_max_bytes` config option (supports human-readable sizes like "4MB", "512KB")
1 parent 4f602dd commit ab17d6e

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

vmm/src/app.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ impl App {
242242
fs::remove_file(path)?;
243243
}
244244
}
245+
// Append current serial.log to serial.history.log before QEMU truncates it.
246+
rotate_serial_log(&work_dir, self.config.cvm.serial_history_max_bytes);
247+
// Add boot separator to stdout/stderr (they are opened in append mode).
248+
append_boot_separator(&work_dir.stdout_file());
249+
append_boot_separator(&work_dir.stderr_file());
245250

246251
let devices = self.try_allocate_gpus(&vm_config.manifest)?;
247252
let processes = vm_config.config_qemu(&work_dir, &self.config.cvm, &devices)?;
@@ -1051,6 +1056,64 @@ impl App {
10511056
}
10521057
}
10531058

1059+
/// Append a boot separator line with timestamp to an append-mode log file.
1060+
fn append_boot_separator(path: &std::path::Path) {
1061+
use std::io::Write;
1062+
if !path.exists() {
1063+
return;
1064+
}
1065+
let Ok(mut file) = std::fs::OpenOptions::new().append(true).open(path) else {
1066+
return;
1067+
};
1068+
let timestamp = humantime::format_rfc3339_seconds(std::time::SystemTime::now());
1069+
let _ = writeln!(file, "\n===== boot @ {timestamp} =====\n");
1070+
}
1071+
1072+
/// Append current serial.log into serial.history.log with a boot separator,
1073+
/// then truncate history if it exceeds `max_bytes`.
1074+
fn rotate_serial_log(work_dir: &VmWorkDir, max_bytes: u64) {
1075+
use std::io::Write;
1076+
1077+
let serial = work_dir.serial_file();
1078+
if !serial.exists() {
1079+
return;
1080+
}
1081+
let Ok(content) = fs::read(&serial) else {
1082+
return;
1083+
};
1084+
if content.is_empty() {
1085+
return;
1086+
}
1087+
let history = work_dir.serial_history_file();
1088+
let Ok(mut file) = std::fs::OpenOptions::new()
1089+
.create(true)
1090+
.append(true)
1091+
.open(&history)
1092+
else {
1093+
return;
1094+
};
1095+
let timestamp = humantime::format_rfc3339_seconds(std::time::SystemTime::now());
1096+
let _ = writeln!(file, "\n===== boot @ {timestamp} =====\n");
1097+
let _ = file.write_all(&content);
1098+
drop(file);
1099+
1100+
// Truncate from the front if history exceeds max_bytes.
1101+
if let Ok(meta) = fs::metadata(&history) {
1102+
if meta.len() > max_bytes {
1103+
if let Ok(data) = fs::read(&history) {
1104+
let skip = data.len() - max_bytes as usize;
1105+
// Find the next newline after skip point to avoid cutting mid-line.
1106+
let start = data[skip..]
1107+
.iter()
1108+
.position(|&b| b == b'\n')
1109+
.map(|p| skip + p + 1)
1110+
.unwrap_or(skip);
1111+
let _ = fs::write(&history, &data[start..]);
1112+
}
1113+
}
1114+
}
1115+
}
1116+
10541117
pub(crate) fn make_sys_config(cfg: &Config, manifest: &Manifest) -> Result<String> {
10551118
let image_path = cfg.image_path.join(&manifest.image);
10561119
let image = Image::load(image_path).context("Failed to load image info")?;

vmm/src/app/qemu.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,6 +1042,10 @@ impl VmWorkDir {
10421042
self.workdir.join("serial.log")
10431043
}
10441044

1045+
pub fn serial_history_file(&self) -> PathBuf {
1046+
self.workdir.join("serial.history.log")
1047+
}
1048+
10451049
pub fn serial_pty(&self) -> PathBuf {
10461050
self.workdir.join("serial.pty")
10471051
}

vmm/src/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,13 @@ pub struct CvmConfig {
203203
/// SMBIOS product information for cloud environment detection
204204
#[serde(default)]
205205
pub product: ProductConfig,
206+
207+
/// Max size in bytes for serial.history.log (default 4MB).
208+
/// Previous boot serial logs are appended here before each restart.
209+
/// Accepts human-readable sizes like "4MB", "512KB".
210+
#[serde(default = "default_serial_history_max_bytes")]
211+
#[serde(with = "size_parser::human_size")]
212+
pub serial_history_max_bytes: u64,
206213
}
207214

208215
/// SMBIOS product information configuration.
@@ -452,6 +459,10 @@ pub struct KeyProviderConfig {
452459
pub port: u16,
453460
}
454461

462+
fn default_serial_history_max_bytes() -> u64 {
463+
4 * 1024 * 1024 // 4MB
464+
}
465+
455466
const CLIENT_CONF_PATH: &str = "/etc/dstack/client.conf";
456467
fn read_qemu_path_from_client_conf() -> Option<PathBuf> {
457468
#[derive(Debug, Deserialize)]

0 commit comments

Comments
 (0)