repos
/ status-rust master

status-rust

mirror archived upstream

Single-binary self-hosted uptime monitoring and status pages on Rust axum: HTTP probes, Lighthouse audits, SEO crawler, and PDF reports.

axumdockerrustself-hostedsqlitestatus-pageuptime-monitoringvite

4.1 KB · 126 lines · Rust Raw History
  1mod alerts;
  2mod app;
  3mod checker;
  4mod crawler;
  5mod db;
  6mod lighthouse;
  7mod middleware;
  8mod migrate;
  9mod models;
 10mod pdf;
 11mod render;
 12mod routes;
 13mod scheduler;
 14mod templates;
 15
 16pub use app::{AppState, Config};
 17
 18use std::net::SocketAddr;
 19use std::path::PathBuf;
 20
 21#[tokio::main]
 22async fn main() -> anyhow::Result<()> {
 23    dotenvy::dotenv().ok();
 24    tracing_subscriber::fmt()
 25        .with_env_filter(
 26            tracing_subscriber::EnvFilter::try_from_default_env()
 27                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,sqlx=warn")),
 28        )
 29        .init();
 30
 31    // rustls 0.23 doesn't pick a CryptoProvider on its own when neither
 32    // the `ring` nor the `aws-lc-rs` feature is enabled at the rustls
 33    // crate level. The phased prober (src/checker.rs) builds rustls
 34    // ClientConfigs directly, which would panic on first probe without
 35    // this. Install once at startup; reqwest/lettre's TLS paths are
 36    // unaffected because they ship their own provider via hyper-rustls.
 37    let _ = rustls::crypto::ring::default_provider().install_default();
 38
 39    // Subcommand dispatch. Anything besides a known subcommand falls through to the server.
 40    let mut argv = std::env::args().skip(1);
 41    if let Some(first) = argv.next() {
 42        match first.as_str() {
 43            "migrate" => return run_migrate(argv.collect()).await,
 44            "preview-email" => return run_preview_email(argv.collect()),
 45            "--help" | "-h" => {
 46                print_usage();
 47                return Ok(());
 48            }
 49            other if !other.is_empty() => {
 50                eprintln!("unknown subcommand: {other}");
 51                print_usage();
 52                std::process::exit(2);
 53            }
 54            _ => {}
 55        }
 56    }
 57
 58    let port: u16 = std::env::var("PORT")
 59        .ok()
 60        .and_then(|v| v.parse().ok())
 61        .unwrap_or(8000);
 62
 63    let state = AppState::from_env().await?;
 64
 65    // Reset wedged crawl/lighthouse rows from a prior crash, then spawn the
 66    // scheduler loop alongside the HTTP server.
 67    scheduler::reset_states_on_boot(&state.pool).await?;
 68    let scheduler_handle = scheduler::spawn(state.pool.clone(), state.config.clone());
 69
 70    let router = app::router(state);
 71
 72    let addr = SocketAddr::from(([0, 0, 0, 0], port));
 73    let listener = tokio::net::TcpListener::bind(addr).await?;
 74    tracing::info!("status listening on http://{addr}");
 75    axum::serve(listener, router).await?;
 76    drop(scheduler_handle);
 77    Ok(())
 78}
 79
 80fn print_usage() {
 81    eprintln!(
 82        "status: single-binary axum uptime monitor\n\
 83         \n\
 84         Usage:\n  \
 85           status                              run the HTTP server + scheduler\n  \
 86           status migrate <path> [--force]     import a Django status SQLite\n  \
 87           status preview-email <down|recovery> render an alert HTML to stdout\n"
 88    );
 89}
 90
 91fn run_preview_email(args: Vec<String>) -> anyhow::Result<()> {
 92    let kind = args
 93        .first()
 94        .map(String::as_str)
 95        .ok_or_else(|| anyhow::anyhow!("usage: status preview-email <down|recovery>"))?;
 96    let base_url = std::env::var("BASE_URL").unwrap_or_else(|_| "http://localhost:8000".to_string());
 97    let html = alerts::render_preview_html(kind, &base_url)?;
 98    print!("{html}");
 99    Ok(())
100}
101
102async fn run_migrate(args: Vec<String>) -> anyhow::Result<()> {
103    let mut source: Option<PathBuf> = None;
104    let mut force = false;
105    for arg in args {
106        match arg.as_str() {
107            "--force" | "-f" => force = true,
108            "--help" | "-h" => {
109                eprintln!("Usage: status migrate <path-to-django-sqlite3> [--force]");
110                return Ok(());
111            }
112            v if v.starts_with("--") => anyhow::bail!("unknown migrate flag: {v}"),
113            v => {
114                if source.is_some() {
115                    anyhow::bail!("migrate takes a single source path");
116                }
117                source = Some(PathBuf::from(v));
118            }
119        }
120    }
121    let source = source.ok_or_else(|| {
122        anyhow::anyhow!("usage: status migrate <path-to-django-sqlite3> [--force]")
123    })?;
124    migrate::run(source, force).await
125}