repos
/ analytics-rust master

analytics-rust

mirror archived upstream

Single-binary self-hosted website analytics on Rust axum: collector API, dashboards, world map, and PDF reports.

analyticsaxumdockerrustself-hostedsqliteviteweb-analytics

2.8 KB · 97 lines · Rust Raw History
 1mod app;
 2mod db;
 3mod geoip;
 4mod middleware;
 5mod migrate;
 6mod models;
 7mod pdf;
 8mod queries;
 9mod render;
10mod routes;
11mod templates;
12mod ua;
13
14pub use app::{AppState, Config};
15
16use std::net::SocketAddr;
17use std::path::PathBuf;
18
19#[tokio::main]
20async fn main() -> anyhow::Result<()> {
21    dotenvy::dotenv().ok();
22    tracing_subscriber::fmt()
23        .with_env_filter(
24            tracing_subscriber::EnvFilter::try_from_default_env()
25                .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,sqlx=warn")),
26        )
27        .init();
28
29    // Subcommand dispatch. Anything besides `migrate` falls through to the server.
30    let mut argv = std::env::args().skip(1);
31    if let Some(first) = argv.next() {
32        match first.as_str() {
33            "migrate" => return run_migrate(argv.collect()).await,
34            "--help" | "-h" => {
35                print_usage();
36                return Ok(());
37            }
38            other if !other.is_empty() => {
39                eprintln!("unknown subcommand: {other}");
40                print_usage();
41                std::process::exit(2);
42            }
43            _ => {}
44        }
45    }
46
47    let port: u16 = std::env::var("PORT")
48        .ok()
49        .and_then(|v| v.parse().ok())
50        .unwrap_or(8000);
51
52    let state = AppState::from_env().await?;
53    app::spawn_background_downloads(&state);
54    let router = app::router(state);
55
56    let addr = SocketAddr::from(([0, 0, 0, 0], port));
57    let listener = tokio::net::TcpListener::bind(addr).await?;
58    tracing::info!("analytics listening on http://{addr}");
59    axum::serve(listener, router).await?;
60    Ok(())
61}
62
63fn print_usage() {
64    eprintln!(
65        "analytics — single-binary axum analytics server\n\
66         \n\
67         Usage:\n  \
68           analytics                              run the HTTP server\n  \
69           analytics migrate <path> [--force]     import a Django analytics SQLite\n"
70    );
71}
72
73async fn run_migrate(args: Vec<String>) -> anyhow::Result<()> {
74    let mut source: Option<PathBuf> = None;
75    let mut force = false;
76    for arg in args {
77        match arg.as_str() {
78            "--force" | "-f" => force = true,
79            "--help" | "-h" => {
80                eprintln!("Usage: analytics migrate <path-to-django-sqlite3> [--force]");
81                return Ok(());
82            }
83            v if v.starts_with("--") => anyhow::bail!("unknown migrate flag: {v}"),
84            v => {
85                if source.is_some() {
86                    anyhow::bail!("migrate takes a single source path");
87                }
88                source = Some(PathBuf::from(v));
89            }
90        }
91    }
92    let source = source.ok_or_else(|| {
93        anyhow::anyhow!("usage: analytics migrate <path-to-django-sqlite3> [--force]")
94    })?;
95    migrate::run(source, force).await
96}