mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
Preserve daemon diagnostics and include updater logs in reports (#49843)
## Why Detached launches truncate stderr logs, losing details from previous failures. Updater handoffs can also leave existing writers with stale offsets after truncation. ## What changed - Record daemon lifecycle and update stages with timings, outcomes, and typed OS errors, omitting error messages and context. - Preserve the last 256 KiB of nonempty stderr logs before truncation and use append handles to prevent gaps during handoff. Preservation failures do not block launch. - Attach current and previous updater log tails when log inclusion is enabled for a report, with a 512 KiB total limit and legacy filename fallback. Skip missing, empty, and nonregular files. ## Testing Add regression coverage for diagnostic error redaction, bounded log preservation, append behavior during handoff, attachment selection, and updater logs in captured report uploads. GitOrigin-RevId: 918317022ab9dcfd1b13d676872706c77e9ffbb2
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
//! Collects bounded current and previous updater log tails after feedback log consent.
|
||||
//! Read failures omit individual files; only fixed regular-file names are considered.
|
||||
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::io::Seek;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::FeedbackAttachment;
|
||||
|
||||
const MAX_LOG_BYTES: u64 = 256 * 1024;
|
||||
const LOG_NAMES: [&str; 2] = ["daemon-updater.stderr.log", "app-server-updater.stderr.log"];
|
||||
|
||||
/// Collect at most two 256 KiB updater tails (512 KiB total) from the app-server host.
|
||||
/// Call only after feedback consent includes logs, on a blocking worker.
|
||||
pub fn daemon_log_attachments(codex_home: &Path) -> Vec<FeedbackAttachment> {
|
||||
let directory = codex_home.join("app-server-daemon");
|
||||
let mut attachments = Vec::new();
|
||||
for suffix in ["", ".previous"] {
|
||||
for name in LOG_NAMES {
|
||||
let filename = format!("{name}{suffix}");
|
||||
let path = directory.join(&filename);
|
||||
let result = (|| -> io::Result<Vec<u8>> {
|
||||
// Reject symlinks and nonregular files before opening (e.g. FIFOs).
|
||||
if !fs::symlink_metadata(&path)?.is_file() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut file = fs::File::open(&path)?;
|
||||
let metadata = file.metadata()?;
|
||||
if !metadata.is_file() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
file.seek(io::SeekFrom::Start(
|
||||
metadata.len().saturating_sub(MAX_LOG_BYTES),
|
||||
))?;
|
||||
let mut bytes = Vec::new();
|
||||
file.take(MAX_LOG_BYTES).read_to_end(&mut bytes)?;
|
||||
Ok(bytes)
|
||||
})();
|
||||
match result {
|
||||
Ok(buffer) if !buffer.is_empty() => {
|
||||
attachments.push(FeedbackAttachment {
|
||||
filename,
|
||||
content_type: Some("text/plain".to_string()),
|
||||
buffer,
|
||||
});
|
||||
// Legacy filenames are a fallback, not additional attachments.
|
||||
break;
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
tracing::warn!(filename, error_kind = ?error.kind(), "failed to read daemon feedback log; skipping attachment")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
attachments
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "daemon_logs_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,55 @@
|
||||
use super::*;
|
||||
use codex_protocol::ThreadId;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn collects_bounded_tails_and_skips_missing_empty_and_nonregular_files() -> io::Result<()> {
|
||||
let home = std::env::temp_dir().join(format!("codex-feedback-daemon-{}", ThreadId::new()));
|
||||
let directory = home.join("app-server-daemon");
|
||||
assert!(daemon_log_attachments(&home).is_empty());
|
||||
fs::create_dir_all(&directory)?;
|
||||
let bytes = vec![b'x'; MAX_LOG_BYTES as usize + 100];
|
||||
fs::write(directory.join("daemon.stderr.log"), b"already included")?;
|
||||
fs::write(directory.join("daemon-updater.stderr.log"), &bytes)?;
|
||||
fs::write(
|
||||
directory.join("daemon.stderr.log.previous"),
|
||||
b"failed restart",
|
||||
)?;
|
||||
fs::write(
|
||||
directory.join("app-server-updater.stderr.log"),
|
||||
b"legacy updater",
|
||||
)?;
|
||||
fs::write(
|
||||
directory.join("app-server-updater.stderr.log.previous"),
|
||||
b"previous updater",
|
||||
)?;
|
||||
fs::create_dir(directory.join("app-server.stderr.log.previous"))?;
|
||||
let attachments = daemon_log_attachments(&home);
|
||||
let actual: Vec<_> = attachments
|
||||
.into_iter()
|
||||
.map(|attachment| {
|
||||
(
|
||||
attachment.filename,
|
||||
attachment.content_type,
|
||||
attachment.buffer,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
(
|
||||
"daemon-updater.stderr.log".to_string(),
|
||||
Some("text/plain".to_string()),
|
||||
bytes[100..].to_vec()
|
||||
),
|
||||
(
|
||||
"app-server-updater.stderr.log.previous".to_string(),
|
||||
Some("text/plain".to_string()),
|
||||
b"previous updater".to_vec()
|
||||
),
|
||||
]
|
||||
);
|
||||
fs::remove_dir_all(home)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -31,6 +31,8 @@ use tracing_subscriber::filter::Targets;
|
||||
use tracing_subscriber::fmt::writer::MakeWriter;
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
|
||||
mod daemon_logs;
|
||||
pub use daemon_logs::daemon_log_attachments;
|
||||
pub(crate) mod feedback_diagnostics;
|
||||
mod guardian;
|
||||
mod report_upload;
|
||||
|
||||
Reference in New Issue
Block a user