Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CubeShim/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

173 changes: 135 additions & 38 deletions CubeShim/shim/src/log/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::io;
use std::io::Write;
use std::mem;
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use std::time::SystemTime;
use time::format_description::well_known::Rfc3339;
Expand Down Expand Up @@ -57,6 +57,7 @@ macro_rules! errf {
}

Comment thread
lisongqian marked this conversation as resolved.
const LOG_ITEM_COUNT: usize = 1024;
const LOG_REOPEN_INTERVAL: Duration = Duration::from_secs(1800);
const LOG_DIR: &str = "/data/log/CubeShim/";
const LOF_FILE: &str = "cube-shim-req.log";
const STAT_FILE: &str = "cube-shim-stat.log";
Expand Down Expand Up @@ -117,6 +118,55 @@ struct StatItem {
function_type: String,
}

struct ReopenableFile {
path: PathBuf,
file: tokio::fs::File,
}

impl ReopenableFile {
async fn open(path: &Path) -> CResult<Self> {
let file = OpenOptions::new()
Comment thread
lisongqian marked this conversation as resolved.
.create(true)
.write(true)
.append(true)
.open(path)
.await
.map_err(|e| format!("open log file failed:{} file:{:?}", e, path))?;
Ok(Self {
path: path.to_path_buf(),
file,
})
}

async fn reopen(&mut self) -> CResult<()> {
self.file
.flush()
.await
.map_err(|e| format!("flush log file before reopen failed:{}", e))?;
let file = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(&self.path)
.await
.map_err(|e| format!("reopen log file failed:{} file:{:?}", e, self.path))?;
self.file = file;
Ok(())
}
Comment thread
lisongqian marked this conversation as resolved.

async fn write(&mut self, content: &[u8]) -> CResult<()> {
Comment thread
lisongqian marked this conversation as resolved.
self.file
.write_all(content)
.await
.map_err(|e| format!("write log file failed:{} file:{:?}", e, self.path))?;
self.file
.flush()
.await
.map_err(|e| format!("flush log file failed:{} file:{:?}", e, self.path))?;
Ok(())
}
}

