feat: better profile-based router assembly, tracing layer for responses with span-based request paths

This commit is contained in:
2025-09-13 18:01:53 -05:00
parent 64449e8976
commit b64aa41b14
3 changed files with 74 additions and 45 deletions

View File

@@ -9,8 +9,8 @@ build-frontend:
pnpm run -C web build pnpm run -C web build
# Auto-reloading backend server # Auto-reloading backend server
backend services=default_services: backend *ARGS:
bacon --headless run -- -- --services "{{services}}" bacon --headless run -- -- {{ARGS}}
# Production build # Production build
build: build:
@@ -19,10 +19,10 @@ build:
# Run auto-reloading development build with release characteristics (frontend is embedded, non-auto-reloading) # Run auto-reloading development build with release characteristics (frontend is embedded, non-auto-reloading)
# This is useful for testing backend release-mode details. # This is useful for testing backend release-mode details.
dev-build services=default_services: build-frontend dev-build *ARGS='--services web --tracing pretty': build-frontend
bacon --headless run -- --profile dev-release -- --services "{{services}}" --tracing pretty bacon --headless run -- --profile dev-release -- {{ARGS}}
# Auto-reloading development build for both frontend and backend # Auto-reloading development build for both frontend and backend
# Will not notice if either the frontend/backend crashes, but will generally be resistant to stopping on their own. # Will not notice if either the frontend/backend crashes, but will generally be resistant to stopping on their own.
[parallel] [parallel]
dev services=default_services: frontend (backend services) dev: frontend backend

View File

@@ -30,7 +30,7 @@ pnpm install -C web # Install frontend dependencies
cargo build # Build the backend cargo build # Build the backend
just dev # Runs auto-reloading dev build just dev # Runs auto-reloading dev build
just dev bot,web # Runs auto-reloading dev build, running only the bot and web services just dev --services bot,web # Runs auto-reloading dev build, running only the bot and web services
just dev-build # Development build with release characteristics (frontend is embedded, non-auto-reloading) just dev-build # Development build with release characteristics (frontend is embedded, non-auto-reloading)
just build # Production build that embeds assets just build # Production build that embeds assets

View File

@@ -2,6 +2,7 @@
use axum::{ use axum::{
Router, Router,
body::Body,
extract::{Request, State}, extract::{Request, State},
http::{HeaderMap, HeaderValue, StatusCode, Uri}, http::{HeaderMap, HeaderValue, StatusCode, Uri},
response::{Html, IntoResponse, Json, Response}, response::{Html, IntoResponse, Json, Response},
@@ -9,12 +10,13 @@ use axum::{
}; };
use http::header; use http::header;
use serde_json::{Value, json}; use serde_json::{Value, json};
use std::sync::Arc; use std::{sync::Arc, time::Duration};
use tower_http::{ use tower_http::{
classify::ServerErrorsFailureClass,
cors::{Any, CorsLayer}, cors::{Any, CorsLayer},
trace::TraceLayer, trace::TraceLayer,
}; };
use tracing::info; use tracing::{Span, debug, info, warn};
use crate::web::assets::{WebAssets, get_asset_metadata_cached}; use crate::web::assets::{WebAssets, get_asset_metadata_cached};
@@ -70,47 +72,74 @@ pub fn create_router(state: BannerState) -> Router {
.route("/metrics", get(metrics)) .route("/metrics", get(metrics))
.with_state(state); .with_state(state);
if cfg!(debug_assertions) { let mut router = Router::new().nest("/api", api_router);
// Development mode: API routes only, frontend served by Vite dev server
Router::new()
.route("/", get(root))
.nest("/api", api_router)
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any),
)
.layer(TraceLayer::new_for_http())
} else {
// Production mode: serve embedded assets and handle SPA routing
Router::new()
.route("/", get(root))
.nest("/api", api_router)
.fallback(fallback)
.layer(TraceLayer::new_for_http())
}
}
async fn root() -> Response {
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
// Development mode: return API info router = router.layer(
Json(json!({ CorsLayer::new()
"message": "Banner Discord Bot API", .allow_origin(Any)
"version": "0.2.1", .allow_methods(Any)
"mode": "development", .allow_headers(Any),
"frontend": "http://localhost:3000", )
"endpoints": {
"health": "/api/health",
"status": "/api/status",
"metrics": "/api/metrics"
}
}))
.into_response()
} else { } else {
// Production mode: serve the SPA index.html router = router.fallback(fallback);
handle_spa_fallback_with_headers(Uri::from_static("/"), HeaderMap::new()).await
} }
router.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<Body>| {
tracing::debug_span!("request", path = request.uri().path())
})
.on_request(())
.on_body_chunk(())
.on_eos(())
.on_response(
|response: &Response<Body>, latency: Duration, _span: &Span| {
let latency_threshold = if cfg!(debug_assertions) {
Duration::from_millis(100)
} else {
Duration::from_millis(1000)
};
// Format latency, status, and code
let (latency_str, status, code) = (
format!("{latency:.2?}"),
response.status().as_u16(),
format!(
"{} {}",
response.status().as_u16(),
response.status().canonical_reason().unwrap_or("??")
),
);
// Log in warn if latency is above threshold, otherwise debug
if latency > latency_threshold {
warn!(
latency = latency_str,
status = status,
code = code,
"Response"
);
} else {
debug!(
latency = latency_str,
status = status,
code = code,
"Response"
);
}
},
)
.on_failure(
|error: ServerErrorsFailureClass, latency: Duration, _span: &Span| {
warn!(
error = ?error,
latency = format!("{latency:.2?}"),
"Request failed"
);
},
),
)
} }
/// Handler that extracts request information for caching /// Handler that extracts request information for caching