Files
Michael Bolin 4f1b5a4b73 app-server: structure and test JSON shutdown logs (#30314)
## Why

`LOG_FORMAT=json` and `RUST_LOG` are supported by app-server, but the
behavior was only covered indirectly. We should verify the actual JSONL
written by both user-facing entry points: `codex app-server` and the
standalone `codex-app-server` binary.

The existing processor shutdown message also always said the channel
closed, even though the processor can exit for several different
reasons. Structured fields make that event more accurate and useful to
log consumers.

## What changed

- Record the processor `exit_reason`, remaining connection count, and
forced-shutdown state as structured tracing fields.
- Add a shared process-test helper that enables JSON logging, validates
every stderr line as JSON, and verifies the top-level timestamp is RFC
3339.
- Cover both `codex app-server` and `codex-app-server`, asserting the
stable `level`, `fields`, and `target` payload.

## Test plan

- `just test -p codex-app-server
standalone_app_server_emits_json_info_events`
- `just test -p codex-cli app_server_emits_json_info_events`
2026-06-26 18:19:56 -07:00

56 lines
1.7 KiB
Rust

use std::path::Path;
use std::process::Command;
use std::process::Stdio;
use anyhow::Context;
use anyhow::Result;
use serde_json::Value;
use serde_json::json;
pub fn app_server_json_shutdown_event(
binary: &str,
args: &[&str],
codex_home: &Path,
) -> Result<Value> {
std::fs::write(
codex_home.join("config.toml"),
"[features]\nplugins = false\n",
)?;
let output = Command::new(codex_utils_cargo_bin::cargo_bin(binary)?)
.stdin(Stdio::null())
.env("CODEX_HOME", codex_home)
.env(
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
codex_home.join("managed_config.toml"),
)
.env("LOG_FORMAT", "json")
.env("RUST_LOG", "codex_app_server=info")
.args(args)
.output()?;
let stderr = String::from_utf8(output.stderr)?;
anyhow::ensure!(output.status.success(), "app-server failed: {stderr}");
let events = stderr
.lines()
.filter(|line| !line.is_empty())
.map(serde_json::from_str::<Value>)
.collect::<serde_json::Result<Vec<_>>>()
.with_context(|| format!("app-server stderr was not JSONL: {stderr}"))?;
let event = events
.iter()
.find(|event| event["fields"]["message"] == "processor task exited")
.context("missing INFO shutdown event in app-server JSON logs")?;
let timestamp = event["timestamp"]
.as_str()
.context("shutdown event did not include a timestamp")?;
chrono::DateTime::parse_from_rfc3339(timestamp)
.with_context(|| format!("shutdown event timestamp was not RFC 3339: {timestamp}"))?;
Ok(json!({
"level": event["level"],
"fields": event["fields"],
"target": event["target"],
}))
}