diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index 0edb57d8a2..7374f688ef 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -529,18 +529,23 @@ pub async fn run_main_with_transport_options( } }; - let otel = codex_core::otel_init::build_provider( + let otel = match codex_core::otel_init::build_provider( &config, env!("CARGO_PKG_VERSION"), Some(OTEL_SERVICE_NAME), default_analytics_enabled, - ) - .map_err(|e| { - std::io::Error::new( - ErrorKind::InvalidData, - format!("error loading otel config: {e}"), - ) - })?; + ) { + Ok(otel) => otel, + Err(err) => { + config_warnings.push(ConfigWarningNotification { + summary: "OpenTelemetry disabled; exporter initialization failed.".to_string(), + details: Some(err.to_string()), + path: None, + range: None, + }); + None + } + }; codex_core::otel_init::record_process_start(otel.as_ref(), OTEL_SERVICE_NAME); codex_core::otel_init::install_sqlite_telemetry(otel.as_ref(), OTEL_SERVICE_NAME); let unix_socket_startup_lock = match &transport { diff --git a/codex-rs/app-server/tests/suite/v2/initialize.rs b/codex-rs/app-server/tests/suite/v2/initialize.rs index 69253e760f..f694d35b38 100644 --- a/codex-rs/app-server/tests/suite/v2/initialize.rs +++ b/codex-rs/app-server/tests/suite/v2/initialize.rs @@ -4,6 +4,7 @@ use app_test_support::create_final_assistant_message_sse_response; use app_test_support::create_mock_responses_server_sequence_unchecked; use app_test_support::to_response; use codex_app_server_protocol::ClientInfo; +use codex_app_server_protocol::ConfigWarningNotification; use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::InitializeResponse; use codex_app_server_protocol::JSONRPCMessage; @@ -62,6 +63,75 @@ async fn initialize_uses_client_info_name_as_originator() -> Result<()> { Ok(()) } +#[tokio::test] +async fn initialize_continues_when_otel_metrics_exporter_config_is_invalid() -> Result<()> { + let responses = Vec::new(); + let server = create_mock_responses_server_sequence_unchecked(responses).await; + let codex_home = TempDir::new()?; + create_config_toml_with_extra( + codex_home.path(), + &server.uri(), + "never", + r#" +[analytics] +enabled = true + +[otel] +metrics_exporter = { otlp-grpc = { endpoint = "http://127.0.0.1:4317" } } +"#, + )?; + let mut mcp = TestAppServer::new_with_env( + codex_home.path(), + &[ + ("OTEL_METRICS_EXPORTER", Some("otlp")), + ("OTEL_EXPORTER_OTLP_ENDPOINT", Some("http://127.0.0.1:4317")), + ("OTEL_EXPORTER_OTLP_PROTOCOL", Some("grpc")), + ("OTEL_EXPORTER_OTLP_COMPRESSION", Some("snappy")), + ], + ) + .await?; + + let message = timeout( + DEFAULT_READ_TIMEOUT, + mcp.initialize_with_client_info(ClientInfo { + name: "codex_vscode".to_string(), + title: Some("Codex VS Code Extension".to_string()), + version: "0.1.0".to_string(), + }), + ) + .await??; + + let JSONRPCMessage::Response(_) = message else { + anyhow::bail!("expected initialize response, got {message:?}"); + }; + + let notification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("configWarning"), + ) + .await??; + let warning: ConfigWarningNotification = serde_json::from_value( + notification + .params + .ok_or_else(|| anyhow::format_err!("config warning notification missing params"))?, + )?; + + assert_eq!( + warning.summary, + "OpenTelemetry disabled; exporter initialization failed." + ); + let details = warning + .details + .ok_or_else(|| anyhow::format_err!("config warning notification missing details"))?; + assert!( + details.contains("failed to build OTLP metrics exporter"), + "unexpected config warning details: {details}" + ); + assert_eq!(warning.path, None); + assert_eq!(warning.range, None); + Ok(()) +} + #[tokio::test] async fn initialize_probe_does_not_override_originator() -> Result<()> { let responses = Vec::new();