Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
63 changes: 63 additions & 0 deletions docs/content/docs/reference/config-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,66 @@ This allows you to run:
```bash
apx add @animate-ui/fade-in
```

### `[tool.apx.dev]`

Development server configuration options.

- **log_config_file**: Path to an external Python logging configuration file (relative to pyproject.toml). Mutually exclusive with `[tool.apx.dev.logging]`.

Example:

```toml
[tool.apx.dev]
log_config_file = "logging_config.py"
```

### `[tool.apx.dev.logging]`

Inline Python logging configuration using the standard [logging.dictConfig](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) format. This configuration is **merged** with the default uvicorn logging setup, allowing you to add custom loggers or override specific settings while preserving the standard uvicorn logging behavior.

When you specify loggers, formatters, or handlers, they are merged with the defaults:

- New entries are added
- Existing entries with the same name are overridden

Example with inline tables:

```toml
[tool.apx.dev.logging]
version = 1
disable_existing_loggers = false

[tool.apx.dev.logging.formatters]
default = { format = "%(levelname)s %(name)s %(message)s" }

[tool.apx.dev.logging.handlers]
console = { class = "logging.StreamHandler", formatter = "default", stream = "ext://sys.stdout" }

[tool.apx.dev.logging.loggers]
"uvicorn" = { level = "DEBUG", handlers = ["console"], propagate = false }
"myapp" = { level = "DEBUG", handlers = ["console"], propagate = false }
```

**Default loggers provided by apx:**

| Logger | Level | Description |
| ---------------- | ----- | ------------------------- |
| `uvicorn` | INFO | Main uvicorn logger |
| `uvicorn.error` | INFO | Uvicorn error logger |
| `uvicorn.access` | INFO | HTTP access logs |
| `{app_slug}` | DEBUG | Your application's logger |

**Configuration options:**

- **version**: Must be `1` (required by Python's dictConfig)
- **disable_existing_loggers**: Whether to disable existing loggers (default: `false`)
- **formatters**: Log message formatters
- **handlers**: Output handlers (console, file, etc.)
- **loggers**: Logger configurations by name
- **root**: Root logger configuration

<Callout type="info">
You cannot use both `log_config_file` and `[tool.apx.dev.logging]` at the same
time.
</Callout>
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@
"@opentelemetry/exporter-logs-otlp-http": "^0.211.0",
"@opentelemetry/resources": "^2.5.0",
"@opentelemetry/sdk-logs": "^0.211.0",
"@tailwindcss/vite": "^4.1.15",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.20",
"@tanstack/react-router": "^1.157.16",
"@tanstack/router-plugin": "^1.133.21",
"@tanstack/react-router": "^1.157.18",
"@tanstack/router-plugin": "^1.157.18",
"@types/bun": "latest",
"@types/node": "^24.7.2",
"@vitejs/plugin-react": "^5.0.4",
"axios": "^1.13.1",
"react": "^19.2.0",
"smol-toml": "^1.4.2",
"@types/node": "^24.10.9",
"@vitejs/plugin-react": "^5.1.3",
"axios": "^1.13.4",
"react": "^19.2.4",
"smol-toml": "^1.6.0",
"typescript": "^5.9.3",
"vite": "^7.1.9"
"vite": "^7.3.1"
},
"peerDependencies": {
"typescript": "^5.9.3"
Expand Down
6 changes: 6 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tokio::process::Command;

use crate::bun_binary_path;
use crate::generate_openapi;
use crate::python_logging::{DevConfig, parse_dev_config};

/// Dev dependencies required by apx frontend entrypoint.ts
/// These must be installed before running any frontend command
Expand Down Expand Up @@ -201,6 +202,7 @@ pub struct ProjectMetadata {
pub metadata_path: PathBuf,
pub ui_root: PathBuf,
pub ui_registries: HashMap<String, String>,
pub dev_config: DevConfig,
}

impl ProjectMetadata {
Expand Down Expand Up @@ -261,6 +263,9 @@ pub fn read_project_metadata(project_root: &Path) -> Result<ProjectMetadata, Str
})
.unwrap_or_default();

// Parse dev configuration
let dev_config = parse_dev_config(&pyproject_value, project_root)?;

Ok(ProjectMetadata {
app_name,
app_slug,
Expand All @@ -269,6 +274,7 @@ pub fn read_project_metadata(project_root: &Path) -> Result<ProjectMetadata, Str
metadata_path: PathBuf::from(metadata_path),
ui_root: PathBuf::from(ui_root),
ui_registries,
dev_config,
})
}

Expand Down
85 changes: 14 additions & 71 deletions src/dev/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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};

