Skip to main content

mini_apm_admin/
server.rs

1use axum::{
2    Router,
3    extract::DefaultBodyLimit,
4    middleware,
5    routing::{get, post},
6};
7use std::net::SocketAddr;
8use tokio::signal;
9use tower_cookies::CookieManagerLayer;
10use tower_http::trace::TraceLayer;
11
12use mini_apm::{DbPool, config::Config, jobs, models};
13use crate::{api, web};
14
15/// Combined state for routes that need both pool and config
16#[derive(Clone)]
17pub struct AppState {
18    pub pool: DbPool,
19    pub config: Config,
20}
21
22/// Maximum request body size (10 MB)
23const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
24
25pub async fn run(pool: DbPool, config: Config, port: u16) -> anyhow::Result<()> {
26    // Initialize start time for uptime tracking
27    api::health::init_start_time();
28
29    // Always ensure default project and admin exist
30    // When features are disabled, we just skip the UI/auth, not the data
31    let default_project = models::project::ensure_default_project(&pool)?;
32    models::user::ensure_default_admin(&pool)?;
33
34    if !config.enable_projects {
35        tracing::info!("Single-project mode - API key: {}", default_project.api_key);
36    }
37
38    // Start background jobs
39    jobs::start(pool.clone(), config.clone());
40
41    // Build router
42    let app = Router::new()
43        // Health check (no auth)
44        .route("/health", get(api::health_handler))
45        // Ingestion API (with API key auth)
46        .nest(
47            "/ingest",
48            Router::new()
49                .route("/deploys", post(api::ingest_deploys))
50                .route("/v1/traces", post(api::ingest_spans))
51                .route("/errors", post(api::ingest_errors))
52                .route("/errors/batch", post(api::ingest_errors_batch))
53                .layer(middleware::from_fn_with_state(
54                    pool.clone(),
55                    api::auth_middleware,
56                )),
57        )
58        // Auth routes (always available)
59        .merge(web::auth_routes())
60        // Web UI (protected when user accounts enabled)
61        .merge(web::routes(pool.clone()))
62        // Static files
63        .nest_service("/static", tower_http::services::ServeDir::new("static"))
64        // State and middleware
65        .with_state(pool)
66        .layer(DefaultBodyLimit::max(MAX_BODY_SIZE))
67        .layer(CookieManagerLayer::new())
68        .layer(TraceLayer::new_for_http());
69
70    let addr = SocketAddr::from(([0, 0, 0, 0], port));
71    tracing::info!("MiniAPM server listening on http://{}", addr);
72
73    if config.enable_user_accounts {
74        tracing::info!("User accounts ENABLED - login required");
75    }
76
77    let listener = tokio::net::TcpListener::bind(addr).await?;
78
79    // Graceful shutdown
80    axum::serve(listener, app)
81        .with_graceful_shutdown(shutdown_signal())
82        .await?;
83
84    tracing::info!("Server shutdown complete");
85    Ok(())
86}
87
88async fn shutdown_signal() {
89    let ctrl_c = async {
90        signal::ctrl_c()
91            .await
92            .expect("Failed to install Ctrl+C handler");
93    };
94
95    #[cfg(unix)]
96    let terminate = async {
97        signal::unix::signal(signal::unix::SignalKind::terminate())
98            .expect("Failed to install SIGTERM handler")
99            .recv()
100            .await;
101    };
102
103    #[cfg(not(unix))]
104    let terminate = std::future::pending::<()>();
105
106    tokio::select! {
107        _ = ctrl_c => {
108            tracing::info!("Received Ctrl+C, starting graceful shutdown...");
109        }
110        _ = terminate => {
111            tracing::info!("Received SIGTERM, starting graceful shutdown...");
112        }
113    }
114}