main.rs (8132B)
1 mod auth; 2 mod config; 3 mod db; 4 mod handlers; 5 mod metrics; 6 mod models; 7 mod proxy; 8 mod telemetry; 9 10 use anyhow::Result; 11 use axum::{ 12 body::Body, 13 http::{Request, StatusCode, Uri}, 14 response::{IntoResponse, Response}, 15 routing::{get, post}, 16 Router, 17 }; 18 use axum_otel_metrics::HttpMetricsLayerBuilder; 19 use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; 20 use std::net::SocketAddr; 21 use std::path::PathBuf; 22 use tower::ServiceExt; 23 use tower_http::{cors::CorsLayer, services::ServeDir}; 24 use tracing::info; 25 26 use config::Config; 27 use db::Database; 28 29 #[derive(Clone)] 30 pub struct AppState { 31 db: Database, 32 http_client: reqwest_middleware::ClientWithMiddleware, 33 products_service_url: String, 34 inventory_service_url: String, 35 orders_service_url: String, 36 checkout_service_url: String, 37 } 38 39 /// SPA fallback handler that serves static files or index.html 40 async fn spa_fallback_handler( 41 uri: Uri, 42 req: Request<Body>, 43 serve_dir: ServeDir, 44 static_dir: String, 45 ) -> impl IntoResponse { 46 let path = uri.path(); 47 48 // Check if the request is for an actual file (has an extension) 49 let has_extension = path.rfind('.').map_or(false, |dot_pos| { 50 let after_dot = &path[dot_pos + 1..]; 51 // Check if it looks like a file extension (not empty and no slashes after) 52 !after_dot.is_empty() && !after_dot.contains('/') 53 }); 54 55 // If it's a file with an extension, try to serve it 56 if has_extension { 57 match serve_dir.oneshot(req).await { 58 Ok(res) => { 59 // If file was found (status is not 404), return it 60 if res.status() != StatusCode::NOT_FOUND { 61 return res.into_response(); 62 } 63 // Otherwise fall through to serve index.html 64 } 65 Err(_) => { 66 // Error serving file, fall through to index.html 67 } 68 } 69 } 70 71 // For all other routes (SPA routes like /products, /checkout, etc.) 72 // or files that weren't found, serve index.html 73 let index_path = PathBuf::from(&static_dir).join("index.html"); 74 75 match tokio::fs::read_to_string(&index_path).await { 76 Ok(contents) => Response::builder() 77 .status(StatusCode::OK) 78 .header("content-type", "text/html; charset=utf-8") 79 .body(Body::from(contents)) 80 .unwrap() 81 .into_response(), 82 Err(_) => Response::builder() 83 .status(StatusCode::NOT_FOUND) 84 .header("content-type", "text/html; charset=utf-8") 85 .body(Body::from( 86 "<h1>UI not built</h1><p>Please build the Angular application first.</p>", 87 )) 88 .unwrap() 89 .into_response(), 90 } 91 } 92 93 #[tokio::main] 94 async fn main() -> Result<()> { 95 // Initialize telemetry (tracing + OpenTelemetry) 96 let _telemetry_guard = telemetry::init_telemetry("otelmart"); 97 98 // Load configuration from config.toml 99 let config = Config::load()?; 100 101 // Log startup information using tracing macros 102 info!( 103 port = config.server.port, 104 database_url = %config.database.url, 105 products_service = format!("{}:{}", config.services.products_host, config.services.products_port), 106 inventory_service = format!("{}:{}", config.services.inventory_host, config.services.inventory_port), 107 orders_service = format!("{}:{}", config.services.orders_host, config.services.orders_port), 108 static_dir = %config.server.static_dir, 109 "Starting OtelMart Service" 110 ); 111 112 // Initialize database connection 113 let db = Database::new(&config.database.url, config.database.max_connections).await?; 114 115 // Register observable gauges for connection pool health metrics 116 let meter = opentelemetry::global::meter("inventory-service"); 117 telemetry::register_pool_metrics(&meter, db.pool().clone()); 118 119 // Build automatic HTTP RED metrics layer 120 let metrics = HttpMetricsLayerBuilder::new().build(); 121 122 // Create HTTP client with tracing middleware for automatic 123 // span creation and trace context propagation 124 let reqwest_client = reqwest::Client::builder() 125 .timeout(std::time::Duration::from_secs( 126 config.http_client.timeout_secs, 127 )) 128 .build()?; 129 let http_client = reqwest_middleware::ClientBuilder::new(reqwest_client) 130 .with(reqwest_tracing::TracingMiddleware::< 131 reqwest_tracing::SpanBackendWithUrl, 132 >::new()) 133 .build(); 134 135 // Create application state 136 let state = AppState { 137 db, 138 http_client, 139 products_service_url: config.services.products_service_url(), 140 inventory_service_url: config.services.inventory_service_url_computed(), 141 orders_service_url: config.services.orders_service_url_computed(), 142 checkout_service_url: config.services.checkout_service_url(), 143 }; 144 145 // Build API router 146 let api_router = Router::new() 147 // Auth routes 148 .route("/auth/register", post(handlers::auth::register)) 149 .route("/auth/login", post(handlers::auth::login)) 150 .route("/auth/logout", post(handlers::auth::logout)) 151 // User routes 152 .route("/users/profile", get(handlers::users::get_profile)) 153 .route("/users/profile", post(handlers::users::update_profile)) 154 .route("/users/addresses", get(handlers::users::get_addresses)) 155 .route("/users/addresses", post(handlers::users::add_address)) 156 .route( 157 "/users/addresses/{id}", 158 post(handlers::users::update_address), 159 ) 160 .route( 161 "/users/addresses/{id}", 162 axum::routing::delete(handlers::users::delete_address), 163 ) 164 // This is a common pattern in API gateways to ensure all request paths are 165 // properly routed to the backend services 166 // Proxy all /products/* requests to products service 167 .route( 168 "/products", 169 axum::routing::any(proxy::products::proxy_products), 170 ) 171 .route( 172 "/products/{*path}", 173 axum::routing::any(proxy::products::proxy_products), 174 ) 175 // Proxy all /inventory/* requests to inventory service 176 .route( 177 "/inventory", 178 axum::routing::any(proxy::inventory::proxy_inventory), 179 ) 180 .route( 181 "/inventory/{*path}", 182 axum::routing::any(proxy::inventory::proxy_inventory), 183 ) 184 // Proxy all /orders/* requests to orders service 185 .route("/orders", axum::routing::any(proxy::orders::proxy_orders)) 186 .route( 187 "/orders/{*path}", 188 axum::routing::any(proxy::orders::proxy_orders), 189 ) 190 .with_state(state.clone()); 191 192 // Create static file service 193 let static_dir = config.server.static_dir.clone(); 194 let serve_dir = ServeDir::new(&static_dir); 195 196 // Build main router 197 let app = Router::new() 198 // Health check 199 .route("/health", get(handlers::health::health_check)) 200 // Mount API routes under /api prefix 201 .nest("/api", api_router) 202 // Fallback handler for SPA routing 203 .fallback(move |uri: Uri, req: Request<Body>| { 204 let static_dir = static_dir.clone(); 205 let serve_dir = serve_dir.clone(); 206 async move { spa_fallback_handler(uri, req, serve_dir, static_dir).await } 207 }) 208 // Automatic RED metrics (request rate, error rate, duration) 209 .layer(metrics) 210 // Include trace context as header into the response 211 .layer(OtelInResponseLayer::default()) 212 // Start OpenTelemetry trace on incoming request 213 .layer(OtelAxumLayer::default()) 214 .layer(CorsLayer::permissive()); 215 216 // Start server 217 let addr = SocketAddr::from(([0, 0, 0, 0], config.server.port)); 218 println!("\nOtelMart Service listening on {}", addr); 219 220 let listener = tokio::net::TcpListener::bind(addr).await?; 221 axum::serve(listener, app).await?; 222 223 // Telemetry is flushed and shut down by the `Drop` impl on `_telemetry` 224 // when this function returns. 225 226 Ok(()) 227 }