tui: make codex-tui.log opt-in (#24081)

## Why

The TUI currently creates a shared plaintext `codex-tui.log` under the
default log directory. That append-only file can keep growing across
runs even though the TUI already records diagnostics in bounded local
stores.

Make the plaintext file log an explicit troubleshooting choice instead
of a default side effect.

This is possible because logs are also stored in the DB with proper
rotation

## What changed

- Only install the TUI file logging layer when `log_dir` is explicitly
set.
- Remove the prior `codex-tui.log` at startup before an opt-in file
layer is created.
- Clarify the `log_dir` config/schema text and `docs/install.md` example
so users opt in with `codex -c log_dir=...` when they need a plaintext
log.
This commit is contained in:
jif-oai
2026-05-22 19:19:51 +02:00
committed by GitHub
Unverified
parent dac98cb635
commit f55f864b9f
5 changed files with 67 additions and 43 deletions
+2 -1
View File
@@ -319,7 +319,8 @@ pub struct ConfigToml {
/// Defaults to `$CODEX_SQLITE_HOME` when set. Otherwise uses `$CODEX_HOME`.
pub sqlite_home: Option<AbsolutePathBuf>,
/// Directory where Codex writes log files, for example `codex-tui.log`.
/// Directory where Codex writes log files. Setting this value explicitly
/// also enables the TUI text log in this directory.
/// Defaults to `$CODEX_HOME/log`.
pub log_dir: Option<AbsolutePathBuf>,
+1 -1
View File
@@ -4654,7 +4654,7 @@
"$ref": "#/definitions/AbsolutePathBuf"
}
],
"description": "Directory where Codex writes log files, for example `codex-tui.log`. Defaults to `$CODEX_HOME/log`."
"description": "Directory where Codex writes log files. Setting this value explicitly also enables the TUI text log in this directory. Defaults to `$CODEX_HOME/log`."
},
"marketplaces": {
"additionalProperties": {
+56 -38
View File
@@ -278,6 +278,8 @@ pub use public_widgets::composer_input::ComposerAction;
pub use public_widgets::composer_input::ComposerInput;
// (tests access modules directly within the crate)
const TUI_LOG_FILE_NAME: &str = "codex-tui.log";
#[cfg(unix)]
const AUTO_CONNECT_DAEMON_CONNECT_TIMEOUT: std::time::Duration =
std::time::Duration::from_millis(50);
@@ -349,6 +351,13 @@ async fn init_state_db_for_app_server_target(
}
}
// TODO(jif) delete after 22/11/2026.
fn remove_legacy_tui_log_file(codex_home: &Path) {
// Shared append-only TUI logs could grow without bound. Existing processes
// may still hold the file open, so startup cleanup is best effort.
let _ = std::fs::remove_file(codex_home.join("log").join(TUI_LOG_FILE_NAME));
}
fn remote_addr_has_explicit_port(addr: &str, parsed: &Url) -> bool {
let Some(host) = parsed.host_str() else {
return false;
@@ -1055,6 +1064,8 @@ pub async fn run_main(
)
.await;
remove_legacy_tui_log_file(config.codex_home.as_path());
let otel_originator = originator().value;
let otel = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
crate::legacy_core::otel_init::build_provider(
@@ -1166,47 +1177,40 @@ pub async fn run_main(
}
}
let log_dir = config.log_dir.clone();
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);
let (tui_file_layer, _tui_file_log_guard) = if config_toml.log_dir.is_some() {
let log_dir = config.log_dir.clone();
std::fs::create_dir_all(&log_dir)?;
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
// Codex CLI is officially supported on Windows.
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
log_file_opts.mode(0o600);
}
// 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.
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
log_file_opts.mode(0o600);
}
let log_file = log_file_opts.open(log_dir.join("codex-tui.log"))?;
// Wrap file in nonblocking writer.
let (non_blocking, _guard) = non_blocking(log_file);
// use RUST_LOG env var, default to info for codex crates.
let env_filter = || {
EnvFilter::try_from_default_env().unwrap_or_else(|_| {
let log_file = log_file_opts.open(log_dir.join(TUI_LOG_FILE_NAME))?;
let (non_blocking, guard) = non_blocking(log_file);
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
EnvFilter::new("codex_core=info,codex_tui=info,codex_rmcp_client=info")
})
});
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_target(true)
.with_ansi(false)
.with_span_events(
tracing_subscriber::fmt::format::FmtSpan::NEW
| tracing_subscriber::fmt::format::FmtSpan::CLOSE,
)
.with_filter(env_filter);
(Some(file_layer), Some(guard))
} else {
(None, None)
};
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
// `with_target(true)` is the default, but we previously disabled it for file output.
// Keep it enabled so we can selectively enable targets via `RUST_LOG=...` and then
// grep for a specific module/target while troubleshooting.
.with_target(true)
.with_ansi(false)
.with_span_events(
tracing_subscriber::fmt::format::FmtSpan::NEW
| tracing_subscriber::fmt::format::FmtSpan::CLOSE,
)
.with_filter(env_filter());
let feedback = codex_feedback::CodexFeedback::new();
let feedback_layer = feedback.logger_layer();
let feedback_metadata_layer = feedback.metadata_layer();
@@ -1236,7 +1240,7 @@ pub async fn run_main(
.map(|layer| layer.with_filter(Targets::new().with_default(Level::TRACE)));
let _ = tracing_subscriber::registry()
.with(file_layer)
.with(tui_file_layer)
.with(feedback_layer)
.with(feedback_metadata_layer)
.with(log_db_layer)
@@ -1915,6 +1919,20 @@ mod tests {
.await
}
#[test]
fn startup_removes_legacy_tui_log_file() -> std::io::Result<()> {
let temp_dir = TempDir::new()?;
let legacy_log_dir = temp_dir.path().join("log");
std::fs::create_dir_all(&legacy_log_dir)?;
let legacy_log = legacy_log_dir.join(TUI_LOG_FILE_NAME);
std::fs::write(&legacy_log, "legacy log")?;
remove_legacy_tui_log_file(temp_dir.path());
assert!(!legacy_log.exists());
Ok(())
}
async fn start_test_embedded_app_server(
config: Config,
) -> color_eyre::Result<InProcessAppServerClient> {
@@ -2364,7 +2382,7 @@ mod tests {
let updated_at =
chrono::DateTime::parse_from_rfc3339(meta_rfc3339)?.with_timezone(&chrono::Utc);
let times = std::fs::FileTimes::new().set_modified(updated_at.into());
OpenOptions::new()
std::fs::OpenOptions::new()
.append(true)
.open(rollout_path)?
.set_times(times)?;
+4
View File
@@ -30,6 +30,10 @@ impl SessionLogger {
let mut opts = OpenOptions::new();
opts.create(true).truncate(true).write(true);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
+4 -3
View File
@@ -51,12 +51,13 @@ just test
Codex is written in Rust, so it honors the `RUST_LOG` environment variable to configure its logging behavior.
The TUI defaults to `RUST_LOG=codex_core=info,codex_tui=info,codex_rmcp_client=info` and log messages are written to `~/.codex/log/codex-tui.log` by default. For a single run, you can override the log directory with `-c log_dir=...` (for example, `-c log_dir=./.codex-log`).
The TUI records diagnostics in bounded local stores by default. Set `log_dir` explicitly to enable a plaintext TUI log for a run:
```bash
tail -F ~/.codex/log/codex-tui.log
codex -c log_dir=./.codex-log
tail -F ./.codex-log/codex-tui.log
```
By comparison, the non-interactive mode (`codex exec`) defaults to `RUST_LOG=error`, but messages are printed inline, so there is no need to monitor a separate file.
The non-interactive mode (`codex 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.