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
This commit is contained in:
jif
2026-08-28 16:19:15 +00:00
committed by copyberry
parent 1cc81ca89a
commit f4f85add41
2 changed files with 103 additions and 0 deletions
@@ -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<DirectorySizes> {
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)
}
+10
View File
@@ -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,