feat(logging): overhaul debug log handling

This commit is contained in:
James Peter
2026-01-05 13:43:25 +10:00
parent f81a1af71a
commit afba3b9cac
12 changed files with 668 additions and 122 deletions
+51 -6
View File
@@ -13,6 +13,7 @@ use once_cell::sync::Lazy;
use reqwest::Client;
use serde::Deserialize;
use serde_json::Value;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::sync::RwLock;
@@ -85,6 +86,43 @@ fn is_temporary_internal_launch_error_message(message: &str) -> bool {
|| message.contains("os error 24")
}
fn chrome_logging_enabled() -> bool {
env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_BROWSER_LOG")
}
fn env_truthy(key: &str) -> bool {
std::env::var(key)
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
.unwrap_or(false)
}
fn resolve_chrome_log_path() -> Option<PathBuf> {
if !chrome_logging_enabled() {
return None;
}
if let Ok(path) = std::env::var("CODEX_BROWSER_LOG_PATH") {
let trimmed = path.trim();
if !trimmed.is_empty() {
return Some(PathBuf::from(trimmed));
}
}
let base = if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
PathBuf::from(home).join("debug_logs")
} else if let Ok(home) = std::env::var("HOME") {
PathBuf::from(home).join(".code").join("debug_logs")
} else {
return Some(std::env::temp_dir().join("code-chrome.log"));
};
let path = base.join("code-chrome.log");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
Some(path)
}
const HANDLER_ERROR_LIMIT: u32 = 3;
fn should_restart_handler(consecutive_errors: u32) -> bool {
@@ -771,7 +809,7 @@ impl BrowserManager {
let max_attempts = INTERNAL_LAUNCH_RETRY_DELAYS_MS.len() + 1;
// Add browser launch flags (keep minimal set for screenshot functionality)
let log_file = format!("{}/code-chrome.log", std::env::temp_dir().display());
let log_file = resolve_chrome_log_path();
let (browser, mut handler, user_data_path) = {
let mut attempt = 1usize;
@@ -814,15 +852,18 @@ impl BrowserManager {
// Disable timeout for slow networks/pages
.arg("--disable-hang-monitor")
.arg("--disable-background-timer-throttling")
// Redirect Chrome logging to a file to keep terminal clean
.arg("--enable-logging")
.arg("--log-level=1") // 0 = INFO, 1 = WARNING, 2 = ERROR, 3 = FATAL (1 to reduce verbosity)
.arg(format!("--log-file={log_file}"))
// Suppress console output
.arg("--silent-launch")
// Set a longer timeout for CDP requests (60 seconds instead of default 30)
.request_timeout(Duration::from_secs(60));
if let Some(ref log_file) = log_file {
builder = builder
.arg("--enable-logging")
.arg("--log-level=1")
.arg(format!("--log-file={}", log_file.display()));
}
let browser_config = builder
.build()
.map_err(|e| BrowserError::CdpError(e.to_string()))?;
@@ -859,8 +900,12 @@ impl BrowserManager {
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
let hint = "Ensure Chrome/Chromium is installed and available on PATH.";
let log_note = log_file
.as_ref()
.map(|path| format!(" Chrome log: {}", path.display()))
.unwrap_or_default();
return Err(BrowserError::CdpError(format!(
"Failed to launch internal browser: {message}. Hint: {hint}. Chrome log: {log_file}"
"Failed to launch internal browser: {message}. Hint: {hint}.{log_note}"
)));
}
}
+1 -2
View File
@@ -378,8 +378,7 @@ struct OrderReplayArgs {
/// Path to a response.json captured under ~/.code/debug_logs/*_response.json
/// (legacy ~/.codex/debug_logs/ is still read).
response_json: std::path::PathBuf,
/// Path to codex-tui.log (typically ~/.code/log/codex-tui.log; legacy
/// ~/.codex/log/codex-tui.log is still read).
/// Path to codex-tui.log (typically ~/.code/debug_logs/codex-tui.log).
tui_log: std::path::PathBuf,
}
+183 -4
View File
@@ -12,6 +12,9 @@ use crate::TurnAttempt;
use crate::api::TaskText;
use chrono::DateTime;
use chrono::Utc;
use std::borrow::Cow;
use std::path::Path;
use std::path::PathBuf;
use code_backend_client as backend;
use code_backend_client::CodeTaskDetailsResponseExt;
@@ -141,7 +144,7 @@ mod api {
.map(map_task_list_item_to_summary)
.collect();
append_error_log(&format!(
append_debug_log(&format!(
"http.list_tasks: env={} items={}",
env.unwrap_or("<all>"),
tasks.len()
@@ -260,7 +263,7 @@ mod api {
match self.backend.create_task(request_body).await {
Ok(id) => {
append_error_log(&format!(
append_info_log(&format!(
"new_task: created id={id} env={} prompt_chars={}",
env_id,
prompt.chars().count()
@@ -756,14 +759,190 @@ mod api {
}
}
const CLOUD_TASKS_LOG_FILE: &str = "cloud-tasks.log";
const CLOUD_TASKS_LOG_MAX_BYTES: u64 = 5 * 1024 * 1024;
const CLOUD_TASKS_LOG_BACKUPS: usize = 2;
const CLOUD_TASKS_LOG_MAX_MESSAGE_BYTES: usize = 64 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum CloudLogLevel {
Off = 0,
Error = 1,
Info = 2,
Debug = 3,
}
fn log_level_from_env() -> CloudLogLevel {
if let Ok(raw) = std::env::var("CODEX_CLOUD_TASKS_LOG_LEVEL") {
let value = raw.trim().to_ascii_lowercase();
return match value.as_str() {
"off" | "none" | "0" => CloudLogLevel::Off,
"error" | "warn" | "1" => CloudLogLevel::Error,
"info" | "2" => CloudLogLevel::Info,
"debug" | "trace" | "3" => CloudLogLevel::Debug,
_ => CloudLogLevel::Error,
};
}
if env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_CLOUD_TASKS_DEBUG") {
return CloudLogLevel::Debug;
}
CloudLogLevel::Off
}
fn env_truthy(key: &str) -> bool {
std::env::var(key)
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
.unwrap_or(false)
}
fn should_log(level: CloudLogLevel) -> bool {
let configured = log_level_from_env();
configured != CloudLogLevel::Off && level <= configured
}
fn user_home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(home) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(home));
}
None
}
fn resolve_log_path() -> Option<PathBuf> {
if let Ok(path) = std::env::var("CODEX_CLOUD_TASKS_LOG_PATH") {
let trimmed = path.trim();
if !trimmed.is_empty() {
return Some(PathBuf::from(trimmed));
}
}
let base = if let Ok(dir) = std::env::var("CODEX_CLOUD_TASKS_LOG_DIR") {
PathBuf::from(dir)
} else if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
PathBuf::from(home).join("debug_logs")
} else if let Some(home) = user_home_dir() {
home.join(".code").join("debug_logs")
} else {
return None;
};
Some(base.join(CLOUD_TASKS_LOG_FILE))
}
fn ensure_parent_dir(path: &Path) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
}
fn rotate_log_file(path: &Path, max_bytes: u64, backups: usize) {
if backups == 0 {
let _ = std::fs::remove_file(path);
return;
}
let Ok(meta) = std::fs::metadata(path) else {
return;
};
if meta.len() <= max_bytes {
return;
}
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
return;
};
let Some(dir) = path.parent() else {
return;
};
let lock_path = dir.join(format!("{file_name}.rotate.lock"));
let lock_file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path);
let Ok(_lock_file) = lock_file else {
return;
};
let Ok(meta) = std::fs::metadata(path) else {
let _ = std::fs::remove_file(&lock_path);
return;
};
if meta.len() <= max_bytes {
let _ = std::fs::remove_file(&lock_path);
return;
}
let oldest = dir.join(format!("{file_name}.{backups}"));
let _ = std::fs::remove_file(&oldest);
if backups > 1 {
for idx in (1..backups).rev() {
let from = dir.join(format!("{file_name}.{idx}"));
let to = dir.join(format!("{file_name}.{}", idx + 1));
let _ = std::fs::rename(&from, &to);
}
}
let rotated = dir.join(format!("{file_name}.1"));
let _ = std::fs::copy(path, &rotated);
if let Ok(file) = std::fs::OpenOptions::new().write(true).open(path) {
let _ = file.set_len(0);
}
let _ = std::fs::remove_file(&lock_path);
}
fn truncate_message(message: &str, max_bytes: usize) -> Cow<'_, str> {
if message.len() <= max_bytes {
return Cow::Borrowed(message);
}
let bytes = message.as_bytes();
let head = String::from_utf8_lossy(&bytes[..max_bytes]).to_string();
Cow::Owned(format!("{head}\n...truncated..."))
}
fn append_error_log(message: &str) {
append_cloud_log(CloudLogLevel::Error, message);
}
fn append_info_log(message: &str) {
append_cloud_log(CloudLogLevel::Info, message);
}
fn append_debug_log(message: &str) {
append_cloud_log(CloudLogLevel::Debug, message);
}
fn append_cloud_log(level: CloudLogLevel, message: &str) {
if !should_log(level) {
return;
}
let Some(path) = resolve_log_path() else {
return;
};
ensure_parent_dir(&path);
rotate_log_file(&path, CLOUD_TASKS_LOG_MAX_BYTES, CLOUD_TASKS_LOG_BACKUPS);
let ts = Utc::now().to_rfc3339();
let level_label = match level {
CloudLogLevel::Error => "ERROR",
CloudLogLevel::Info => "INFO",
CloudLogLevel::Debug => "DEBUG",
CloudLogLevel::Off => return,
};
let message = truncate_message(message, CLOUD_TASKS_LOG_MAX_MESSAGE_BYTES);
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open("error.log")
.open(&path)
{
use std::io::Write as _;
let _ = writeln!(f, "[{ts}] {message}");
let _ = writeln!(f, "[{ts}] {level_label} {message}");
}
}
+15 -15
View File
@@ -28,7 +28,7 @@ pub async fn autodetect_environment_id(
) -> anyhow::Result<AutodetectSelection> {
// 1) Try repo-specific environments based on local git origins (GitHub only, like VSCode)
let origins = get_git_origins();
crate::append_error_log(format!("env: git origins: {origins:?}"));
crate::append_debug_log(format!("env: git origins: {origins:?}"));
let mut by_repo_envs: Vec<CodeEnvironment> = Vec::new();
for origin in &origins {
if let Some((owner, repo)) = parse_owner_repo(origin) {
@@ -43,16 +43,16 @@ pub async fn autodetect_environment_id(
base_url, "github", owner, repo
)
};
crate::append_error_log(format!("env: GET {url}"));
crate::append_debug_log(format!("env: GET {url}"));
match get_json::<Vec<CodeEnvironment>>(&url, headers).await {
Ok(mut list) => {
crate::append_error_log(format!(
crate::append_debug_log(format!(
"env: by-repo returned {} env(s) for {owner}/{repo}",
list.len(),
));
by_repo_envs.append(&mut list);
}
Err(e) => crate::append_error_log(format!(
Err(e) => crate::append_debug_log(format!(
"env: by-repo fetch failed for {owner}/{repo}: {e}"
)),
}
@@ -71,7 +71,7 @@ pub async fn autodetect_environment_id(
} else {
format!("{base_url}/api/codex/environments")
};
crate::append_error_log(format!("env: GET {list_url}"));
crate::append_debug_log(format!("env: GET {list_url}"));
// Fetch and log the full environments JSON for debugging
let http = reqwest::Client::builder().build()?;
let res = http.get(&list_url).headers(headers.clone()).send().await?;
@@ -83,13 +83,13 @@ pub async fn autodetect_environment_id(
.unwrap_or("")
.to_string();
let body = res.text().await.unwrap_or_default();
crate::append_error_log(format!("env: status={status} content-type={ct}"));
crate::append_debug_log(format!("env: status={status} content-type={ct}"));
match serde_json::from_str::<serde_json::Value>(&body) {
Ok(v) => {
let pretty = serde_json::to_string_pretty(&v).unwrap_or(body.clone());
crate::append_error_log(format!("env: /environments JSON (pretty):\n{pretty}"));
crate::append_debug_log(format!("env: /environments JSON (pretty):\n{pretty}"));
}
Err(_) => crate::append_error_log(format!("env: /environments (raw):\n{body}")),
Err(_) => crate::append_debug_log(format!("env: /environments (raw):\n{body}")),
}
if !status.is_success() {
anyhow::bail!("GET {list_url} failed: {status}; content-type={ct}; body={body}");
@@ -119,16 +119,16 @@ fn pick_environment_row(
.iter()
.find(|e| e.label.as_deref().unwrap_or("").to_lowercase() == lc)
{
crate::append_error_log(format!("env: matched by label: {label} -> {}", e.id));
crate::append_debug_log(format!("env: matched by label: {label} -> {}", e.id));
return Some(e.clone());
}
}
if envs.len() == 1 {
crate::append_error_log("env: single environment available; selecting it");
crate::append_debug_log("env: single environment available; selecting it");
return Some(envs[0].clone());
}
if let Some(e) = envs.iter().find(|e| e.is_pinned.unwrap_or(false)) {
crate::append_error_log(format!("env: selecting pinned environment: {}", e.id));
crate::append_debug_log(format!("env: selecting pinned environment: {}", e.id));
return Some(e.clone());
}
// Highest task_count as heuristic
@@ -137,7 +137,7 @@ fn pick_environment_row(
.max_by_key(|e| e.task_count.unwrap_or(0))
.or_else(|| envs.first())
{
crate::append_error_log(format!("env: selecting by task_count/first: {}", e.id));
crate::append_debug_log(format!("env: selecting by task_count/first: {}", e.id));
return Some(e.clone());
}
None
@@ -157,7 +157,7 @@ async fn get_json<T: serde::de::DeserializeOwned>(
.unwrap_or("")
.to_string();
let body = res.text().await.unwrap_or_default();
crate::append_error_log(format!("env: status={status} content-type={ct}"));
crate::append_debug_log(format!("env: status={status} content-type={ct}"));
if !status.is_success() {
anyhow::bail!("GET {url} failed: {status}; content-type={ct}; body={body}");
}
@@ -228,7 +228,7 @@ fn parse_owner_repo(url: &str) -> Option<(String, String)> {
let mut parts = rest.splitn(2, '/');
let owner = parts.next()?.to_string();
let repo = parts.next()?.to_string();
crate::append_error_log(format!("env: parsed SSH GitHub origin => {owner}/{repo}"));
crate::append_debug_log(format!("env: parsed SSH GitHub origin => {owner}/{repo}"));
return Some((owner, repo));
}
// HTTPS or git protocol
@@ -243,7 +243,7 @@ fn parse_owner_repo(url: &str) -> Option<(String, String)> {
let mut parts = rest.splitn(2, '/');
let owner = parts.next()?.to_string();
let repo = parts.next()?.to_string();
crate::append_error_log(format!("env: parsed HTTP GitHub origin => {owner}/{repo}"));
crate::append_debug_log(format!("env: parsed HTTP GitHub origin => {owner}/{repo}"));
return Some((owner, repo));
}
}
+22 -14
View File
@@ -17,7 +17,10 @@ use tokio::sync::mpsc::UnboundedSender;
use tracing::info;
use tracing_subscriber::EnvFilter;
use code_tui::public_widgets::composer_input::ComposerAction;
use util::append_debug_log;
use util::append_error_log;
use util::append_info_log;
use util::log_path_hint;
use util::set_user_agent_suffix;
struct ApplyJob {
@@ -192,7 +195,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
} else {
"codex-api"
};
append_error_log(format!("startup: base_url={base_url} path_style={style}"));
append_info_log(format!("startup: base_url={base_url} path_style={style}"));
// Require ChatGPT login (SWIC). Exit with a clear message if missing.
let _token = match code_core::config::find_code_home()
@@ -209,7 +212,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
Some(auth) => {
// Log account context for debugging workspace selection.
if let Some(acc) = auth.get_account_id() {
append_error_log(format!("auth: mode=ChatGPT account_id={acc}"));
append_info_log(format!("auth: mode=ChatGPT account_id={acc}"));
}
match auth.get_token().await {
Ok(t) if !t.is_empty() => {
@@ -219,7 +222,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
.get_account_id()
.or_else(|| util::extract_chatgpt_account_id(&t))
{
append_error_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
append_info_log(format!("auth: set ChatGPT-Account-Id header: {acc}"));
http = http.with_chatgpt_account_id(acc);
}
t
@@ -282,7 +285,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
.as_deref(),
Some("1") | Some("true") | Some("TRUE")
);
append_error_log(format!(
append_info_log(format!(
"startup: wham_force_internal={} ua={}",
force_internal,
code_core::default_client::get_code_user_agent(None)
@@ -432,7 +435,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
app::AppEvent::TasksLoaded { env, result } => {
// Only apply results for the current filter to avoid races.
if env.as_deref() != app.env_filter.as_deref() {
append_error_log(format!(
append_debug_log(format!(
"refresh.drop: env={} current={}",
env.clone().unwrap_or_else(|| "<all>".to_string()),
app.env_filter.clone().unwrap_or_else(|| "<all>".to_string())
@@ -442,7 +445,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
app.refresh_inflight = false;
match result {
Ok(tasks) => {
append_error_log(format!(
append_debug_log(format!(
"refresh.apply: env={} count={}",
env.clone().unwrap_or_else(|| "<all>".to_string()),
tasks.len()
@@ -462,7 +465,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
app::AppEvent::NewTaskSubmitted(result) => {
match result {
Ok(created) => {
append_error_log(format!("new-task: created id={}", created.id.0));
append_info_log(format!("new-task: created id={}", created.id.0));
app.status = format!("Submitted as {}", created.id.0);
app.new_task = None;
// Refresh tasks in background for current filter
@@ -482,7 +485,12 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
Err(msg) => {
append_error_log(format!("new-task: submit failed: {msg}"));
if let Some(page) = app.new_task.as_mut() { page.submitting = false; }
app.status = format!("Submit failed: {msg}. See error.log for details.");
if let Some(log_hint) = log_path_hint() {
app.status =
format!("Submit failed: {msg}. See {log_hint} for details.");
} else {
app.status = format!("Submit failed: {msg}.");
}
needs_redraw = true;
let _ = frame_tx.send(Instant::now());
}
@@ -523,7 +531,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
if let Ok(sel) = result {
// Only apply if user hasn't set a filter yet or it's different.
if app.env_filter.as_deref() != Some(sel.id.as_str()) {
append_error_log(format!(
append_info_log(format!(
"env.select: autodetected id={} label={}",
sel.id,
sel.label.clone().unwrap_or_else(|| "<none>".to_string())
@@ -909,7 +917,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
if let Some(page) = app.new_task.as_mut() {
page.best_of_n = new_value;
}
append_error_log(format!("best-of.select: attempts={new_value}"));
append_info_log(format!("best-of.select: attempts={new_value}"));
app.status = format!(
"Best-of updated to {new_value} attempt{}",
if new_value == 1 { "" } else { "s" }
@@ -970,7 +978,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
} else if let ComposerAction::Submitted(text) = page.composer.input(key) {
// Submit only if we have an env id
if let Some(env) = page.env_id.clone() {
append_error_log(format!(
append_debug_log(format!(
"new-task: submit env={} size={}",
env,
text.chars().count()
@@ -1242,11 +1250,11 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
}).collect();
// Keep original order (already sorted) — no need to re-sort
let idx = state.selected;
if idx == 0 { app.env_filter = None; append_error_log("env.select: All"); }
if idx == 0 { app.env_filter = None; append_info_log("env.select: All"); }
else {
let env_idx = idx.saturating_sub(1);
if let Some(row) = filtered.get(env_idx) {
append_error_log(format!(
append_info_log(format!(
"env.select: id={} label={}",
row.id,
row.label.clone().unwrap_or_else(|| "<none>".to_string())
@@ -1293,7 +1301,7 @@ pub async fn run_main(cli: Cli, _code_linux_sandbox_exe: Option<PathBuf>) -> any
// Ensure 'r' does not refresh tasks when the env modal is open.
KeyCode::Char('r') | KeyCode::Char('R') => {
if app.env_modal.is_some() { break 0; }
append_error_log(format!(
append_debug_log(format!(
"refresh.request: env={}",
app.env_filter.clone().unwrap_or_else(|| "<all>".to_string())
));
+206 -12
View File
@@ -1,6 +1,212 @@
use base64::Engine as _;
use chrono::Utc;
use reqwest::header::HeaderMap;
use std::borrow::Cow;
use std::path::Path;
use std::path::PathBuf;
const CLOUD_TASKS_LOG_FILE: &str = "cloud-tasks.log";
const CLOUD_TASKS_LOG_MAX_BYTES: u64 = 5 * 1024 * 1024;
const CLOUD_TASKS_LOG_BACKUPS: usize = 2;
const CLOUD_TASKS_LOG_MAX_MESSAGE_BYTES: usize = 64 * 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum CloudLogLevel {
Off = 0,
Error = 1,
Info = 2,
Debug = 3,
}
fn log_level_from_env() -> CloudLogLevel {
if let Ok(raw) = std::env::var("CODEX_CLOUD_TASKS_LOG_LEVEL") {
let value = raw.trim().to_ascii_lowercase();
return match value.as_str() {
"off" | "none" | "0" => CloudLogLevel::Off,
"error" | "warn" | "1" => CloudLogLevel::Error,
"info" | "2" => CloudLogLevel::Info,
"debug" | "trace" | "3" => CloudLogLevel::Debug,
_ => CloudLogLevel::Error,
};
}
if env_truthy("CODE_SUBAGENT_DEBUG") || env_truthy("CODEX_CLOUD_TASKS_DEBUG") {
return CloudLogLevel::Debug;
}
CloudLogLevel::Off
}
fn env_truthy(key: &str) -> bool {
std::env::var(key)
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
.unwrap_or(false)
}
fn should_log(level: CloudLogLevel) -> bool {
let configured = log_level_from_env();
configured != CloudLogLevel::Off && level <= configured
}
fn user_home_dir() -> Option<PathBuf> {
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home));
}
if let Ok(home) = std::env::var("USERPROFILE") {
return Some(PathBuf::from(home));
}
None
}
fn resolve_log_path() -> Option<PathBuf> {
if let Ok(path) = std::env::var("CODEX_CLOUD_TASKS_LOG_PATH") {
let trimmed = path.trim();
if !trimmed.is_empty() {
return Some(PathBuf::from(trimmed));
}
}
let base = if let Ok(dir) = std::env::var("CODEX_CLOUD_TASKS_LOG_DIR") {
PathBuf::from(dir)
} else if let Ok(home) = std::env::var("CODE_HOME").or_else(|_| std::env::var("CODEX_HOME")) {
PathBuf::from(home).join("debug_logs")
} else if let Some(home) = user_home_dir() {
home.join(".code").join("debug_logs")
} else {
return None;
};
Some(base.join(CLOUD_TASKS_LOG_FILE))
}
fn ensure_parent_dir(path: &Path) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
}
fn rotate_log_file(path: &Path, max_bytes: u64, backups: usize) {
if backups == 0 {
let _ = std::fs::remove_file(path);
return;
}
let Ok(meta) = std::fs::metadata(path) else {
return;
};
if meta.len() <= max_bytes {
return;
}
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
return;
};
let Some(dir) = path.parent() else {
return;
};
let lock_path = dir.join(format!("{file_name}.rotate.lock"));
let lock_file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path);
let Ok(_lock_file) = lock_file else {
return;
};
let Ok(meta) = std::fs::metadata(path) else {
let _ = std::fs::remove_file(&lock_path);
return;
};
if meta.len() <= max_bytes {
let _ = std::fs::remove_file(&lock_path);
return;
}
let oldest = dir.join(format!("{file_name}.{backups}"));
let _ = std::fs::remove_file(&oldest);
if backups > 1 {
for idx in (1..backups).rev() {
let from = dir.join(format!("{file_name}.{idx}"));
let to = dir.join(format!("{file_name}.{}", idx + 1));
let _ = std::fs::rename(&from, &to);
}
}
let rotated = dir.join(format!("{file_name}.1"));
let _ = std::fs::copy(path, &rotated);
if let Ok(file) = std::fs::OpenOptions::new().write(true).open(path) {
let _ = file.set_len(0);
}
let _ = std::fs::remove_file(&lock_path);
}
fn truncate_message(message: &str, max_bytes: usize) -> Cow<'_, str> {
if message.len() <= max_bytes {
return Cow::Borrowed(message);
}
let bytes = message.as_bytes();
let head = String::from_utf8_lossy(&bytes[..max_bytes]).to_string();
Cow::Owned(format!("{head}\n...truncated..."))
}
pub fn log_path_hint() -> Option<String> {
if !logging_enabled() {
return None;
}
Some(
resolve_log_path()
.map(|path| path.display().to_string())
.unwrap_or_else(|| CLOUD_TASKS_LOG_FILE.to_string()),
)
}
pub fn logging_enabled() -> bool {
log_level_from_env() != CloudLogLevel::Off
}
pub fn append_error_log(message: impl AsRef<str>) {
append_cloud_log(CloudLogLevel::Error, message.as_ref());
}
pub fn append_info_log(message: impl AsRef<str>) {
append_cloud_log(CloudLogLevel::Info, message.as_ref());
}
pub fn append_debug_log(message: impl AsRef<str>) {
append_cloud_log(CloudLogLevel::Debug, message.as_ref());
}
fn append_cloud_log(level: CloudLogLevel, message: &str) {
if !should_log(level) {
return;
}
let Some(path) = resolve_log_path() else {
return;
};
ensure_parent_dir(&path);
rotate_log_file(&path, CLOUD_TASKS_LOG_MAX_BYTES, CLOUD_TASKS_LOG_BACKUPS);
let ts = Utc::now().to_rfc3339();
let level_label = match level {
CloudLogLevel::Error => "ERROR",
CloudLogLevel::Info => "INFO",
CloudLogLevel::Debug => "DEBUG",
CloudLogLevel::Off => return,
};
let message = truncate_message(message, CLOUD_TASKS_LOG_MAX_MESSAGE_BYTES);
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
use std::io::Write as _;
let _ = writeln!(f, "[{ts}] {level_label} {message}");
}
}
pub fn set_user_agent_suffix(suffix: &str) {
if let Ok(mut guard) = code_core::default_client::USER_AGENT_SUFFIX.lock() {
@@ -8,18 +214,6 @@ pub fn set_user_agent_suffix(suffix: &str) {
}
}
pub fn append_error_log(message: impl AsRef<str>) {
let ts = Utc::now().to_rfc3339();
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open("error.log")
{
use std::io::Write as _;
let _ = writeln!(f, "[{ts}] {}", message.as_ref());
}
}
/// Normalize the configured base URL to a canonical form used by the backend client.
/// - trims trailing '/'
/// - appends '/backend-api' for ChatGPT hosts when missing
+60 -1
View File
@@ -1490,10 +1490,69 @@ impl Config {
pub fn log_dir(cfg: &Config) -> std::io::Result<PathBuf> {
let mut p = cfg.code_home.clone();
p.push("log");
p.push("debug_logs");
Ok(p)
}
pub fn migrate_legacy_log_dirs(code_home: &Path) {
let target = code_home.join("debug_logs");
let legacy_dirs = [code_home.join("log"), code_home.join("logs")];
for legacy in legacy_dirs {
let _ = migrate_dir_contents(&legacy, &target);
}
}
fn migrate_dir_contents(source: &Path, target: &Path) -> std::io::Result<()> {
if !source.exists() {
return Ok(());
}
std::fs::create_dir_all(target)?;
for entry in std::fs::read_dir(source)? {
let entry = entry?;
migrate_entry(&entry.path(), target)?;
}
let _ = std::fs::remove_dir(source);
Ok(())
}
fn migrate_entry(path: &Path, target_dir: &Path) -> std::io::Result<()> {
let Some(file_name) = path.file_name() else {
return Ok(());
};
let mut target = target_dir.join(file_name);
if target.exists() {
let base = file_name.to_string_lossy();
let mut idx = 1u32;
loop {
let candidate = target_dir.join(format!("{base}.legacy.{idx}"));
if !candidate.exists() {
target = candidate;
break;
}
idx = idx.saturating_add(1);
}
}
if path.is_dir() {
std::fs::create_dir_all(&target)?;
for entry in std::fs::read_dir(path)? {
let entry = entry?;
migrate_entry(&entry.path(), &target)?;
}
let _ = std::fs::remove_dir_all(path);
return Ok(());
}
match std::fs::rename(path, &target) {
Ok(()) => Ok(()),
Err(_) => {
std::fs::copy(path, &target)?;
std::fs::remove_file(path)?;
Ok(())
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
+2 -2
View File
@@ -1220,8 +1220,8 @@ fn compute_composer_rect(area: Rect, top_spacer_enabled: bool) -> Rect {
if top_spacer_enabled {
y_offset = y_offset.saturating_add(1);
}
let height = (area.height - y_offset)
- BottomPane::BOTTOM_PAD_LINES.min((area.height - y_offset).saturating_sub(1));
let available = area.height.saturating_sub(y_offset);
let height = available - BottomPane::BOTTOM_PAD_LINES.min(available.saturating_sub(1));
Rect {
x: area.x + horizontal_padding,
y: area.y + y_offset,
+24 -24
View File
@@ -25821,10 +25821,10 @@ Have we met every part of this goal and is there no further work to do?"#
fn launch_chrome_with_profile(&mut self, port: u16) {
use std::process::Stdio;
let log_path = self.chrome_log_path();
#[cfg(target_os = "macos")]
{
let log_path = format!("{}/code-chrome.log", std::env::temp_dir().display());
let mut cmd = std::process::Command::new(
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
);
@@ -25838,12 +25838,10 @@ Have we met every part of this goal and is there no further work to do?"#
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
.arg("--disable-hang-monitor")
.arg("--disable-background-timer-throttling")
.arg("--enable-logging")
.arg("--log-level=1")
.arg(format!("--log-file={}", log_path))
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null());
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
tracing::warn!("failed to launch Chrome with profile: {err}");
}
@@ -25851,7 +25849,6 @@ Have we met every part of this goal and is there no further work to do?"#
#[cfg(target_os = "linux")]
{
let log_path = format!("{}/code-chrome.log", std::env::temp_dir().display());
let mut cmd = std::process::Command::new("google-chrome");
cmd.arg(format!("--remote-debugging-port={}", port))
.arg("--no-first-run")
@@ -25863,12 +25860,10 @@ Have we met every part of this goal and is there no further work to do?"#
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
.arg("--disable-hang-monitor")
.arg("--disable-background-timer-throttling")
.arg("--enable-logging")
.arg("--log-level=1")
.arg(format!("--log-file={}", log_path))
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null());
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
tracing::warn!("failed to launch Chrome with profile: {err}");
}
@@ -25876,7 +25871,6 @@ Have we met every part of this goal and is there no further work to do?"#
#[cfg(target_os = "windows")]
{
let log_path = format!("{}\\code-chrome.log", std::env::temp_dir().display());
let chrome_paths = vec![
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe".to_string(),
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe".to_string(),
@@ -25899,12 +25893,10 @@ Have we met every part of this goal and is there no further work to do?"#
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
.arg("--disable-hang-monitor")
.arg("--disable-background-timer-throttling")
.arg("--enable-logging")
.arg("--log-level=1")
.arg(format!("--log-file={}", log_path))
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null());
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
tracing::warn!("failed to launch Chrome with profile: {err}");
}
@@ -25920,6 +25912,22 @@ Have we met every part of this goal and is there no further work to do?"#
.update_status_text("using browser".to_string());
}
fn chrome_log_path(&self) -> Option<String> {
if !self.config.debug {
return None;
}
let log_dir = code_core::config::log_dir(&self.config).ok()?;
Some(log_dir.join("code-chrome.log").display().to_string())
}
fn apply_chrome_logging(&self, cmd: &mut std::process::Command, log_path: Option<&str>) {
if let Some(path) = log_path {
cmd.arg("--enable-logging")
.arg("--log-level=1")
.arg(format!("--log-file={path}"));
}
}
fn connect_to_chrome_after_launch(
&mut self,
port: u16,
@@ -26467,10 +26475,10 @@ Have we met every part of this goal and is there no further work to do?"#
let temp_dir = std::env::temp_dir();
let profile_dir = temp_dir.join(format!("code-chrome-temp-{}", port));
let log_path = self.chrome_log_path();
#[cfg(target_os = "macos")]
{
let log_path = format!("{}/code-chrome.log", std::env::temp_dir().display());
let mut cmd = std::process::Command::new(
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
);
@@ -26485,12 +26493,10 @@ Have we met every part of this goal and is there no further work to do?"#
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
.arg("--disable-hang-monitor")
.arg("--disable-background-timer-throttling")
.arg("--enable-logging")
.arg("--log-level=1")
.arg(format!("--log-file={}", log_path))
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null());
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
tracing::warn!("failed to launch Chrome with temp profile: {err}");
}
@@ -26498,7 +26504,6 @@ Have we met every part of this goal and is there no further work to do?"#
#[cfg(target_os = "linux")]
{
let log_path = format!("{}/code-chrome.log", std::env::temp_dir().display());
let mut cmd = std::process::Command::new("google-chrome");
cmd.arg(format!("--remote-debugging-port={}", port))
.arg(format!("--user-data-dir={}", profile_dir.display()))
@@ -26511,12 +26516,10 @@ Have we met every part of this goal and is there no further work to do?"#
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
.arg("--disable-hang-monitor")
.arg("--disable-background-timer-throttling")
.arg("--enable-logging")
.arg("--log-level=1")
.arg(format!("--log-file={}", log_path))
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null());
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
tracing::warn!("failed to launch Chrome with temp profile: {err}");
}
@@ -26524,7 +26527,6 @@ Have we met every part of this goal and is there no further work to do?"#
#[cfg(target_os = "windows")]
{
let log_path = format!("{}\\code-chrome.log", std::env::temp_dir().display());
let chrome_paths = vec![
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe".to_string(),
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe".to_string(),
@@ -26548,12 +26550,10 @@ Have we met every part of this goal and is there no further work to do?"#
.arg("--disable-features=ChromeWhatsNewUI,TriggerFirstRunUI")
.arg("--disable-hang-monitor")
.arg("--disable-background-timer-throttling")
.arg("--enable-logging")
.arg("--log-level=1")
.arg(format!("--log-file={}", log_path))
.stdout(Stdio::null())
.stderr(Stdio::null())
.stdin(Stdio::null());
self.apply_chrome_logging(&mut cmd, log_path.as_deref());
if let Err(err) = spawn_std_command_with_retry(&mut cmd) {
tracing::warn!("failed to launch Chrome with temp profile: {err}");
}
+74 -38
View File
@@ -131,6 +131,41 @@ pub use cli::Cli;
pub use self::markdown_render::render_markdown_text;
pub use public_widgets::composer_input::{ComposerAction, ComposerInput};
const TUI_LOG_MAX_BYTES: u64 = 50 * 1024 * 1024;
const TUI_LOG_BACKUPS: usize = 2;
fn rotate_log_file(log_dir: &Path, file_name: &str) {
let path = log_dir.join(file_name);
let Ok(meta) = std::fs::metadata(&path) else {
return;
};
if meta.len() <= TUI_LOG_MAX_BYTES {
return;
}
if TUI_LOG_BACKUPS == 0 {
let _ = std::fs::remove_file(&path);
return;
}
let oldest = log_dir.join(format!("{file_name}.{TUI_LOG_BACKUPS}"));
let _ = std::fs::remove_file(&oldest);
if TUI_LOG_BACKUPS > 1 {
for idx in (1..TUI_LOG_BACKUPS).rev() {
let from = log_dir.join(format!("{file_name}.{idx}"));
let to = log_dir.join(format!("{file_name}.{}", idx + 1));
let _ = std::fs::rename(&from, &to);
}
}
let rotated = log_dir.join(format!("{file_name}.1"));
if std::fs::rename(&path, rotated).is_err() {
if let Ok(file) = std::fs::OpenOptions::new().write(true).open(&path) {
let _ = file.set_len(0);
}
}
}
#[cfg(feature = "test-helpers")]
pub mod test_helpers {
pub use crate::chatwidget::smoke_helpers::AutoContinueModeFixture;
@@ -443,6 +478,8 @@ pub async fn run_main(
}
};
code_core::config::migrate_legacy_log_dirs(&code_home);
let housekeeping_home = code_home.clone();
let housekeeping_handle = thread_spawner::spawn_lightweight("housekeeping", move || {
if let Err(err) = code_core::run_housekeeping_if_due(&housekeeping_home) {
@@ -604,52 +641,51 @@ pub async fn run_main(
let log_dir = code_core::config::log_dir(&config)?;
std::fs::create_dir_all(&log_dir)?;
// Open (or create) your log file, appending to it.
let mut log_file_opts = OpenOptions::new();
log_file_opts.create(true).append(true);
// Ensure the file is only readable and writable by the current user.
// Doing the equivalent to `chmod 600` on Windows is quite a bit more code
// and requires the Windows API crates, so we can reconsider that when
// Code CLI is officially supported on Windows.
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
log_file_opts.mode(0o600);
}
let (env_layer, _log_guard) = if cli.debug {
rotate_log_file(&log_dir, "codex-tui.log");
// Open (or create) your log file, appending to it.
let mut log_file_opts = OpenOptions::new();
log_file_opts.create(true).append(true);
let log_file = log_file_opts.open(log_dir.join("codex-tui.log"))?;
// Ensure the file is only readable and writable by the current user.
// Doing the equivalent to `chmod 600` on Windows is quite a bit more code
// and requires the Windows API crates, so we can reconsider that when
// Code CLI is officially supported on Windows.
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
log_file_opts.mode(0o600);
}
// Wrap file in non‑blocking writer.
let (log_writer, _log_guard) = non_blocking(log_file);
let log_file = log_file_opts.open(log_dir.join("codex-tui.log"))?;
let critical_dir = {
let mut path = code_home.clone();
path.push("logs");
path
// Wrap file in non‑blocking writer.
let (log_writer, log_guard) = non_blocking(log_file);
let default_filter = "code_core=info,code_tui=info,code_browser=warn,code_auto_drive_core=info";
// use RUST_LOG env var, defaulting based on debug flag.
let env_filter = || {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter))
};
let env_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
.with_ansi(false)
.with_writer(log_writer)
.with_filter(env_filter());
(Some(env_layer), Some(log_guard))
} else {
(None, None)
};
std::fs::create_dir_all(&critical_dir)?;
let critical_dir = log_dir.clone();
let critical_appender = rolling::daily(&critical_dir, "critical.log");
let (critical_writer, _critical_guard) = non_blocking(critical_appender);
let default_filter = if cli.debug {
"code_core=info,code_tui=info,code_browser=info,code_auto_drive_core=info"
} else {
"code_core=warn,code_tui=warn,code_browser=warn"
};
// use RUST_LOG env var, defaulting based on debug flag.
let env_filter = || {
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter))
};
let env_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
.with_ansi(false)
.with_writer(log_writer)
.with_filter(env_filter());
let critical_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_ansi(false)
+23
View File
@@ -1,3 +1,4 @@
use std::sync::OnceLock;
use unicode_width::UnicodeWidthChar;
/// Sanitization mode:
@@ -26,9 +27,31 @@ pub fn sanitize_for_tui(input: &str, mode: Mode, opts: Options) -> String {
// with spaces we insert.
let mut text = if opts.expand_tabs { expand_tabs_to_spaces(input, opts.tabstop) } else { input.to_string() };
text = strip_specials(text, mode, opts.debug_markers);
if ascii_only_enabled() {
text = force_ascii(text);
}
text
}
fn ascii_only_enabled() -> bool {
static ASCII_ONLY: OnceLock<bool> = OnceLock::new();
*ASCII_ONLY.get_or_init(|| {
std::env::var("CODEX_TUI_ASCII")
.map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"))
.unwrap_or(false)
})
}
fn force_ascii(input: String) -> String {
if input.is_ascii() {
return input;
}
input
.chars()
.map(|ch| if ch.is_ascii() { ch } else { '?' })
.collect()
}
fn expand_tabs_to_spaces(input: &str, tabstop: usize) -> String {
let ts = tabstop.max(1);
let mut out = String::with_capacity(input.len());
+7 -4
View File
@@ -50,14 +50,14 @@ Notes:
Because Code is written in Rust, it honors the `RUST_LOG` environment variable to configure its logging behavior.
The TUI defaults to `RUST_LOG=code_core=info,code_tui=info,code_browser=info,code_auto_drive_core=info` and log messages are written to `~/.code/log/codex-tui.log` (Code still reads the legacy `~/.codex/log/` path), so you can leave the following running in a separate terminal to monitor log messages as they are written:
When you run the TUI with `--debug`, log messages are written to `~/.code/debug_logs/codex-tui.log`, so you can leave the following running in a separate terminal to monitor log messages as they are written:
```
tail -F ~/.code/log/codex-tui.log
tail -F ~/.code/debug_logs/codex-tui.log
```
When you also enable the CLI `--debug` flag, request/response JSON is
partitioned into helper-specific folders under `~/.code/debug_logs/`. Expect
When you enable the CLI `--debug` flag, request/response JSON is partitioned
into helper-specific folders under `~/.code/debug_logs/`. Expect
subdirectories such as:
- `auto/coordinator`
@@ -74,6 +74,9 @@ subdirectories such as:
Tags become nested path components, so custom helpers appear alongside the
existing timestamped filenames.
Without `--debug`, Code only writes critical crash/error logs to
`~/.code/debug_logs/critical.log.*`; routine log output is suppressed.
By comparison, the non-interactive mode (`code exec`) defaults to `RUST_LOG=error`, but messages are printed inline, so there is no need to monitor a separate file.
See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) for more information on the configuration options.