diff --git a/Cargo.lock b/Cargo.lock index 741fb74..bab0702 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1872,6 +1872,7 @@ name = "harmony_cli" version = "0.1.0" dependencies = [ "assert_cmd", + "chrono", "clap", "console", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index c770668..a91bf56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ readme = "README.md" license = "GNU AGPL v3" [workspace.dependencies] -log = "0.4" +log = { version = "0.4", features = ["kv"] } env_logger = "0.11" derive-new = "0.7" async-trait = "0.1" diff --git a/examples/application_monitoring_with_tenant/harmony b/examples/application_monitoring_with_tenant/harmony new file mode 100755 index 0000000..d8d9885 Binary files /dev/null and b/examples/application_monitoring_with_tenant/harmony differ diff --git a/examples/nanodc/src/main.rs b/examples/nanodc/src/main.rs index 51b92d4..10754b4 100644 --- a/examples/nanodc/src/main.rs +++ b/examples/nanodc/src/main.rs @@ -8,7 +8,6 @@ use harmony::{ hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup}, infra::opnsense::OPNSenseManagementInterface, inventory::Inventory, - maestro::Maestro, modules::{ http::StaticFilesHttpScore, ipxe::IpxeScore, @@ -130,16 +129,21 @@ async fn main() { "./data/watchguard/pxe-http-files".to_string(), )); let ipxe_score = IpxeScore::new(); - let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); - maestro.register_all(vec![ - Box::new(dns_score), - Box::new(bootstrap_dhcp_score), - Box::new(bootstrap_load_balancer_score), - Box::new(load_balancer_score), - Box::new(tftp_score), - Box::new(http_score), - Box::new(ipxe_score), - Box::new(dhcp_score), - ]); - harmony_tui::init(maestro).await.unwrap(); + + harmony_tui::run( + inventory, + topology, + vec![ + Box::new(dns_score), + Box::new(bootstrap_dhcp_score), + Box::new(bootstrap_load_balancer_score), + Box::new(load_balancer_score), + Box::new(tftp_score), + Box::new(http_score), + Box::new(ipxe_score), + Box::new(dhcp_score), + ], + ) + .await + .unwrap(); } diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index c07c60e..61f8f18 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -8,7 +8,6 @@ use harmony::{ hardware::{FirewallGroup, HostCategory, Location, PhysicalHost, SwitchGroup}, infra::opnsense::OPNSenseManagementInterface, inventory::Inventory, - maestro::Maestro, modules::{ dummy::{ErrorScore, PanicScore, SuccessScore}, http::StaticFilesHttpScore, @@ -84,20 +83,25 @@ async fn main() { let http_score = StaticFilesHttpScore::new(Url::LocalFolder( "./data/watchguard/pxe-http-files".to_string(), )); - let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); - maestro.register_all(vec![ - Box::new(dns_score), - Box::new(dhcp_score), - Box::new(load_balancer_score), - Box::new(tftp_score), - Box::new(http_score), - Box::new(OPNsenseShellCommandScore { - opnsense: opnsense.get_opnsense_config(), - command: "touch /tmp/helloharmonytouching".to_string(), - }), - Box::new(SuccessScore {}), - Box::new(ErrorScore {}), - Box::new(PanicScore {}), - ]); - harmony_tui::init(maestro).await.unwrap(); + + harmony_tui::run( + inventory, + topology, + vec![ + Box::new(dns_score), + Box::new(dhcp_score), + Box::new(load_balancer_score), + Box::new(tftp_score), + Box::new(http_score), + Box::new(OPNsenseShellCommandScore { + opnsense: opnsense.get_opnsense_config(), + command: "touch /tmp/helloharmonytouching".to_string(), + }), + Box::new(SuccessScore {}), + Box::new(ErrorScore {}), + Box::new(PanicScore {}), + ], + ) + .await + .unwrap(); } diff --git a/examples/tui/src/main.rs b/examples/tui/src/main.rs index 39d5039..4b1aabe 100644 --- a/examples/tui/src/main.rs +++ b/examples/tui/src/main.rs @@ -2,7 +2,6 @@ use std::net::{SocketAddr, SocketAddrV4}; use harmony::{ inventory::Inventory, - maestro::Maestro, modules::{ dns::DnsScore, dummy::{ErrorScore, PanicScore, SuccessScore}, @@ -16,18 +15,19 @@ use harmony_macros::ipv4; #[tokio::main] async fn main() { - let inventory = Inventory::autoload(); - let topology = DummyInfra {}; - let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); - - maestro.register_all(vec![ - Box::new(SuccessScore {}), - Box::new(ErrorScore {}), - Box::new(PanicScore {}), - Box::new(DnsScore::new(vec![], None)), - Box::new(build_large_score()), - ]); - harmony_tui::init(maestro).await.unwrap(); + harmony_tui::run( + Inventory::autoload(), + DummyInfra {}, + vec![ + Box::new(SuccessScore {}), + Box::new(ErrorScore {}), + Box::new(PanicScore {}), + Box::new(DnsScore::new(vec![], None)), + Box::new(build_large_score()), + ], + ) + .await + .unwrap(); } fn build_large_score() -> LoadBalancerScore { diff --git a/harmony/src/domain/topology/ha_cluster.rs b/harmony/src/domain/topology/ha_cluster.rs index 02ee66e..737419f 100644 --- a/harmony/src/domain/topology/ha_cluster.rs +++ b/harmony/src/domain/topology/ha_cluster.rs @@ -241,7 +241,7 @@ pub struct DummyInfra; #[async_trait] impl Topology for DummyInfra { fn name(&self) -> &str { - todo!() + "DummyInfra" } async fn ensure_ready(&self) -> Result { diff --git a/harmony_cli/Cargo.toml b/harmony_cli/Cargo.toml index a887b60..248b702 100644 --- a/harmony_cli/Cargo.toml +++ b/harmony_cli/Cargo.toml @@ -22,6 +22,7 @@ indicatif = "0.18.0" lazy_static = "1.5.0" log.workspace = true indicatif-log-bridge = "0.2.3" +chrono.workspace = true [dev-dependencies] harmony = { path = "../harmony", features = ["testing"] } diff --git a/harmony_cli/src/cli_logger.rs b/harmony_cli/src/cli_logger.rs index 9078e5d..c2fc8d0 100644 --- a/harmony_cli/src/cli_logger.rs +++ b/harmony_cli/src/cli_logger.rs @@ -1,22 +1,17 @@ +use chrono::Local; +use console::style; use harmony::{ instrumentation::{self, HarmonyEvent}, modules::application::ApplicationFeatureStatus, topology::TopologyStatus, }; -use indicatif::MultiProgress; -use indicatif_log_bridge::LogWrapper; -use log::error; -use std::{ - sync::{Arc, Mutex}, - thread, - time::Duration, -}; - -use crate::progress::{IndicatifProgressTracker, ProgressTracker}; +use log::{error, info, log_enabled}; +use std::io::Write; +use std::sync::{Arc, Mutex}; pub fn init() -> tokio::task::JoinHandle<()> { - let base_progress = configure_logger(); - let handle = tokio::spawn(handle_events(base_progress)); + configure_logger(); + let handle = tokio::spawn(handle_events()); loop { if instrumentation::instrument(HarmonyEvent::HarmonyStarted).is_ok() { @@ -27,28 +22,76 @@ pub fn init() -> tokio::task::JoinHandle<()> { handle } -fn configure_logger() -> MultiProgress { - let logger = - env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build(); - let level = logger.filter(); - let progress = MultiProgress::new(); +fn configure_logger() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .format(|buf, record| { + let debug_mode = log_enabled!(log::Level::Debug); + let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S"); - LogWrapper::new(progress.clone(), logger) - .try_init() - .unwrap(); - log::set_max_level(level); - - progress + let level = match record.level() { + log::Level::Error => style("ERROR").red(), + log::Level::Warn => style("WARN").yellow(), + log::Level::Info => style("INFO").green(), + log::Level::Debug => style("DEBUG").blue(), + log::Level::Trace => style("TRACE").magenta(), + }; + if let Some(status) = record.key_values().get(log::kv::Key::from("status")) { + let status = status.to_borrowed_str().unwrap(); + let emoji = match status { + "finished" => style(crate::theme::EMOJI_SUCCESS.to_string()).green(), + "skipped" => style(crate::theme::EMOJI_SKIP.to_string()).yellow(), + "failed" => style(crate::theme::EMOJI_ERROR.to_string()).red(), + _ => style("".into()), + }; + if debug_mode { + writeln!( + buf, + "[{} {:<5} {}] {} {}", + timestamp, + level, + record.target(), + emoji, + record.args() + ) + } else { + writeln!(buf, "[{:<5}] {} {}", level, emoji, record.args()) + } + } else if let Some(emoji) = record.key_values().get(log::kv::Key::from("emoji")) { + if debug_mode { + writeln!( + buf, + "[{} {:<5} {}] {} {}", + timestamp, + level, + record.target(), + emoji, + record.args() + ) + } else { + writeln!(buf, "[{:<5}] {} {}", level, emoji, record.args()) + } + } else if debug_mode { + writeln!( + buf, + "[{} {:<5} {}] {}", + timestamp, + level, + record.target(), + record.args() + ) + } else { + writeln!(buf, "[{:<5}] {}", level, record.args()) + } + }) + .init(); } -async fn handle_events(base_progress: MultiProgress) { - let progress_tracker = Arc::new(IndicatifProgressTracker::new(base_progress.clone())); +async fn handle_events() { let preparing_topology = Arc::new(Mutex::new(false)); let current_score: Arc>> = Arc::new(Mutex::new(None)); instrumentation::subscribe("Harmony CLI Logger", { move |event| { - let progress_tracker = Arc::clone(&progress_tracker); let preparing_topology = Arc::clone(&preparing_topology); let current_score = Arc::clone(¤t_score); @@ -59,90 +102,57 @@ async fn handle_events(base_progress: MultiProgress) { match event { HarmonyEvent::HarmonyStarted => {} HarmonyEvent::HarmonyFinished => { - progress_tracker.add_section( - "harmony-summary", - &format!("\n{} Harmony completed\n\n", crate::theme::EMOJI_HARMONY), - ); - progress_tracker.add_section("harmony-finished", "\n\n"); - thread::sleep(Duration::from_millis(200)); + let emoji = crate::theme::EMOJI_HARMONY.to_string(); + info!(emoji = emoji.as_str(); "Harmony completed"); return false; } HarmonyEvent::TopologyStateChanged { topology, status, message, - } => { - let section_key = topology_key(&topology); - - match status { - TopologyStatus::Queued => {} - TopologyStatus::Preparing => { - progress_tracker.add_section( - §ion_key, - &format!( - "\n{} Preparing environment: {topology}...", - crate::theme::EMOJI_TOPOLOGY - ), - ); - (*preparing_topology) = true; - } - TopologyStatus::Success => { - (*preparing_topology) = false; - progress_tracker.add_task(§ion_key, "topology-success", ""); - progress_tracker - .finish_task("topology-success", &message.unwrap_or("".into())); - } - TopologyStatus::Noop => { - (*preparing_topology) = false; - progress_tracker.add_task(§ion_key, "topology-skip", ""); - progress_tracker - .skip_task("topology-skip", &message.unwrap_or("".into())); - } - TopologyStatus::Error => { - progress_tracker.add_task(§ion_key, "topology-error", ""); - (*preparing_topology) = false; - progress_tracker - .fail_task("topology-error", &message.unwrap_or("".into())); + } => match status { + TopologyStatus::Queued => {} + TopologyStatus::Preparing => { + let emoji = format!("{}", style(crate::theme::EMOJI_TOPOLOGY.to_string()).yellow()); + info!(emoji = emoji.as_str(); "Preparing environment: {topology}..."); + (*preparing_topology) = true; + } + TopologyStatus::Success => { + (*preparing_topology) = false; + if let Some(message) = message { + info!(status = "finished"; "{message}"); } } - } + TopologyStatus::Noop => { + (*preparing_topology) = false; + if let Some(message) = message { + info!(status = "skipped"; "{message}"); + } + } + TopologyStatus::Error => { + (*preparing_topology) = false; + if let Some(message) = message { + error!(status = "failed"; "{message}"); + } + } + }, HarmonyEvent::InterpretExecutionStarted { - execution_id: task_key, - topology, + execution_id: _, + topology: _, interpret: _, score, message, } => { - let is_key_topology = (*preparing_topology) - && progress_tracker.contains_section(&topology_key(&topology)); - let is_key_current_score = current_score.is_some() - && progress_tracker - .contains_section(&score_key(¤t_score.clone().unwrap())); - let is_key_score = progress_tracker.contains_section(&score_key(&score)); - - let section_key = if is_key_topology { - topology_key(&topology) - } else if is_key_current_score { - score_key(¤t_score.clone().unwrap()) - } else if is_key_score { - score_key(&score) + if *preparing_topology || current_score.is_some() { + info!("{message}"); } else { (*current_score) = Some(score.clone()); - let key = score_key(&score); - progress_tracker.add_section( - &key, - &format!( - "{} Interpreting score: {score}...", - crate::theme::EMOJI_SCORE - ), - ); - key - }; - - progress_tracker.add_task(§ion_key, &task_key, &message); + let emoji = format!("{}", style(crate::theme::EMOJI_SCORE).blue()); + info!(emoji = emoji.as_str(); "Interpreting score: {score}..."); + } } HarmonyEvent::InterpretExecutionFinished { - execution_id: task_key, + execution_id: _, topology: _, interpret: _, score, @@ -155,16 +165,17 @@ async fn handle_events(base_progress: MultiProgress) { match outcome { Ok(outcome) => match outcome.status { harmony::interpret::InterpretStatus::SUCCESS => { - progress_tracker.finish_task(&task_key, &outcome.message); + info!(status = "finished"; "{}", outcome.message); } harmony::interpret::InterpretStatus::NOOP => { - progress_tracker.skip_task(&task_key, &outcome.message); + info!(status = "skipped"; "{}", outcome.message); + } + _ => { + error!(status = "failed"; "{}", outcome.message); } - _ => progress_tracker.fail_task(&task_key, &outcome.message), }, Err(err) => { - error!("Interpret error: {err}"); - progress_tracker.fail_task(&task_key, &err.to_string()); + error!(status = "failed"; "{}", err); } } } @@ -173,30 +184,17 @@ async fn handle_events(base_progress: MultiProgress) { application, feature, status, - } => { - if let Some(score) = &(*current_score) { - let section_key = score_key(score); - let task_key = app_feature_key(&application, &feature); - - match status { - ApplicationFeatureStatus::Installing => { - let message = format!("Feature '{}' installing...", feature); - progress_tracker.add_task(§ion_key, &task_key, &message); - } - ApplicationFeatureStatus::Installed => { - let message = format!("Feature '{}' installed", feature); - progress_tracker.finish_task(&task_key, &message); - } - ApplicationFeatureStatus::Failed { details } => { - let message = format!( - "Feature '{}' installation failed: {}", - feature, details - ); - progress_tracker.fail_task(&task_key, &message); - } - } + } => match status { + ApplicationFeatureStatus::Installing => { + info!("Installing feature '{}' for '{}'...", feature, application); } - } + ApplicationFeatureStatus::Installed => { + info!(status = "finished"; "Feature '{}' installed", feature); + } + ApplicationFeatureStatus::Failed { details } => { + error!(status = "failed"; "Feature '{}' installation failed: {}", feature, details); + } + }, } true } @@ -204,15 +202,3 @@ async fn handle_events(base_progress: MultiProgress) { }) .await; } - -fn topology_key(topology: &str) -> String { - format!("topology-{topology}") -} - -fn score_key(score: &str) -> String { - format!("score-{score}") -} - -fn app_feature_key(application: &str, feature: &str) -> String { - format!("app-{application}-{feature}") -} diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 711a709..53de86e 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -90,13 +90,37 @@ pub async fn run( topology: T, scores: Vec>>, args_struct: Option, +) -> Result<(), Box> { + let args = match args_struct { + Some(args) => args, + None => Args::parse(), + }; + + #[cfg(not(feature = "tui"))] + if args.interactive { + return Err("Not compiled with interactive support".into()); + } + + #[cfg(feature = "tui")] + if args.interactive { + return harmony_tui::run(inventory, topology, scores).await; + } + + run_cli(inventory, topology, scores, args).await +} + +pub async fn run_cli( + inventory: Inventory, + topology: T, + scores: Vec>>, + args: Args, ) -> Result<(), Box> { let cli_logger_handle = cli_logger::init(); let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); maestro.register_all(scores); - let result = init(maestro, args_struct).await; + let result = init(maestro, args).await; instrumentation::instrument(instrumentation::HarmonyEvent::HarmonyFinished).unwrap(); let _ = tokio::try_join!(cli_logger_handle); @@ -105,23 +129,8 @@ pub async fn run( async fn init( maestro: harmony::maestro::Maestro, - args_struct: Option, + args: Args, ) -> Result<(), Box> { - let args = match args_struct { - Some(args) => args, - None => Args::parse(), - }; - - #[cfg(feature = "tui")] - if args.interactive { - return harmony_tui::init(maestro).await; - } - - #[cfg(not(feature = "tui"))] - if args.interactive { - return Err("Not compiled with interactive support".into()); - } - let _ = env_logger::builder().try_init(); let scores_vec = maestro_scores_filter(&maestro, args.all, args.filter, args.number); @@ -193,14 +202,14 @@ mod tests { let maestro = init_test_maestro(); let res = crate::init( maestro, - Some(crate::Args { + crate::Args { yes: true, filter: Some("SuccessScore".to_owned()), interactive: false, all: true, number: 0, list: false, - }), + }, ) .await; @@ -213,14 +222,14 @@ mod tests { let res = crate::init( maestro, - Some(crate::Args { + crate::Args { yes: true, filter: Some("ErrorScore".to_owned()), interactive: false, all: true, number: 0, list: false, - }), + }, ) .await; @@ -233,14 +242,14 @@ mod tests { let res = crate::init( maestro, - Some(crate::Args { + crate::Args { yes: true, filter: None, interactive: false, all: false, number: 0, list: false, - }), + }, ) .await; diff --git a/harmony_tui/src/lib.rs b/harmony_tui/src/lib.rs index 4a807ff..4fb4591 100644 --- a/harmony_tui/src/lib.rs +++ b/harmony_tui/src/lib.rs @@ -9,7 +9,13 @@ use widget::{help::HelpWidget, score::ScoreListWidget}; use std::{panic, sync::Arc, time::Duration}; use crossterm::event::{Event, EventStream, KeyCode, KeyEventKind}; -use harmony::{maestro::Maestro, score::Score, topology::Topology}; +use harmony::{ + instrumentation::{self, HarmonyEvent}, + inventory::Inventory, + maestro::Maestro, + score::Score, + topology::Topology, +}; use ratatui::{ self, Frame, layout::{Constraint, Layout, Position}, @@ -39,22 +45,62 @@ pub mod tui { /// /// #[tokio::main] /// async fn main() { -/// let inventory = Inventory::autoload(); -/// let topology = HAClusterTopology::autoload(); -/// let mut maestro = Maestro::new_without_initialization(inventory, topology); -/// -/// maestro.register_all(vec![ -/// Box::new(SuccessScore {}), -/// Box::new(ErrorScore {}), -/// Box::new(PanicScore {}), -/// ]); -/// harmony_tui::init(maestro).await.unwrap(); +/// harmony_tui::run( +/// Inventory::autoload(), +/// HAClusterTopology::autoload(), +/// vec![ +/// Box::new(SuccessScore {}), +/// Box::new(ErrorScore {}), +/// Box::new(PanicScore {}), +/// ] +/// ).await.unwrap(); /// } /// ``` -pub async fn init( +pub async fn run( + inventory: Inventory, + topology: T, + scores: Vec>>, +) -> Result<(), Box> { + let handle = init_instrumentation().await; + + let mut maestro = Maestro::initialize(inventory, topology).await.unwrap(); + maestro.register_all(scores); + + let result = init(maestro).await; + + let _ = tokio::try_join!(handle); + result +} + +async fn init( maestro: Maestro, ) -> Result<(), Box> { - HarmonyTUI::new(maestro).init().await + let result = HarmonyTUI::new(maestro).init().await; + + instrumentation::instrument(HarmonyEvent::HarmonyFinished).unwrap(); + result +} + +async fn init_instrumentation() -> tokio::task::JoinHandle<()> { + let handle = tokio::spawn(handle_harmony_events()); + + loop { + if instrumentation::instrument(HarmonyEvent::HarmonyStarted).is_ok() { + break; + } + } + + handle +} + +async fn handle_harmony_events() { + instrumentation::subscribe("Harmony TUI Logger", async |event| { + if let HarmonyEvent::HarmonyFinished = event { + return false; + }; + true + }) + .await; } pub struct HarmonyTUI {