From d644cc8eb2b26a9e60bc9a2459b5dc16101096ed Mon Sep 17 00:00:00 2001 From: renardeinside Date: Tue, 3 Feb 2026 15:07:37 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=90=9B=20fix:=20resolve=20invalid=20l?= =?UTF-8?q?ogging=20config=20causing=20StreamHandler=20init=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + Cargo.toml | 1 + src/common.rs | 13 +++++++++++ src/dev/process.rs | 53 +++++++++++++++++++++++++++++++++++++++++-- src/python_logging.rs | 47 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63f7dc5..6cba7df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,7 @@ dependencies = [ "schemars 1.2.0", "serde", "serde_json", + "serde_with", "serde_yaml", "similar", "sysinfo", diff --git a/Cargo.toml b/Cargo.toml index 37f8522..2cf6c9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,6 +156,7 @@ opentelemetry-proto.workspace = true prost.workspace = true similar.workspace = true tempfile.workspace = true +serde_with = { version = "3.16.1", default-features = false, features = ["macros"] } [dev-dependencies] tempfile.workspace = true diff --git a/src/common.rs b/src/common.rs index 9bc35f9..f8062f0 100644 --- a/src/common.rs +++ b/src/common.rs @@ -84,6 +84,19 @@ impl UvCommand { cmd } + /// Create a new tokio::process::Command with additional uv arguments. + /// + /// The `uv_args` are inserted between `run` and the tool name. + /// Example: `UvCommand::new("python").tokio_command_with_uv_args(&["--no-sync"])` + /// produces: `uv run --no-sync python` + pub fn tokio_command_with_uv_args(&self, uv_args: &[&str]) -> tokio::process::Command { + let mut cmd = tokio::process::Command::new("uv"); + cmd.arg("run"); + cmd.args(uv_args); + cmd.arg(self.tool); + cmd + } + /// Format the command for display/logging. pub fn display(&self) -> String { format!("uv run {}", self.tool) diff --git a/src/dev/process.rs b/src/dev/process.rs index 3b9cd1f..13f70a2 100644 --- a/src/dev/process.rs +++ b/src/dev/process.rs @@ -24,7 +24,10 @@ use crate::common::{ApxCommand, BunCommand, UvCommand, handle_spawn_error, read_ use crate::dev::common::CLIENT_HOST; use crate::dev::otel::forward_log_to_flux; use crate::dotenv::DotenvFile; -use crate::python_logging::{DevConfig, resolve_log_config}; +use crate::python_logging::{ + default_logging_config, resolve_log_config, write_logging_config_json, DevConfig, + LogConfigResult, +}; #[derive(Debug, Clone, Copy)] enum LogSource { @@ -794,7 +797,53 @@ impl ProcessManager { // Resolve uvicorn logging config (inline TOML, external Python file, or default) let log_config_result = resolve_log_config(dev_config, app_slug, app_dir).await?; - let log_config_str = log_config_result.to_string_path(); + + // Validate JSON configs before use (skip validation for Python file configs) + let log_config_str = match &log_config_result { + LogConfigResult::PythonFile(path) => path.display().to_string(), + LogConfigResult::JsonConfig(config_path) => { + // Validate the JSON config can be loaded by Python's logging.config.dictConfig + let validation_script = format!( + "import json, logging.config; logging.config.dictConfig(json.load(open('{}')))", + config_path.display() + ); + + let mut validation_cmd = + UvCommand::new("python").tokio_command_with_uv_args(&["--no-sync"]); + validation_cmd + .args(["-c", &validation_script]) + .current_dir(app_dir) + .stdout(Stdio::null()) + .stderr(Stdio::piped()); + + let output = validation_cmd + .output() + .await + .map_err(|e| format!("Failed to validate logging config: {e}"))?; + + if output.status.success() { + config_path.display().to_string() + } else { + // Validation failed - log error and fall back to default config + let stderr = String::from_utf8_lossy(&output.stderr); + warn!( + "Logging config validation failed, falling back to default:\n{}", + stderr + ); + eprintln!( + "⚠️ Custom logging config is invalid, using default config:\n{}", + stderr + ); + + // Generate and write default config + let default_config = default_logging_config(app_slug); + let fallback_path = write_logging_config_json(&default_config, app_dir) + .await + .map_err(|e| format!("Failed to write fallback logging config: {e}"))?; + fallback_path.display().to_string() + } + } + }; // Run uvicorn via uv to ensure correct Python environment let mut cmd = UvCommand::new("uvicorn").tokio_command(); diff --git a/src/python_logging.rs b/src/python_logging.rs index a1e789a..5915eed 100644 --- a/src/python_logging.rs +++ b/src/python_logging.rs @@ -7,6 +7,7 @@ //! When neither is specified, generates a default logging configuration. use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -20,6 +21,7 @@ pub struct DevConfig { } /// Python logging.dictConfig format +#[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggingConfig { pub version: i32, @@ -36,6 +38,7 @@ pub struct LoggingConfig { } /// Formatter configuration +#[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FormatterConfig { #[serde(default)] @@ -47,6 +50,7 @@ pub struct FormatterConfig { } /// Handler configuration +#[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HandlerConfig { #[serde(rename = "class")] @@ -64,6 +68,7 @@ pub struct HandlerConfig { } /// Logger configuration +#[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoggerConfig { #[serde(default)] @@ -75,6 +80,7 @@ pub struct LoggerConfig { } /// Root logger configuration +#[skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RootLoggerConfig { #[serde(default)] @@ -789,4 +795,45 @@ console = { class = "logging.StreamHandler", formatter = "custom", stream = "ext // User's myapp logger should also be present assert!(merged.loggers.contains_key("myapp")); } + + /// Test that the default logging config can be loaded by Python's logging.config.dictConfig. + /// This ensures the generated JSON is valid and doesn't contain unexpected fields like + /// `filename: null` that would cause StreamHandler to fail. + #[test] + fn test_default_config_python_validation() { + use std::io::Write; + + let config = default_logging_config("testapp"); + let json = serde_json::to_string_pretty(&config).expect("Failed to serialize config"); + + // Write config to temp file + let mut temp_file = tempfile::NamedTempFile::new().expect("Failed to create temp file"); + temp_file + .write_all(json.as_bytes()) + .expect("Failed to write config"); + let config_path = temp_file.path().to_string_lossy().to_string(); + + // Validate using Python's logging.config.dictConfig + let output = std::process::Command::new("uv") + .args([ + "run", + "--no-sync", + "python", + "-c", + &format!( + "import json, logging.config; logging.config.dictConfig(json.load(open('{}')))", + config_path + ), + ]) + .output() + .expect("Failed to run Python validation"); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!( + "Default logging config failed Python validation:\n{}\n\nConfig JSON:\n{}", + stderr, json + ); + } + } } From d90838b69284629eb5a889230e1b1cdd9cd4a2c6 Mon Sep 17 00:00:00 2001 From: renardeinside Date: Tue, 3 Feb 2026 15:41:05 +0100 Subject: [PATCH 2/2] fix uv command --- src/common.rs | 13 ------------- src/dev/process.rs | 14 +++++++------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/common.rs b/src/common.rs index f8062f0..9bc35f9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -84,19 +84,6 @@ impl UvCommand { cmd } - /// Create a new tokio::process::Command with additional uv arguments. - /// - /// The `uv_args` are inserted between `run` and the tool name. - /// Example: `UvCommand::new("python").tokio_command_with_uv_args(&["--no-sync"])` - /// produces: `uv run --no-sync python` - pub fn tokio_command_with_uv_args(&self, uv_args: &[&str]) -> tokio::process::Command { - let mut cmd = tokio::process::Command::new("uv"); - cmd.arg("run"); - cmd.args(uv_args); - cmd.arg(self.tool); - cmd - } - /// Format the command for display/logging. pub fn display(&self) -> String { format!("uv run {}", self.tool) diff --git a/src/dev/process.rs b/src/dev/process.rs index 13f70a2..83c5557 100644 --- a/src/dev/process.rs +++ b/src/dev/process.rs @@ -25,8 +25,8 @@ use crate::dev::common::CLIENT_HOST; use crate::dev::otel::forward_log_to_flux; use crate::dotenv::DotenvFile; use crate::python_logging::{ - default_logging_config, resolve_log_config, write_logging_config_json, DevConfig, - LogConfigResult, + DevConfig, LogConfigResult, default_logging_config, resolve_log_config, + write_logging_config_json, }; #[derive(Debug, Clone, Copy)] @@ -808,8 +808,7 @@ impl ProcessManager { config_path.display() ); - let mut validation_cmd = - UvCommand::new("python").tokio_command_with_uv_args(&["--no-sync"]); + let mut validation_cmd = UvCommand::new("python").tokio_command(); validation_cmd .args(["-c", &validation_script]) .current_dir(app_dir) @@ -837,9 +836,10 @@ impl ProcessManager { // Generate and write default config let default_config = default_logging_config(app_slug); - let fallback_path = write_logging_config_json(&default_config, app_dir) - .await - .map_err(|e| format!("Failed to write fallback logging config: {e}"))?; + let fallback_path = + write_logging_config_json(&default_config, app_dir) + .await + .map_err(|e| format!("Failed to write fallback logging config: {e}"))?; fallback_path.display().to_string() } }