mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
## Why Windows sandbox diagnostics currently append to a single `sandbox.log` under `CODEX_HOME/.sandbox`. That file never rolls over, which makes it hard to safely include sandbox diagnostics in future feedback reports without risking unbounded growth. ## What changed - Replaced direct append-open sandbox logging with `tracing_appender::rolling::RollingFileAppender`. - Configured sandbox logs to rotate daily using names like `sandbox.YYYY-MM-DD.log`. - Added a conservative `MAX_LOG_FILES` cap of 90 retained matching log files. - Routed the Windows sandbox setup helper through the same rolling writer. - Added helpers for resolving the current daily sandbox log path so future feedback upload work can use the same filename logic. - Updated tests and test diagnostics to read the dated daily log file. This intentionally does not include sandbox logs in `/feedback` yet; scrubbing and attachment behavior can happen in a follow-up. ## Testing - `cargo fmt -p codex-windows-sandbox` - `cargo check -p codex-windows-sandbox` - `cargo test -p codex-windows-sandbox` - `cargo test -p codex-windows-sandbox logging::tests` - `cargo clippy -p codex-windows-sandbox --all-targets -- -D warnings`
146 lines
4.4 KiB
Rust
146 lines
4.4 KiB
Rust
use std::io::Write;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::sync::OnceLock;
|
|
|
|
use codex_utils_string::take_bytes_at_char_boundary;
|
|
use tracing_appender::rolling::RollingFileAppender;
|
|
use tracing_appender::rolling::Rotation;
|
|
|
|
const LOG_COMMAND_PREVIEW_LIMIT: usize = 200;
|
|
pub const LOG_FILE_PREFIX: &str = "sandbox";
|
|
pub const LOG_FILE_SUFFIX: &str = "log";
|
|
pub const MAX_LOG_FILES: usize = 90;
|
|
|
|
fn exe_label() -> &'static str {
|
|
static LABEL: OnceLock<String> = OnceLock::new();
|
|
LABEL.get_or_init(|| {
|
|
std::env::current_exe()
|
|
.ok()
|
|
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
|
|
.unwrap_or_else(|| "proc".to_string())
|
|
})
|
|
}
|
|
|
|
fn preview(command: &[String]) -> String {
|
|
let joined = command.join(" ");
|
|
if joined.len() <= LOG_COMMAND_PREVIEW_LIMIT {
|
|
joined
|
|
} else {
|
|
take_bytes_at_char_boundary(&joined, LOG_COMMAND_PREVIEW_LIMIT).to_string()
|
|
}
|
|
}
|
|
|
|
pub fn log_file_path_for_utc_date(base_dir: &Path, date: chrono::NaiveDate) -> PathBuf {
|
|
base_dir.join(format!(
|
|
"{LOG_FILE_PREFIX}.{}.{}",
|
|
date.format("%Y-%m-%d"),
|
|
LOG_FILE_SUFFIX
|
|
))
|
|
}
|
|
|
|
pub fn current_log_file_path(base_dir: &Path) -> PathBuf {
|
|
log_file_path_for_utc_date(base_dir, chrono::Utc::now().date_naive())
|
|
}
|
|
|
|
pub fn log_writer(base_dir: &Path) -> Option<RollingFileAppender> {
|
|
if !base_dir.is_dir() {
|
|
return None;
|
|
}
|
|
|
|
RollingFileAppender::builder()
|
|
.rotation(Rotation::DAILY)
|
|
.filename_prefix(LOG_FILE_PREFIX)
|
|
.filename_suffix(LOG_FILE_SUFFIX)
|
|
.max_log_files(MAX_LOG_FILES)
|
|
.build(base_dir)
|
|
.ok()
|
|
}
|
|
|
|
fn append_line(line: &str, base_dir: Option<&Path>) {
|
|
if let Some(dir) = base_dir
|
|
&& let Some(mut f) = log_writer(dir)
|
|
{
|
|
let _ = writeln!(f, "{line}");
|
|
}
|
|
}
|
|
|
|
pub fn log_start(command: &[String], base_dir: Option<&Path>) {
|
|
let p = preview(command);
|
|
log_note(&format!("START: {p}"), base_dir);
|
|
}
|
|
|
|
pub fn log_success(command: &[String], base_dir: Option<&Path>) {
|
|
let p = preview(command);
|
|
log_note(&format!("SUCCESS: {p}"), base_dir);
|
|
}
|
|
|
|
pub fn log_failure(command: &[String], detail: &str, base_dir: Option<&Path>) {
|
|
let p = preview(command);
|
|
log_note(&format!("FAILURE: {p} ({detail})"), base_dir);
|
|
}
|
|
|
|
// Debug logging helper. Emits only when SBX_DEBUG=1 to avoid noisy logs.
|
|
pub fn debug_log(msg: &str, base_dir: Option<&Path>) {
|
|
if std::env::var("SBX_DEBUG").ok().as_deref() == Some("1") {
|
|
append_line(&format!("DEBUG: {msg}"), base_dir);
|
|
eprintln!("{msg}");
|
|
}
|
|
}
|
|
|
|
// Unconditional note logging to the daily sandbox log.
|
|
pub fn log_note(msg: &str, base_dir: Option<&Path>) {
|
|
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
|
|
append_line(&format!("[{ts} {}] {}", exe_label(), msg), base_dir);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn preview_does_not_panic_on_utf8_boundary() {
|
|
// Place a 4-byte emoji such that naive (byte-based) truncation would split it.
|
|
let prefix = "x".repeat(LOG_COMMAND_PREVIEW_LIMIT - 1);
|
|
let command = vec![format!("{prefix}😀")];
|
|
let result = std::panic::catch_unwind(|| preview(&command));
|
|
assert!(result.is_ok());
|
|
let previewed = result.unwrap();
|
|
assert!(previewed.len() <= LOG_COMMAND_PREVIEW_LIMIT);
|
|
}
|
|
|
|
#[test]
|
|
fn log_note_writes_to_daily_rolling_log() {
|
|
let tempdir = tempfile::tempdir().expect("tempdir");
|
|
|
|
log_note("hello daily log", Some(tempdir.path()));
|
|
|
|
let entries = std::fs::read_dir(tempdir.path())
|
|
.expect("read log dir")
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.expect("read entries");
|
|
assert_eq!(entries.len(), 1);
|
|
|
|
let log_path = entries[0].path();
|
|
let filename = log_path
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.expect("utf-8 filename");
|
|
assert!(filename.starts_with("sandbox."));
|
|
assert!(filename.ends_with(".log"));
|
|
|
|
let log = std::fs::read_to_string(log_path).expect("read log");
|
|
assert!(log.contains("hello daily log"));
|
|
}
|
|
|
|
#[test]
|
|
fn log_file_path_for_utc_date_matches_rolling_appender_name() {
|
|
let date = chrono::NaiveDate::from_ymd_opt(2026, 5, 21).expect("valid date");
|
|
|
|
assert_eq!(
|
|
log_file_path_for_utc_date(Path::new("logs"), date),
|
|
PathBuf::from("logs").join("sandbox.2026-05-21.log")
|
|
);
|
|
}
|
|
}
|