app-server: add JSON tracing logs (#12287)

- add `LOG_FORMAT=json` support for app-server tracing logs via
`tracing_subscriber`'s built-in JSON formatter
- keep the default human-readable format unchanged and keep `RUST_LOG`
filtering behavior
- document the env var and update lockfile
This commit is contained in:
Max Johnson
2026-02-20 10:10:51 -08:00
committed by GitHub
Unverified
parent 86803ca9bf
commit 41f15bf07b
5 changed files with 82 additions and 9 deletions
+13
View File
@@ -9530,6 +9530,16 @@ dependencies = [
"web-time",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.22"
@@ -9540,12 +9550,15 @@ dependencies = [
"nu-ansi-term",
"once_cell",
"regex-automata",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]
+1 -1
View File
@@ -50,7 +50,7 @@ tokio = { workspace = true, features = [
] }
tokio-tungstenite = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] }
uuid = { workspace = true, features = ["serde", "v7"] }
[dev-dependencies]
+5
View File
@@ -28,6 +28,11 @@ Supported transports:
Websocket transport is currently experimental and unsupported. Do not rely on it for production workloads.
Tracing/log output:
- `RUST_LOG` controls log filtering/verbosity.
- Set `LOG_FORMAT=json` to emit app-server tracing logs to `stderr` as JSON (one event per line).
Backpressure behavior:
- The server uses bounded queues between transport ingress, request processing, and outbound writes.
+62 -8
View File
@@ -49,6 +49,7 @@ use tracing::warn;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::Layer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::registry::Registry;
use tracing_subscriber::util::SubscriberInitExt;
mod bespoke_event_handling;
@@ -67,6 +68,16 @@ mod transport;
pub use crate::transport::AppServerTransport;
const LOG_FORMAT_ENV_VAR: &str = "LOG_FORMAT";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LogFormat {
Default,
Json,
}
type StderrLogLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
/// Control-plane messages from the processor/transport side to the outbound router task.
///
/// `run_main_with_transport` now uses two loops/tasks:
@@ -198,6 +209,20 @@ fn project_config_warning(config: &Config) -> Option<ConfigWarningNotification>
})
}
impl LogFormat {
fn from_env_value(value: Option<&str>) -> Self {
match value.map(str::trim).map(str::to_ascii_lowercase) {
Some(value) if value == "json" => Self::Json,
_ => Self::Default,
}
}
}
fn log_format_from_env() -> LogFormat {
let value = std::env::var(LOG_FORMAT_ENV_VAR).ok();
LogFormat::from_env_value(value.as_deref())
}
pub async fn run_main(
codex_linux_sandbox_exe: Option<PathBuf>,
cli_config_overrides: CliConfigOverrides,
@@ -342,18 +367,26 @@ pub async fn run_main_with_transport(
)
})?;
// Install a simple subscriber so `tracing` output is visible. Users can
// control the log level with `RUST_LOG`.
let stderr_fmt = tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
.with_filter(EnvFilter::from_default_env());
// Install a simple subscriber so `tracing` output is visible. Users can
// control the log level with `RUST_LOG` and switch to JSON logs with
// `LOG_FORMAT=json`.
let stderr_fmt: StderrLogLayer = match log_format_from_env() {
LogFormat::Json => tracing_subscriber::fmt::layer()
.json()
.with_writer(std::io::stderr)
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
.with_filter(EnvFilter::from_default_env())
.boxed(),
LogFormat::Default => tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::FULL)
.with_filter(EnvFilter::from_default_env())
.boxed(),
};
let feedback_layer = feedback.logger_layer();
let feedback_metadata_layer = feedback.metadata_layer();
let otel_logger_layer = otel.as_ref().and_then(|o| o.logger_layer());
let otel_tracing_layer = otel.as_ref().and_then(|o| o.tracing_layer());
let _ = tracing_subscriber::registry()
@@ -594,3 +627,24 @@ pub async fn run_main_with_transport(
Ok(())
}
#[cfg(test)]
mod tests {
use super::LogFormat;
use pretty_assertions::assert_eq;
#[test]
fn log_format_from_env_value_matches_json_values_case_insensitively() {
assert_eq!(LogFormat::from_env_value(Some("json")), LogFormat::Json);
assert_eq!(LogFormat::from_env_value(Some("JSON")), LogFormat::Json);
assert_eq!(LogFormat::from_env_value(Some(" Json ")), LogFormat::Json);
}
#[test]
fn log_format_from_env_value_defaults_for_non_json_values() {
assert_eq!(LogFormat::from_env_value(None), LogFormat::Default);
assert_eq!(LogFormat::from_env_value(Some("")), LogFormat::Default);
assert_eq!(LogFormat::from_env_value(Some("text")), LogFormat::Default);
assert_eq!(LogFormat::from_env_value(Some("jsonl")), LogFormat::Default);
}
}