mod api; mod data_value; mod dto; mod engine; mod filter_datetime; mod filter_parser; mod index_key; mod ingest; mod metric_primitive; mod settings; mod timestamp; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use clap::Parser; use tokio::net::TcpListener; use tokio::sync::mpsc; use settings::Settings; /// Observability dashboard from terminal output. #[derive(Parser, Debug)] struct Args { /// Port to serve the API and webapp on. #[arg(long, default_value_t = 8420)] port: u16, /// Directory containing the built webapp (index.html + assets). #[arg(long, default_value = "webapp/dist")] static_dir: String, /// Path to the settings file. Defaults to a per-OS config location /// (e.g. `~/.config/ottyl/settings.json` on Linux). #[arg(long)] settings_path: Option, } #[tokio::main] async fn main() { let args = Args::parse(); let settings_path = args.settings_path.clone().unwrap_or_else(|| { settings::default_settings_path() .expect("could not determine a default settings path; pass --settings-path") }); let settings = Arc::new(Mutex::new(Settings::load(&settings_path))); let (engine_tx, engine_rx) = mpsc::channel(1024); tokio::spawn(engine::run(engine_rx)); tokio::spawn(ingest::run(engine_tx.clone())); let app = api::router(engine_tx, &args.static_dir, settings); let listener = TcpListener::bind(("0.0.0.0", args.port)) .await .expect("failed to bind listener"); eprintln!( "ottyl serving on http://localhost:{} (reading logs from stdin)", args.port ); axum::serve(listener, app) .await .expect("server error"); } #[cfg(test)] mod tests { use super::*; #[test] fn settings_path_defaults_to_none() { let args = Args::parse_from(["ottyl"]); assert_eq!(args.settings_path, None); } #[test] fn settings_path_flag_overrides_default() { let args = Args::parse_from(["ottyl", "--settings-path", "/tmp/custom-settings.json"]); assert_eq!( args.settings_path, Some(PathBuf::from("/tmp/custom-settings.json")) ); } }