#[derive(Debug, Clone, Copy)]
enum LogSource {
Expand Down Expand Up @@ -70,6 +71,7 @@ pub struct ProcessManager {
app_slug: String,
app_entrypoint: String,
dotenv_vars: Arc<Mutex<HashMap<String, String>>>,
dev_config: DevConfig,
}

impl ProcessManager {
Expand All @@ -90,6 +92,7 @@ impl ProcessManager {
let dotenv_vars = Arc::new(Mutex::new(dotenv.get_vars()));
let app_slug = metadata.app_slug.clone();
let app_entrypoint = metadata.app_entrypoint.clone();
let dev_config = metadata.dev_config.clone();

let dev_token = Self::generate_dev_token();
let db_password = Self::generate_dev_token(); // Random password for PGlite
Expand Down Expand Up @@ -119,6 +122,7 @@ impl ProcessManager {
app_slug,
app_entrypoint,
dotenv_vars,
dev_config,
})
}

Expand Down Expand Up @@ -301,8 +305,10 @@ impl ProcessManager {
// 2026-01-28 14:09:02.413 | app | INFO: Uvicorn running...
// ============================================================================

// Create uvicorn logging config for consistent log format
let log_config = self.create_uvicorn_log_config(app_dir).await?;
// Resolve uvicorn logging config (inline TOML, external Python file, or default)
let log_config_result =
resolve_log_config(&self.dev_config, &self.app_slug, app_dir).await?;
let log_config = log_config_result.to_string_path();

// Run uvicorn via uv to ensure correct Python environment
let mut cmd = UvCommand::new("uvicorn").tokio_command();
Expand Down Expand Up @@ -377,72 +383,6 @@ impl ProcessManager {
Ok(())
}

/// Create a uvicorn logging config file (JSON format, no pyyaml dependency).
/// Always overwrites the existing config to ensure format updates are applied.
async fn create_uvicorn_log_config(&self, app_dir: &Path) -> Result<String, String> {
let config_dir = app_dir.join(".apx");
tokio::fs::create_dir_all(&config_dir)
.await
.map_err(|e| format!("Failed to create .apx directory: {e}"))?;

let config_path = config_dir.join("uvicorn_logging.json");
// APX adds: timestamp | source | channel | <this output>
// So we only need: location | message
//
// IMPORTANT: Uvicorn's access logger passes values as positional args, not named fields.
// Use %(message)s to get the pre-formatted message, not %(client_addr)s etc.
let config_content = r#"{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"default": {
"format": "%(module)s.%(funcName)s | %(message)s"
},
"access": {
"format": "%(message)s"
}
},
"handlers": {
"default": {
"class": "logging.StreamHandler",
"stream": "ext://sys.stderr",
"formatter": "default"
},
"access": {
"class": "logging.StreamHandler",
"stream": "ext://sys.stdout",
"formatter": "access"
}
},
"loggers": {
"uvicorn": {
"handlers": ["default"],
"level": "INFO",
"propagate": false
},
"uvicorn.error": {
"level": "INFO",
"propagate": true
},
"uvicorn.access": {
"handlers": ["access"],
"level": "INFO",
"propagate": false
}
},
"root": {
"level": "INFO",
"handlers": ["default"]
}
}"#;

tokio::fs::write(&config_path, config_content)
.await
.map_err(|e| format!("Failed to write uvicorn logging config: {e}"))?;

Ok(config_path.display().to_string())
}

async fn spawn_pglite(&self, bun: &BunCommand) -> Result<(), String> {
let child = self
.spawn_process(
Expand Down Expand Up @@ -581,6 +521,7 @@ impl ProcessManager {
let dev_server_port = self.dev_server_port;
let dev_token = self.dev_token.clone();
let db_password = self.db_password.clone();
let dev_config = self.dev_config.clone();

tokio::spawn(async move {
let (tx, mut rx) = tokio::sync::mpsc::channel::<Event>(100);
Expand Down Expand Up @@ -700,6 +641,7 @@ impl ProcessManager {
&db_password,
&dotenv_vars,
&backend_child,
&dev_config,
)
.await
{
Expand Down Expand Up @@ -804,16 +746,17 @@ impl ProcessManager {
db_password: &str,
dotenv_vars: &Arc<Mutex<HashMap<String, String>>>,
backend_child: &Arc<Mutex<Option<Child>>>,
dev_config: &DevConfig,
) -> Result<(), String> {
// ============================================================================
// Backend logs are captured via stdout/stderr and forwarded to flux.
// No OTEL Python dependencies required - apx handles log collection.
// See spawn_uvicorn() for detailed explanation.
// ============================================================================

// Reuse the existing log config file (created by spawn_uvicorn)
let log_config = app_dir.join(".apx").join("uvicorn_logging.json");
let log_config_str = log_config.display().to_string();
// 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();

// Run uvicorn via uv to ensure correct Python environment
let mut cmd = UvCommand::new("uvicorn").tokio_command();
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod flux;
mod interop;
mod mcp;
mod openapi;
mod python_logging;
mod registry;
mod search;
mod sources;
Expand Down
5 changes: 3 additions & 2 deletions src/openapi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2096,7 +2096,8 @@ mod tests {
"name": "apx-ts-typecheck",
"private": true,
"dependencies": {
"@tanstack/react-query": "^5"
"@tanstack/react-query": "^5",
"typescript": "^5"
}
}
"#;
Expand Down Expand Up @@ -2152,7 +2153,7 @@ mod tests {
// Run tsc from the test environment directory with explicit compiler options
// Using `bun x` which is equivalent to `bunx`
let output = Command::new("bun")
.arg("x")
.arg("run")
.args([
"tsc",
"--noEmit",
Expand Down
Loading