impl Default for Log {
fn default() -> Self {
let (sender, _) = mpsc::channel::<(LogType, String)>(1);
Expand Down Expand Up @@ -252,7 +302,7 @@ impl Log {
});

loop {
sleep(Duration::from_secs(1800)).await;
sleep(LOG_REOPEN_INTERVAL).await;
if let Err(e) = send.send((LogType::Rotate, "".to_string())).await {
eprintln!("send rotate failed:{}", e);
}
Expand All @@ -264,51 +314,21 @@ impl Log {
log_file_path: &PathBuf,
stat_file_path: &PathBuf,
) -> CResult<()> {
let log_file = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(log_file_path.clone())
.await
.map_err(|e| format!("open log file failed:{} file:{:?}", e, log_file_path))?;
let mut log_writer = tokio::io::BufReader::new(log_file);

let stat_file = OpenOptions::new()
.create(true)
.write(true)
.append(true)
.open(stat_file_path.clone())
.await
.map_err(|e| format!("open stat file failed:{} file:{:?}", e, stat_file_path))?;
let mut stat_writer = tokio::io::BufReader::new(stat_file);
let mut log_writer = ReopenableFile::open(log_file_path).await?;
let mut stat_writer = ReopenableFile::open(stat_file_path).await?;

//let lf = ['\n' as u8];
while let Some(msg) = recv.recv().await {
match msg.0 {
LogType::Log => {
log_writer
.write_all(msg.1.as_bytes())
.await
.map_err(|e| format!("write log file failed:{}", e))?;
//log_writer.write_all(&lf).await.map_err(|e| format!("write log file failed:{}", e));
log_writer
.flush()
.await
.map_err(|e| format!("flush log failed:{}", e))?;
log_writer.write(msg.1.as_bytes()).await?;
}
LogType::Stat => {
stat_writer
.write_all(msg.1.as_bytes())
.await
.map_err(|e| format!("write stat file failed:{}", e))?;
//stat_writer.write_all(&lf).await.map_err(|e| format!("write stat file failed:{}", e));
stat_writer
.flush()
.await
.map_err(|e| format!("flush stat failed:{}", e))?;
stat_writer.write(msg.1.as_bytes()).await?;
Comment thread
lisongqian marked this conversation as resolved.
}
LogType::Rotate => {
break;
log_writer.reopen().await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says "keep the current descriptor usable when reopen fails; retry on later writes with a one-second cooldown" — that behavior is implemented on the VMM side, but on the CubeShim side a failed reopen() propagates through ? and aborts the whole write_log_rotate invocation. The old descriptor is dropped when the function returns, and the consumer loop restarts the writer after a 3-second sleep with freshly-opened files.

Two consequences worth noting:

  • log_writer.reopen() and stat_writer.reopen() are not independent: if the log-file reopen succeeds but the stat-file reopen fails (or vice-versa), the error tears down both writers even though one descriptor was already swapped, causing a 3s gap plus a redundant reopen of the healthy file.
  • During the 3s restart window (or if the file remains unopenable and the 3s retry loop spins), the mpsc channel (cap 1024) can fill and try_send in Log::log/Log::stat silently drops messages.

This is recoverable, but it differs from the documented "keep old descriptor, retry with 1s cooldown" behavior. Consider matching the VMM's tolerant handling here (e.g. keep the old descriptor on reopen failure instead of ?), or adjusting the docs to describe the shim's restart behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? propagates a failed reopen() out of write_log_rotate, so the outer retry loop sleeps 3s during which the receiver is not drained; try_send (channel cap 1024) then drops log/stat messages. That drop window pre-exists for write errors, but reopen failures are a new, more likely trigger (e.g. logrotate create 0640 root root producing a file the shim's account can't reopen, which the docs explicitly warn about). Consider logging the reopen failure and continuing the loop instead of aborting it, so one failed reopen doesn't pause the writer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reopen() fails here, ? exits write_log_rotate, the outer loop in consumer drops the ReopenableFile (closing the old descriptor), sleeps 3 s, and restarts. That contradicts the PR description's "keep the current descriptor usable when reopen fails; retry on later writes with a one-second cooldown": the shim's descriptor is closed on failure and the retry interval is 3 s (during which messages queue in the bounded 1024-slot channel and are silently dropped under load).

Since ReopenableFile::reopen already keeps self.file on failure, the Rotate arm could log the error and continue the loop, retrying at the next 30-minute Rotate — matching the stated design. If the current propagate-and-restart behavior is intended, the PR description should be updated.

stat_writer.reopen().await?;
}
}
}
Expand Down Expand Up @@ -434,3 +454,80 @@ fn log_to_file(module: String, insid: String, log: String, func_type: String) ->

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

static NEXT_TEST_ID: AtomicU64 = AtomicU64::new(0);

fn test_log_path() -> PathBuf {
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before unix epoch")
.as_nanos();
let test_id = NEXT_TEST_ID.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"cube-shim-log-reopen-{}-{}-{}",
std::process::id(),
suffix,
test_id
))
}

struct TestLogFiles {
active: PathBuf,
rotated: PathBuf,
}

impl TestLogFiles {
fn new() -> Self {
let active = test_log_path();
let rotated = active.with_extension("log.1");
Self { active, rotated }
}
}

impl Drop for TestLogFiles {
fn drop(&mut self) {
let _ = fs::remove_file(&self.active);
let _ = fs::remove_file(&self.rotated);
}
}

#[tokio::test]
async fn reopens_after_periodic_rotation() {
let files = TestLogFiles::new();
let mut writer = ReopenableFile::open(&files.active).await.unwrap();

writer.write(b"before\n").await.unwrap();
fs::rename(&files.active, &files.rotated).unwrap();
fs::File::create(&files.active).unwrap();
Comment thread
lisongqian marked this conversation as resolved.
Comment thread
lisongqian marked this conversation as resolved.
Comment thread
lisongqian marked this conversation as resolved.
writer.reopen().await.unwrap();
writer.write(b"after\n").await.unwrap();
drop(writer);

assert_eq!(fs::read_to_string(&files.rotated).unwrap(), "before\n");
assert_eq!(fs::read_to_string(&files.active).unwrap(), "after\n");
}

#[tokio::test]
async fn failed_reopen_keeps_current_descriptor_usable() {
let files = TestLogFiles::new();
let mut writer = ReopenableFile::open(&files.active).await.unwrap();

writer.write(b"before\n").await.unwrap();
writer.path = std::env::temp_dir();
assert!(writer.reopen().await.is_err());
writer.write(b"after\n").await.unwrap();
drop(writer);

assert_eq!(
fs::read_to_string(&files.active).unwrap(),
"before\nafter\n"
);
}
}
43 changes: 43 additions & 0 deletions docs/guide/service-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,49 @@ sudo tail -F /data/log/CubeAPI/cube-api-$(date +%F).log
sudo tail -200 /data/log/CubeVmm/vmm.log
```

### Rotating CubeShim and VMM logs

CubeShim and the VMM keep their log files open while they run. CubeShim
reopens its files on an internal 30-minute rotation event. The VMM control
thread owns a monotonic `timerfd` and emits the existing `LOG_CTRL_REOPEN`
control record once per hour. Reopen is schedule-driven: an external
`rename` + `create` is picked up at the next scheduled reopen rather than
detected immediately on an arbitrary write. The timer is owned by the VMM
control thread, not by deferred logger initialization or a vCPU/API thread.
The host-side policy should run hourly and use `rename` + `create`; do not use
`copytruncate`.

For example, install the following as `/etc/logrotate.d/cubesandbox` and make
sure the host invokes `logrotate` hourly:

```text
/data/log/CubeVmm/vmm.log
/data/log/CubeShim/*.log {
hourly
rotate 24
missingok
notifempty
compress
delaycompress
create 0640 root root
Comment thread
lisongqian marked this conversation as resolved.
}
```

`rotate 24` retains 24 hourly files; adjust it to the required retention period.
`delaycompress` keeps the newest rotated file uncompressed for one cycle because
Comment thread
lisongqian marked this conversation as resolved.
a writer may still use the old descriptor until its next scheduled reopen.
CubeShim's internal event runs every 30 minutes, while the VMM control thread
sends a reopen control event every hour. No `postrotate` signal or service
restart is required. This policy guarantees bounded retention on the host,
but does not promise immediate handling of an arbitrary manual rotation; it
does not support `copytruncate`.

The example uses `root root` because the bundled one-click systemd services run as
root. If CubeShim or the VMM run under another account, set the `create` owner and
group to that account; otherwise a newly-created file may not be reopenable.
The `0640` mode in this example applies to files created by `logrotate`; the
CubeShim and VMM writers themselves continue to use the process umask.

### `journalctl` startup logs

journalctl captures **stdout/stderr from when systemd starts the process until it stabilizes (or exits)**, useful for:
Expand Down
24 changes: 24 additions & 0 deletions docs/zh/guide/service-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,30 @@ sudo tail -F /data/log/CubeAPI/cube-api-$(date +%F).log
sudo tail -200 /data/log/CubeVmm/vmm.log
```

### CubeShim 和 VMM 日志轮转

CubeShim 和 VMM 运行期间会保持日志文件打开。CubeShim 通过内部每 30 分钟轮转事件 reopen。VMM 控制线程自己持有 monotonic `timerfd`,每小时发出已有的 `LOG_CTRL_REOPEN` 控制记录。reopen 由固定周期驱动:宿主机执行“`rename` + `create`”后,会在下一次计划中的 reopen 时切换到新文件,而不是在任意一次写入时立即检测。该定时器由 VMM 控制线程持有,不依赖延迟 logger 初始化,也不会由 vCPU/API 线程创建。因此宿主机侧仍应按小时执行轮转,并使用“`rename` + `create`”方式,不要使用 `copytruncate`。

例如,将下面内容保存为 `/etc/logrotate.d/cubesandbox`,并确保宿主机每小时执行一次 `logrotate`:

```text
/data/log/CubeVmm/vmm.log
/data/log/CubeShim/*.log {
hourly
rotate 24
missingok
notifempty
compress
delaycompress
Comment thread
lisongqian marked this conversation as resolved.
create 0640 root root
}
```

`rotate 24` 表示保留 24 个小时文件,可按实际保留周期调整。`delaycompress` 会将最新的轮转文件延迟一个周期压缩,因为 writer 在下一次计划中的 reopen 之前可能仍使用旧 fd。CubeShim 每 30 分钟触发一次内部事件,VMM 控制线程每小时发送一次 reopen 控制事件。不需要 `postrotate` 信号或重启服务;该策略保证宿主机侧 retention 有界,但不承诺对任意手工轮转立即响应,也不支持 `copytruncate`。

示例使用 `root root`,因为随附的一键部署 systemd 服务以 root 运行。如果 CubeShim 或 VMM 使用其他账号运行,请把 `create` 的 owner 和 group 改成对应账号;否则新建文件可能无法被 reopen。
示例中的 `0640` 只适用于 `logrotate` 创建的文件;CubeShim 和 VMM writer 本身仍遵循进程的 umask。

### `journalctl` 启动期日志

journalctl 看的是**进程被 systemd 拉起到稳定运行(或失败退出)期间**的 stdout/stderr,主要用于:
Expand Down
2 changes: 1 addition & 1 deletion hypervisor/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion hypervisor/logging/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ authors = ["The Cloud Hypervisor Authors"]
edition = "2018"

[dependencies]
chrono = "0.4.24"
lazy_static = "1.2"
serde = { workspace = true, features = ["derive"] }
slog = "2.7.0"
Expand Down
Loading
Loading