exercises

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }