From 44ff9fcb69ec687fcf9f3dfdb6f22cf2594f531b Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Mon, 24 Nov 2025 19:18:20 -0800 Subject: [PATCH] review --- codex-rs/cli/src/status.rs | 103 ++++------ codex-rs/status/Cargo.toml | 1 + codex-rs/status/src/lib.rs | 389 +++++++++++++++++++++---------------- 3 files changed, 261 insertions(+), 232 deletions(-) diff --git a/codex-rs/cli/src/status.rs b/codex-rs/cli/src/status.rs index c40b933ccc..f603d4e49a 100644 --- a/codex-rs/cli/src/status.rs +++ b/codex-rs/cli/src/status.rs @@ -1,5 +1,5 @@ use clap::Args; -use codex_status::CodexStatusReport; +use codex_status::CodexStatus; use codex_status::StatusClient; use std::io::Write; use std::io::{self}; @@ -13,46 +13,27 @@ pub struct StatusCommand { pub async fn run_status(cmd: StatusCommand) -> anyhow::Result<()> { let client = StatusClient::new()?; - let report = client.fetch_codex_status().await?; + let codex_status = client.fetch_codex_status().await?; if cmd.json { - let json = serde_json::to_string_pretty(&report)?; + let json = serde_json::to_string_pretty(&codex_status)?; println!("{json}"); } else { - write_human(&report, &mut io::stdout())?; + write_human(&codex_status, &mut io::stdout())?; } Ok(()) } -fn write_human(report: &CodexStatusReport, writer: &mut W) -> anyhow::Result<()> { - writeln!( - writer, - "overall: {} ({})", - report.overall_description, report.overall_indicator - )?; - writeln!(writer, "updated_at: {}", report.updated_at)?; +fn write_human(status: &CodexStatus, writer: &mut W) -> anyhow::Result<()> { + writeln!(writer, "Codex status: {}", status.status)?; + writeln!(writer, "operational: {}", status.is_operational)?; + writeln!(writer, "component_id: {}", status.component_id)?; - writeln!(writer, "codex components:")?; - if report.components.is_empty() { - writeln!(writer, " none")?; + if let Some(affected) = &status.raw_affected { + writeln!(writer, "affected status: {}", affected.status)?; } else { - for component in &report.components { - writeln!(writer, " {}: {}", component.name, component.status)?; - } - } - - writeln!(writer, "codex incidents:")?; - if report.incidents.is_empty() { - writeln!(writer, " none")?; - } else { - for incident in &report.incidents { - writeln!( - writer, - " {}: status={} impact={} updated={}", - incident.name, incident.status, incident.impact, incident.updated_at - )?; - } + writeln!(writer, "affected status: none")?; } Ok(()) @@ -61,56 +42,50 @@ fn write_human(report: &CodexStatusReport, writer: &mut W) -> anyhow:: #[cfg(test)] mod tests { use super::*; - use codex_status::ComponentStatus; - use codex_status::IncidentStatus; + use codex_status::AffectedComponent; + use codex_status::ComponentHealth; #[test] - fn human_output_handles_empty_lists() -> anyhow::Result<()> { - let report = CodexStatusReport { - overall_description: "All Systems Operational".to_string(), - overall_indicator: "none".to_string(), - updated_at: "2025-11-07T21:55:20Z".to_string(), - components: Vec::new(), - incidents: Vec::new(), + fn human_output_handles_absent_affected_entry() -> anyhow::Result<()> { + let status = CodexStatus { + component_id: "cmp-1".to_string(), + name: "Codex".to_string(), + status: ComponentHealth::Operational, + is_operational: true, + raw_affected: None, }; let mut buffer = Vec::new(); - write_human(&report, &mut buffer)?; + write_human(&status, &mut buffer)?; let output = String::from_utf8(buffer).expect("utf8"); - assert!(output.contains("overall: All Systems Operational (none)")); - assert!(output.contains("updated_at: 2025-11-07T21:55:20Z")); - assert!(output.contains("codex components:\n none")); - assert!(output.contains("codex incidents:\n none")); + assert!(output.contains("Codex status: operational")); + assert!(output.contains("operational: true")); + assert!(output.contains("component_id: cmp-1")); + assert!(output.contains("affected status: none")); Ok(()) } #[test] - fn human_output_lists_components_and_incidents() -> anyhow::Result<()> { - let report = CodexStatusReport { - overall_description: "Degraded".to_string(), - overall_indicator: "minor".to_string(), - updated_at: "2025-11-07T21:55:20Z".to_string(), - components: vec![ComponentStatus { - name: "Codex".to_string(), - status: "degraded_performance".to_string(), - }], - incidents: vec![IncidentStatus { - name: "Codex degraded".to_string(), - status: "investigating".to_string(), - impact: "minor".to_string(), - updated_at: "2025-11-07T21:45:00Z".to_string(), - }], + fn human_output_shows_affected_status() -> anyhow::Result<()> { + let status = CodexStatus { + component_id: "cmp-1".to_string(), + name: "Codex".to_string(), + status: ComponentHealth::DegradedPerformance, + is_operational: false, + raw_affected: Some(AffectedComponent { + component_id: "cmp-1".to_string(), + status: ComponentHealth::DegradedPerformance, + }), }; let mut buffer = Vec::new(); - write_human(&report, &mut buffer)?; + write_human(&status, &mut buffer)?; let output = String::from_utf8(buffer).expect("utf8"); - assert!(output.contains("codex components:\n Codex: degraded_performance")); - assert!(output.contains( - "codex incidents:\n Codex degraded: status=investigating impact=minor updated=2025-11-07T21:45:00Z" - )); + assert!(output.contains("Codex status: degraded_performance")); + assert!(output.contains("operational: false")); + assert!(output.contains("affected status: degraded_performance")); Ok(()) } } diff --git a/codex-rs/status/Cargo.toml b/codex-rs/status/Cargo.toml index 5a17295767..28aaad1278 100644 --- a/codex-rs/status/Cargo.toml +++ b/codex-rs/status/Cargo.toml @@ -11,6 +11,7 @@ workspace = true anyhow = { workspace = true } reqwest = { workspace = true, features = ["json"] } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/status/src/lib.rs b/codex-rs/status/src/lib.rs index edfa715156..1aa76f4f5a 100644 --- a/codex-rs/status/src/lib.rs +++ b/codex-rs/status/src/lib.rs @@ -1,47 +1,103 @@ -use anyhow::Context; -use reqwest::Url; -use serde::Deserialize; -use serde::Serialize; +use anyhow::{bail, Context, Result}; +use reqwest::{header::CONTENT_TYPE, Url}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::env; +use std::fmt; use std::time::Duration; -pub const STATUS_SUMMARY_URL: &str = "https://status.openai.com/api/v2/summary.json"; +pub const DEFAULT_STATUS_WIDGET_URL: &str = "https://status.openai.com/proxy/status.openai.com"; +pub const STATUS_WIDGET_ENV_VAR: &str = "STATUS_WIDGET_URL"; +pub const CODEX_COMPONENT_NAME: &str = "Codex"; -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct CodexStatusReport { - pub overall_description: String, - pub overall_indicator: String, - pub updated_at: String, - pub components: Vec, - pub incidents: Vec, +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ComponentHealth { + Operational, + DegradedPerformance, + PartialOutage, + MajorOutage, + UnderMaintenance, + #[serde(other)] + Unknown, +} + +impl ComponentHealth { + fn operational() -> Self { + Self::Operational + } + + pub fn is_operational(self) -> bool { + self == Self::Operational + } + +} + +impl fmt::Display for ComponentHealth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = Value::from(self) + .as_str() + .ok_or(fmt::Error)?; + + f.write_str(value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AffectedComponent { + pub component_id: String, + #[serde(default = "ComponentHealth::operational")] + pub status: ComponentHealth, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Component { + pub id: String, + pub name: String, + pub status_page_id: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Summary { + pub id: String, + pub name: String, + #[serde(default)] + pub components: Vec, + #[serde(default)] + pub affected_components: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct StatusPayload { + pub summary: Summary, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct ComponentStatus { +pub struct CodexStatus { + pub component_id: String, pub name: String, - pub status: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct IncidentStatus { - pub name: String, - pub status: String, - pub impact: String, - pub updated_at: String, + pub status: ComponentHealth, + pub is_operational: bool, + pub raw_affected: Option, } #[derive(Debug, Clone)] pub struct StatusClient { client: reqwest::Client, - summary_url: Url, + widget_url: Url, } impl StatusClient { - pub fn new() -> anyhow::Result { - Self::with_summary_url(Url::parse(STATUS_SUMMARY_URL)?) + pub fn new() -> Result { + let widget_url = env::var(STATUS_WIDGET_ENV_VAR) + .unwrap_or_else(|_| DEFAULT_STATUS_WIDGET_URL.to_string()); + Self::with_widget_url(Url::parse(&widget_url)?) } - pub fn with_summary_url(summary_url: Url) -> anyhow::Result { - let user_agent = format!("codex-status/{}", env!("CARGO_PKG_VERSION")); + pub fn with_widget_url(widget_url: Url) -> Result { + let version = env!("CARGO_PKG_VERSION"); + let user_agent = format!("codex-status/{version}"); + let client = reqwest::Client::builder() .user_agent(user_agent) .connect_timeout(Duration::from_secs(5)) @@ -49,125 +105,83 @@ impl StatusClient { .build() .context("building HTTP client")?; - Ok(Self { - client, - summary_url, - }) + Ok(Self { client, widget_url }) } - pub async fn fetch_codex_status(&self) -> anyhow::Result { + pub async fn fetch_status_payload(&self) -> Result { let response = self .client - .get(self.summary_url.clone()) + .get(self.widget_url.clone()) .send() .await - .context("requesting status summary")?; - - let summary: StatusSummary = response + .context("requesting status widget")? .error_for_status() - .context("status summary returned error")? - .json() + .context("status widget returned error")?; + + let content_type = response + .headers() + .get(CONTENT_TYPE) + .and_then(|value| value.to_str().ok()) + .unwrap_or_default() + .to_ascii_lowercase(); + + if !content_type.contains("json") { + let snippet = response + .text() + .await + .unwrap_or_default() + .chars() + .take(200) + .collect::(); + + let url = &self.widget_url; + bail!( + "Expected JSON from {url}, got Content-Type={content_type}. Body starts with: {snippet:?}" + ); + } + + response + .json::() .await - .context("parsing status summary JSON")?; + .context("parsing status widget JSON") + } - Ok(CodexStatusReport::from_summary(summary)) + pub async fn fetch_codex_status(&self) -> Result { + let payload = self.fetch_status_payload().await?; + derive_component_status(&payload, CODEX_COMPONENT_NAME) } } -impl CodexStatusReport { - fn from_summary(summary: StatusSummary) -> Self { - let components = summary - .components - .into_iter() - .filter(|component| is_codex_name(&component.name)) - .map(ComponentStatus::from) - .collect(); +pub fn derive_component_status(payload: &StatusPayload, component_name: &str) -> Result { + let component = payload + .summary + .components + .iter() + .find(|component| component.name == component_name) + .cloned() + .ok_or_else(|| anyhow::anyhow!("Component {component_name:?} not found in status summary"))?; - let incidents = summary - .incidents - .into_iter() - .filter(|incident| is_codex_name(&incident.name)) - .map(IncidentStatus::from) - .collect(); + let affected = payload + .summary + .affected_components + .iter() + .find(|affected| affected.component_id == component.id) + .cloned(); - CodexStatusReport { - overall_description: summary.status.description, - overall_indicator: summary.status.indicator, - updated_at: summary.page.updated_at, - components, - incidents, - } - } -} + let status = affected + .as_ref() + .map(|affected| affected.status) + .unwrap_or(ComponentHealth::Operational); -fn is_codex_name(name: &str) -> bool { - name.to_ascii_lowercase().contains("codex") -} + let is_operational = status.is_operational(); -#[derive(Debug, Deserialize)] -struct StatusSummary { - #[serde(default)] - page: Page, - #[serde(default)] - status: OverallStatus, - #[serde(default)] - components: Vec, - #[serde(default)] - incidents: Vec, -} - -#[derive(Debug, Deserialize, Default)] -struct Page { - #[serde(default)] - updated_at: String, -} - -#[derive(Debug, Deserialize, Default)] -struct OverallStatus { - #[serde(default)] - indicator: String, - #[serde(default)] - description: String, -} - -#[derive(Debug, Deserialize)] -struct Component { - name: String, - status: String, -} - -#[derive(Debug, Deserialize)] -struct Incident { - name: String, - status: String, - #[serde(default = "default_impact")] - impact: String, - #[serde(default)] - updated_at: String, -} - -fn default_impact() -> String { - "unknown".to_string() -} - -impl From for ComponentStatus { - fn from(value: Component) -> Self { - Self { - name: value.name, - status: value.status, - } - } -} - -impl From for IncidentStatus { - fn from(value: Incident) -> Self { - Self { - name: value.name, - status: value.status, - impact: value.impact, - updated_at: value.updated_at, - } - } + Ok(CodexStatus { + component_id: component.id, + name: component.name, + status, + is_operational, + raw_affected: affected, + }) } #[cfg(test)] @@ -177,54 +191,93 @@ mod tests { use serde_json::json; #[test] - fn filters_non_codex_components_and_incidents() { - let summary = serde_json::from_value::(json!({ - "page": {"updated_at": "2025-11-07T21:55:20Z"}, - "status": {"description": "All Systems Operational", "indicator": "none"}, - "components": [ - {"name": "Codex", "status": "operational"}, - {"name": "Chat Completions", "status": "operational"} - ], - "incidents": [ - {"name": "Codex degraded performance", "status": "investigating", "impact": "minor", "updated_at": "2025-11-07T21:50:00Z"}, - {"name": "Chat downtime", "status": "resolved", "impact": "critical", "updated_at": "2025-11-07T21:00:00Z"} - ] + fn defaults_to_operational_when_not_affected() { + let payload = serde_json::from_value::(json!({ + "summary": { + "id": "sum-1", + "name": "OpenAI", + "components": [ + {"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"}, + {"id": "cmp-2", "name": "Chat", "status_page_id": "page-1"} + ] + } })) - .expect("valid summary"); + .expect("valid payload"); - let report = CodexStatusReport::from_summary(summary); + let status = derive_component_status(&payload, "Codex").expect("codex component exists"); - assert_eq!(report.overall_description, "All Systems Operational"); - assert_eq!(report.overall_indicator, "none"); - assert_eq!(report.updated_at, "2025-11-07T21:55:20Z"); - assert_eq!(report.components.len(), 1); - assert_eq!(report.components[0].name, "Codex"); - assert_eq!(report.components[0].status, "operational"); - assert_eq!(report.incidents.len(), 1); - assert_eq!(report.incidents[0].name, "Codex degraded performance"); - assert_eq!(report.incidents[0].status, "investigating"); - assert_eq!(report.incidents[0].impact, "minor"); - assert_eq!(report.incidents[0].updated_at, "2025-11-07T21:50:00Z"); + assert_eq!(status.status, ComponentHealth::Operational); + assert!(status.is_operational); + assert!(status.raw_affected.is_none()); } #[test] - fn handles_missing_fields_with_defaults() { - let summary = - serde_json::from_value::(json!({})).expect("valid empty summary"); + fn uses_affected_component_status() { + let payload = serde_json::from_value::(json!({ + "summary": { + "id": "sum-1", + "name": "OpenAI", + "components": [ + {"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"} + ], + "affected_components": [ + {"component_id": "cmp-1", "status": "major_outage"} + ] + } + })) + .expect("valid payload"); - let report = CodexStatusReport::from_summary(summary); + let status = derive_component_status(&payload, "Codex").expect("codex component exists"); - assert_eq!(report.overall_description, ""); - assert_eq!(report.overall_indicator, ""); - assert_eq!(report.updated_at, ""); - assert!(report.components.is_empty()); - assert!(report.incidents.is_empty()); + assert_eq!(status.status, ComponentHealth::MajorOutage); + assert!(!status.is_operational); + assert_eq!( + status + .raw_affected + .as_ref() + .map(|affected| affected.status), + Some(ComponentHealth::MajorOutage) + ); } #[test] - fn is_codex_name_matches_case_insensitive() { - assert!(is_codex_name("Codex")); - assert!(is_codex_name("my-codex-component")); - assert!(!is_codex_name("Chat")); + fn unknown_status_is_preserved_as_unknown() { + let payload = serde_json::from_value::(json!({ + "summary": { + "id": "sum-1", + "name": "OpenAI", + "components": [ + {"id": "cmp-1", "name": "Codex", "status_page_id": "page-1"} + ], + "affected_components": [ + {"component_id": "cmp-1", "status": "custom_status"} + ] + } + })) + .expect("valid payload"); + + let status = derive_component_status(&payload, "Codex").expect("codex component exists"); + + assert_eq!(status.status, ComponentHealth::Unknown); + assert!(!status.is_operational); + } + + #[test] + fn missing_component_returns_error() { + let payload = serde_json::from_value::(json!({ + "summary": { + "id": "sum-1", + "name": "OpenAI", + "components": [], + "affected_components": [] + } + })) + .expect("valid payload"); + + let error = derive_component_status(&payload, "Codex").expect_err("missing component should error"); + + assert!(error + .to_string() + .contains("Component \"Codex\" not found in status summary")); } }