From f4f85add41288c2059dc1a4326a598f739e47fe9 Mon Sep 17 00:00:00 2001 From: jif Date: Fri, 28 Aug 2026 15:35:55 +0000 Subject: [PATCH] Measure Codex home usage at app-server startup (#41360) ## What changed - Scan regular files under `CODEX_HOME` in a background blocking task when standalone app-server metrics are enabled. - Record `codex.app_server.codex_home.size_bytes` for the entire home and the `sessions` and `archived_sessions` subdirectories. - Avoid following symlinks, discard incomplete scans, and cancel scanning during shutdown. GitOrigin-RevId: 236fc0a5c156dbd4a99e9835135de194809b4a7b --- codex-rs/app-server/src/codex_home_metrics.rs | 93 +++++++++++++++++++ codex-rs/app-server/src/lib.rs | 10 ++ 2 files changed, 103 insertions(+) create mode 100644 codex-rs/app-server/src/codex_home_metrics.rs diff --git a/codex-rs/app-server/src/codex_home_metrics.rs b/codex-rs/app-server/src/codex_home_metrics.rs new file mode 100644 index 0000000000..76308b5b58 --- /dev/null +++ b/codex-rs/app-server/src/codex_home_metrics.rs @@ -0,0 +1,93 @@ +//! Measures local CODEX_HOME storage once at standalone app-server startup. +//! +//! The background scan sums regular-file lengths, without reading file contents or +//! following symlinks. Incomplete scans emit no samples, and shutdown cancels the scan. + +use codex_otel::MetricsClient; +use codex_rollout::ARCHIVED_SESSIONS_SUBDIR; +use codex_rollout::SESSIONS_SUBDIR; +use std::io; +use std::path::Path; +use std::path::PathBuf; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; + +const SIZE_BYTES_METRIC: &str = "codex.app_server.codex_home.size_bytes"; +// Bucket sizes range from 1 MiB through 1 TiB; larger homes use the overflow bucket. +const SIZE_BYTES_BOUNDARIES: &[f64] = &[ + 1_048_576.0, + 10_485_760.0, + 104_857_600.0, + 1_073_741_824.0, + 10_737_418_240.0, + 107_374_182_400.0, + 1_099_511_627_776.0, +]; + +pub(crate) fn spawn( + codex_home: PathBuf, + metrics: MetricsClient, + shutdown: CancellationToken, +) -> JoinHandle<()> { + tokio::task::spawn_blocking(move || { + let sizes = match directory_sizes(&codex_home, &shutdown) { + Ok(sizes) => sizes, + Err(error) => { + tracing::debug!(error_kind = ?error.kind(), "Skipping CODEX_HOME size metrics"); + return; + } + }; + for (directory, bytes) in [ + ("codex_home", sizes.codex_home), + (SESSIONS_SUBDIR, sizes.sessions), + (ARCHIVED_SESSIONS_SUBDIR, sizes.archived_sessions), + ] { + let _ = metrics.histogram_with_boundaries( + SIZE_BYTES_METRIC, + i64::try_from(bytes).unwrap_or(i64::MAX), + SIZE_BYTES_BOUNDARIES, + &[("directory", directory)], + ); + } + }) +} + +#[derive(Debug, Default, PartialEq, Eq)] +struct DirectorySizes { + codex_home: u64, + sessions: u64, + archived_sessions: u64, +} + +fn directory_sizes(codex_home: &Path, shutdown: &CancellationToken) -> io::Result { + let sessions = codex_home.join(SESSIONS_SUBDIR); + let archived_sessions = codex_home.join(ARCHIVED_SESSIONS_SUBDIR); + let mut sizes = DirectorySizes::default(); + let mut pending = vec![codex_home.to_path_buf()]; + while let Some(directory) = pending.pop() { + if shutdown.is_cancelled() { + return Err(io::ErrorKind::Interrupted.into()); + } + for entry in std::fs::read_dir(directory)? { + if shutdown.is_cancelled() { + return Err(io::ErrorKind::Interrupted.into()); + } + let entry = entry?; + // DirEntry::metadata does not follow symbolic links. + let metadata = entry.metadata()?; + let path = entry.path(); + if metadata.is_dir() { + pending.push(path); + } else if metadata.is_file() { + let bytes = metadata.len(); + sizes.codex_home = sizes.codex_home.saturating_add(bytes); + if path.starts_with(&sessions) { + sizes.sessions = sizes.sessions.saturating_add(bytes); + } else if path.starts_with(&archived_sessions) { + sizes.archived_sessions = sizes.archived_sessions.saturating_add(bytes); + } + } + } + } + Ok(sizes) +} diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index fcf9ecff1b..6aa20e574d 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -96,6 +96,7 @@ mod attestation; mod auth_mode; mod bespoke_event_handling; mod code_mode_host; +mod codex_home_metrics; mod command_exec; mod config_layer; mod config_manager; @@ -821,6 +822,15 @@ pub async fn run_main_with_transport_options( } transport_accept_handles.push(remote_control_accept_handle); + // Only the standalone server measures its local home, not embedded/cloud runtimes. + if let Some(metrics) = otel.as_ref().and_then(codex_otel::OtelProvider::metrics) { + codex_home_metrics::spawn( + config.codex_home.to_path_buf(), + metrics.clone(), + transport_shutdown_token.clone(), + ); + } + let otel_reloader_handle = otel_reloader::spawn( otel, otel_logger_reload_handle,