diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index bb0daf3890..0abafe8f94 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -41,6 +41,9 @@ use owo_colors::OwoColorize; use std::io::IsTerminal; use std::path::PathBuf; use supports_color::Stream; +use tracing::Instrument; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::prelude::*; #[cfg(any(target_os = "macos", target_os = "windows"))] mod app_cmd; @@ -570,6 +573,10 @@ struct ExecServerCommand { use_agent_identity_auth: bool, } +const EXEC_SERVER_DEFAULT_ANALYTICS_ENABLED: bool = false; +const EXEC_SERVER_DEFAULT_LOG_FILTER: &str = "error,opentelemetry_sdk=off,opentelemetry_otlp=off"; +const EXEC_SERVER_OTEL_SERVICE_NAME: &str = "codex-exec-server"; + #[derive(Debug, clap::Subcommand)] #[allow(clippy::enum_variant_names)] enum AppServerSubcommand { @@ -1628,6 +1635,7 @@ async fn run_exec_server_command( .environment_id .ok_or_else(|| anyhow::anyhow!("--environment-id is required when --remote is set"))?; let config = load_exec_server_config(root_config_overrides, strict_config).await?; + let _otel = init_exec_server_tracing(Some(&config)); let auth_provider = load_exec_server_remote_auth_provider(&config, &base_url, cmd.use_agent_identity_auth) .await?; @@ -1639,25 +1647,73 @@ async fn run_exec_server_command( if let Some(name) = cmd.name { remote_config.name = name; } - codex_exec_server::run_remote_environment(remote_config, runtime_paths).await?; + codex_exec_server::run_remote_environment(remote_config, runtime_paths) + .instrument(codex_exec_server::runtime_span()) + .await?; Ok(()) } else { - if strict_config { - // Local exec-server startup does not consume Config, but strict - // mode should still reject unknown fields before opening a listener. - let _validated_config = - load_exec_server_config(root_config_overrides, strict_config).await?; - } + let config = if strict_config { + Some(load_exec_server_config(root_config_overrides, strict_config).await?) + } else { + load_exec_server_config(root_config_overrides, /*strict_config*/ false) + .await + .ok() + }; + let _otel = init_exec_server_tracing(config.as_ref()); let listen_url = cmd .listen .as_deref() .unwrap_or(codex_exec_server::DEFAULT_LISTEN_URL); codex_exec_server::run_main(listen_url, runtime_paths) + .instrument(codex_exec_server::runtime_span()) .await .map_err(anyhow::Error::from_boxed) } } +fn init_exec_server_tracing(config: Option<&codex_core::config::Config>) -> impl Send + Sync { + let fmt_layer = tracing_subscriber::fmt::layer() + .with_writer(std::io::stderr) + .with_filter(exec_server_stderr_env_filter()); + let otel = config.and_then(|config| { + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + codex_core::otel_init::build_provider( + config, + env!("CARGO_PKG_VERSION"), + Some(EXEC_SERVER_OTEL_SERVICE_NAME), + EXEC_SERVER_DEFAULT_ANALYTICS_ENABLED, + ) + })) { + Ok(Ok(otel)) => otel, + Ok(Err(err)) => { + eprintln!("Could not create otel exporter: {err}"); + None + } + Err(_) => { + eprintln!("Could not create otel exporter: panicked during initialization"); + None + } + } + }); + codex_core::otel_init::record_process_start(otel.as_ref(), EXEC_SERVER_OTEL_SERVICE_NAME); + + let otel_logger_layer = otel.as_ref().and_then(|otel| otel.logger_layer()); + let otel_tracing_layer = otel.as_ref().and_then(|otel| otel.tracing_layer()); + let _ = tracing_subscriber::registry() + .with(fmt_layer) + .with(otel_tracing_layer) + .with(otel_logger_layer) + .try_init(); + tracing::callsite::rebuild_interest_cache(); + otel +} + +fn exec_server_stderr_env_filter() -> EnvFilter { + EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new(EXEC_SERVER_DEFAULT_LOG_FILTER)) + .unwrap_or_else(|_| EnvFilter::new("error")) +} + async fn load_exec_server_remote_auth_provider( config: &codex_core::config::Config, base_url: &str, diff --git a/codex-rs/cli/tests/exec_server.rs b/codex-rs/cli/tests/exec_server.rs index 0afe0d37b5..5e412a9de1 100644 --- a/codex-rs/cli/tests/exec_server.rs +++ b/codex-rs/cli/tests/exec_server.rs @@ -1,6 +1,7 @@ use std::path::Path; use anyhow::Result; +use predicates::prelude::PredicateBooleanExt; use predicates::str::contains; use tempfile::TempDir; @@ -33,3 +34,17 @@ foo = "bar" Ok(()) } + +#[test] +fn local_exec_server_ignores_invalid_config_without_strict_config() -> Result<()> { + let codex_home = TempDir::new()?; + std::fs::write(codex_home.path().join("config.toml"), "not valid toml = [")?; + + let mut cmd = codex_command(codex_home.path())?; + cmd.args(["exec-server", "--listen", "stdio"]) + .assert() + .success() + .stderr(contains("not valid toml").not()); + + Ok(()) +} diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index e306ec395e..b86988c2c8 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -23,6 +23,7 @@ mod rpc; mod runtime_paths; mod sandboxed_file_system; mod server; +mod telemetry; pub use client::ExecServerClient; pub use client::ExecServerError; @@ -107,3 +108,4 @@ pub use runtime_paths::ExecServerRuntimePaths; pub use server::DEFAULT_LISTEN_URL; pub use server::ExecServerListenUrlParseError; pub use server::run_main; +pub use telemetry::runtime_span; diff --git a/codex-rs/exec-server/src/telemetry.rs b/codex-rs/exec-server/src/telemetry.rs new file mode 100644 index 0000000000..d6741e7efb --- /dev/null +++ b/codex-rs/exec-server/src/telemetry.rs @@ -0,0 +1,3 @@ +pub fn runtime_span() -> tracing::Span { + tracing::info_span!("codex.exec_server", otel.kind = "internal") +}