mirror of
https://github.com/Xevion/banner.git
synced 2025-12-06 05:14:26 -06:00
feat: better profile-based router assembly, tracing layer for responses with span-based request paths
This commit is contained in:
10
Justfile
10
Justfile
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
let mut router = Router::new().nest("/api", api_router);
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
if cfg!(debug_assertions) {
|
||||||
// Development mode: API routes only, frontend served by Vite dev server
|
router = router.layer(
|
||||||
Router::new()
|
|
||||||
.route("/", get(root))
|
|
||||||
.nest("/api", api_router)
|
|
||||||
.layer(
|
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
.allow_origin(Any)
|
.allow_origin(Any)
|
||||||
.allow_methods(Any)
|
.allow_methods(Any)
|
||||||
.allow_headers(Any),
|
.allow_headers(Any),
|
||||||
)
|
)
|
||||||
.layer(TraceLayer::new_for_http())
|
|
||||||
} else {
|
} else {
|
||||||
// Production mode: serve embedded assets and handle SPA routing
|
router = router.fallback(fallback);
|
||||||
Router::new()
|
|
||||||
.route("/", get(root))
|
|
||||||
.nest("/api", api_router)
|
|
||||||
.fallback(fallback)
|
|
||||||
.layer(TraceLayer::new_for_http())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn root() -> Response {
|
router.layer(
|
||||||
if cfg!(debug_assertions) {
|
TraceLayer::new_for_http()
|
||||||
// Development mode: return API info
|
.make_span_with(|request: &Request<Body>| {
|
||||||
Json(json!({
|
tracing::debug_span!("request", path = request.uri().path())
|
||||||
"message": "Banner Discord Bot API",
|
})
|
||||||
"version": "0.2.1",
|
.on_request(())
|
||||||
"mode": "development",
|
.on_body_chunk(())
|
||||||
"frontend": "http://localhost:3000",
|
.on_eos(())
|
||||||
"endpoints": {
|
.on_response(
|
||||||
"health": "/api/health",
|
|response: &Response<Body>, latency: Duration, _span: &Span| {
|
||||||
"status": "/api/status",
|
let latency_threshold = if cfg!(debug_assertions) {
|
||||||
"metrics": "/api/metrics"
|
Duration::from_millis(100)
|
||||||
}
|
|
||||||
}))
|
|
||||||
.into_response()
|
|
||||||
} else {
|
} else {
|
||||||
// Production mode: serve the SPA index.html
|
Duration::from_millis(1000)
|
||||||
handle_spa_fallback_with_headers(Uri::from_static("/"), HeaderMap::new()).await
|
};
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|||||||
Reference in New Issue
Block a user