### /Users/vlad/Developer/madapes/madbase/gateway/src/control.rs ```rust 1: use axum::{ 2: extract::{Request, Query}, 3: middleware::{from_fn, Next}, 4: response::{Response, IntoResponse}, 5: routing::get, 6: Router, 7: }; 8: use axum::http::StatusCode; 9: use axum_prometheus::PrometheusMetricLayer; 10: use common::{init_pool, Config}; 11: use sqlx::PgPool; 12: use crate::admin_auth::admin_auth_middleware; 13: use std::collections::HashMap; 14: use std::net::SocketAddr; 15: use std::time::Duration; 16: use tower_http::services::ServeDir; 17: use tower_http::cors::{AllowOrigin, CorsLayer}; use axum::http::{HeaderMap, HeaderValue, Method}; use axum::http::header; 18: use tower_http::trace::TraceLayer; 19: 20: async fn logs_proxy_handler( 21: Query(params): Query>, 22: ) -> impl IntoResponse { 23: let client = reqwest::Client::new(); 24: let loki_url = std::env::var("LOKI_URL") 25: .unwrap_or_else(|_| "http://loki:3100".to_string()); 26: let query_url = format!("{}/loki/api/v1/query_range", loki_url); 27: 28: let resp = client 29: .get(&query_url) 30: .query(¶ms) 31: .send() 32: .await; 33: 34: match resp { 35: Ok(r) => { 36: let status = StatusCode::from_u16(r.status().as_u16()) 37: .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); 38: let body = r.bytes().await.unwrap_or_default(); 39: (status, body).into_response() 40: }, 41: Err(e) => { 42: tracing::error!("Loki proxy error: {}", e); 43: (StatusCode::BAD_GATEWAY, e.to_string()).into_response() 44: } 45: } 46: } 47: 48: async fn dashboard_handler() -> axum::response::Html<&'static str> { 49: axum::response::Html(include_str!("../../web/admin.html")) 50: } 51: 52: async fn wait_for_db(db_url: &str) -> PgPool { 53: loop { 54: match init_pool(db_url).await { 55: Ok(pool) => return pool, 56: Err(e) => { 57: tracing::warn!("Database not ready yet, retrying in 2s: {}", e); 58: tokio::time::sleep(Duration::from_secs(2)).await; 59: } 60: } 61: } 62: } 63: 64: async fn log_headers(req: Request, next: Next) -> Response { 65: tracing::debug!("Request Headers: {:?}", req.headers()); 66: next.run(req).await 67: } 68: 69: pub async fn run() -> anyhow::Result<()> { 70: let config = Config::new().expect("Failed to load configuration"); 71: 72: tracing::info!("Starting MadBase Control Plane..."); 73: 74: let pool = wait_for_db(&config.database_url).await; 75: 76: sqlx::migrate!("../migrations") 77: .run(&pool) 78: .await 79: .expect("Failed to run migrations"); 80: 81: let default_tenant_db_url = std::env::var("DEFAULT_TENANT_DB_URL") 82: .expect("DEFAULT_TENANT_DB_URL must be set"); 83: let tenant_pool = wait_for_db(&default_tenant_db_url).await; 84: 85: let control_state = control_plane::ControlPlaneState { 86: db: pool.clone(), 87: tenant_db: tenant_pool.clone(), 88: }; 89: 90: let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair(); 91: 92: let platform_router = control_plane::router(control_state) 93: .route("/logs", get(logs_proxy_handler)); 94: 95: let app = Router::new() 96: .route("/", get(|| async { "MadBase Control Plane" })) 97: .route("/health", get(|| async { "OK" })) 98: .route("/metrics", get(|| async move { metric_handle.render() })) 99: .route("/dashboard", get(dashboard_handler)) 100: .nest_service("/css", ServeDir::new("web/css")) 101: .nest_service("/js", ServeDir::new("web/js")) 102: .nest("/platform/v1", platform_router) 103: .layer(from_fn(admin_auth_middleware)) 104: .layer( 105: CorsLayer::new() 106: .allow_origin(Any) 107: .allow_methods(Any) 108: .allow_headers(Any), 109: ) 110: .layer(TraceLayer::new_for_http()) 111: .layer(from_fn(log_headers)) 112: .layer(prometheus_layer); 113: 114: let port = std::env::var("CONTROL_PORT") 115: .unwrap_or_else(|_| "8001".to_string()) 116: .parse::()?; 117: 118: let addr = SocketAddr::from(([0, 0, 0, 0], port)); 119: tracing::info!("Control plane listening on {}", addr); 120: 121: let listener = tokio::net::TcpListener::bind(addr).await?; 122: axum::serve(listener, app.into_make_service_with_connect_info::()).await?; 123: 124: Ok(()) 125: } ```