From 6f7e1640c1d114b71dcfcba3ba7282ba292d9040 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Sun, 27 Jul 2025 17:41:43 -0400 Subject: [PATCH] fix(cli): reduce noise & better track progress within Harmony --- Cargo.lock | 43 +++++++++++ harmony/Cargo.toml | 1 + harmony/src/domain/instrumentation.rs | 51 +++++++++++++ harmony/src/domain/maestro/mod.rs | 9 ++- harmony/src/domain/mod.rs | 1 + harmony/src/domain/topology/k8s_anywhere.rs | 2 +- harmony_composer/Cargo.toml | 4 + harmony_composer/src/cli_logger.rs | 84 +++++++++++++++++++++ harmony_composer/src/main.rs | 69 +++++++++++++---- 9 files changed, 249 insertions(+), 15 deletions(-) create mode 100644 harmony/src/domain/instrumentation.rs create mode 100644 harmony_composer/src/cli_logger.rs diff --git a/Cargo.lock b/Cargo.lock index e19e6f3..870c2ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -650,6 +650,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.60.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1137,6 +1150,12 @@ dependencies = [ "serde", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1765,6 +1784,7 @@ dependencies = [ "libredfish", "log", "non-blank-string-rs", + "once_cell", "opnsense-config", "opnsense-config-xml", "pretty_assertions", @@ -1810,9 +1830,13 @@ dependencies = [ "bollard", "cargo_metadata", "clap", + "console", "current_platform", "env_logger", "futures-util", + "harmony", + "indicatif", + "lazy_static", "log", "serde_json", "tokio", @@ -2409,6 +2433,19 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" +dependencies = [ + "console", + "portable-atomic", + "unicode-width 0.2.0", + "unit-prefix", + "web-time", +] + [[package]] name = "indoc" version = "2.0.6" @@ -5181,6 +5218,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index 87b97ac..daa9fd3 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -62,6 +62,7 @@ serde_with = "3.14.0" bollard.workspace = true tar.workspace = true base64.workspace = true +once_cell = "1.21.3" [dev-dependencies] pretty_assertions.workspace = true diff --git a/harmony/src/domain/instrumentation.rs b/harmony/src/domain/instrumentation.rs new file mode 100644 index 0000000..ca5e3e9 --- /dev/null +++ b/harmony/src/domain/instrumentation.rs @@ -0,0 +1,51 @@ +use log::debug; +use once_cell::sync::Lazy; +use tokio::sync::broadcast; + +// FIXME: Ce module d'instrumentation ne peut pas fonctionner à la fois pour Harmony Composer et +// Harmony CLI. Elle n'a donc pas entièrement sa place ici et devra être séparée en deux. +#[derive(Debug, Clone)] +pub enum HarmonyEvent { + ProjectInitializationStarted, + ProjectInitialized, + ProjectCompilationStarted { details: String }, + ProjectCompiled, + ProjectCompilationFailed { details: String }, + DeploymentStarted { target: String }, + DeploymentCompleted { details: String }, + PrepareTopologyStarted { name: String }, + Shutdown, +} + +static EVENT_BUS: Lazy> = Lazy::new(|| { + // TODO: Adjust channel capacity + let (tx, _rx) = broadcast::channel(16); + tx +}); + +pub fn instrument(event: HarmonyEvent) { + EVENT_BUS.send(event).expect("couldn't send event"); +} + +pub async fn subscribe(name: &str, mut handler: F) +where + F: FnMut(HarmonyEvent) -> Fut + Send + 'static, + Fut: Future + Send, +{ + let mut rx = EVENT_BUS.subscribe(); + debug!("[{name}] Service started. Listening for events..."); + loop { + match rx.recv().await { + Ok(event) => { + if !handler(event).await { + debug!("[{name}] Handler requested exit."); + break; + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + debug!("[{name}] Lagged behind by {n} messages."); + } + Err(_) => break, + } + } +} diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 8a90eed..e97195e 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -2,6 +2,8 @@ use std::sync::{Arc, Mutex, RwLock}; use log::{info, warn}; +use crate::instrumentation::{self, HarmonyEvent}; + use super::{ interpret::{InterpretError, InterpretStatus, Outcome}, inventory::Inventory, @@ -40,7 +42,12 @@ impl Maestro { /// Ensures the associated Topology is ready for operations. /// Delegates the readiness check and potential setup actions to the Topology. pub async fn prepare_topology(&self) -> Result { - info!("Ensuring topology '{}' is ready...", self.topology.name()); + // FIXME: Cette instrumentation ne peut pas communiquer avec Harmony Composer puisqu'il + // s'agit d'un process différent. + // + // instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted { + // name: self.topology.name().to_string(), + // }); let outcome = self.topology.ensure_ready().await?; info!( "Topology '{}' readiness check complete: {}", diff --git a/harmony/src/domain/mod.rs b/harmony/src/domain/mod.rs index 349191a..028fa7f 100644 --- a/harmony/src/domain/mod.rs +++ b/harmony/src/domain/mod.rs @@ -3,6 +3,7 @@ pub mod data; pub mod executors; pub mod filter; pub mod hardware; +pub mod instrumentation; pub mod interpret; pub mod inventory; pub mod maestro; diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index adbfbec..80627c8 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -159,7 +159,7 @@ impl K8sAnywhereTopology { if !k8s_anywhere_config.autoinstall { debug!("Autoinstall confirmation prompt"); - let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? : ") + let confirmation = Confirm::new( "Harmony autoinstallation is not activated, do you wish to launch autoinstallation? :") .with_default(false) .prompt() .expect("Unexpected prompt error"); diff --git a/harmony_composer/Cargo.toml b/harmony_composer/Cargo.toml index 6d8c5be..fbe03c7 100644 --- a/harmony_composer/Cargo.toml +++ b/harmony_composer/Cargo.toml @@ -15,3 +15,7 @@ current_platform = "0.2.0" futures-util = "0.3.31" serde_json = "1.0.140" cargo_metadata = "0.20.0" +harmony = { path = "../harmony" } +indicatif = "0.18.0" +console = "0.16.0" +lazy_static = "1.5.0" diff --git a/harmony_composer/src/cli_logger.rs b/harmony_composer/src/cli_logger.rs new file mode 100644 index 0000000..e5db42f --- /dev/null +++ b/harmony_composer/src/cli_logger.rs @@ -0,0 +1,84 @@ +use console::Emoji; +use harmony::instrumentation::{self, HarmonyEvent}; +use indicatif::{ProgressBar, ProgressStyle}; +use lazy_static::lazy_static; +use log::error; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; + +static EMOJI_HARMONY: Emoji<'_, '_> = Emoji("🎼", ""); +static EMOJI_SUCCESS: Emoji<'_, '_> = Emoji("✅", ""); +static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", ""); +static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", ""); +static EMOJI_TOPOLOGY: Emoji<'_, '_> = Emoji("📦", ""); + +lazy_static! { + pub static ref SPINNER_STYLE: ProgressStyle = ProgressStyle::default_spinner() + .template(" {spinner:.green} {msg}") + .unwrap() + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); + pub static ref SUCCESS_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE + .clone() + .tick_strings(&[format!("{}", EMOJI_SUCCESS).as_str()]); + pub static ref ERROR_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE + .clone() + .tick_strings(&[format!("{}", EMOJI_ERROR).as_str()]); +} + +pub async fn init() { + instrumentation::subscribe("CLI Logger", { + let current_spinner = Arc::new(Mutex::new(None::)); + + move |event| { + let spinner_clone = Arc::clone(¤t_spinner); + + async move { + let mut spinner_guard = spinner_clone.lock().unwrap(); + + match event { + HarmonyEvent::ProjectInitializationStarted => { + println!("{} Initializing Harmony project...", EMOJI_HARMONY); + } + HarmonyEvent::ProjectInitialized => println!("\n"), + HarmonyEvent::ProjectCompilationStarted { details } => { + let progress = ProgressBar::new_spinner(); + progress.set_style(SPINNER_STYLE.clone()); + progress.set_message(details); + progress.enable_steady_tick(Duration::from_millis(100)); + *spinner_guard = Some(progress); + } + HarmonyEvent::ProjectCompiled => { + if let Some(progress) = spinner_guard.take() { + progress.set_style(SUCCESS_SPINNER_STYLE.clone()); + progress.finish_with_message("project compiled"); + } + } + HarmonyEvent::ProjectCompilationFailed { details } => { + if let Some(progress) = spinner_guard.take() { + progress.set_style(ERROR_SPINNER_STYLE.clone()); + progress.finish_with_message("failed to compile project"); + error!("{details}"); + } + } + HarmonyEvent::DeploymentStarted { target } => { + println!("{} Starting deployment to {target}...", EMOJI_DEPLOY); + } + HarmonyEvent::DeploymentCompleted { details } => println!("\n"), + HarmonyEvent::PrepareTopologyStarted { name } => { + println!("{} Preparing environment: {name}...", EMOJI_TOPOLOGY); + } + HarmonyEvent::Shutdown => { + if let Some(progress) = spinner_guard.take() { + progress.abandon(); + } + return false; + } + } + true + } + } + }) + .await +} diff --git a/harmony_composer/src/main.rs b/harmony_composer/src/main.rs index ef2756f..4ab5017 100644 --- a/harmony_composer/src/main.rs +++ b/harmony_composer/src/main.rs @@ -7,12 +7,15 @@ use bollard::secret::HostConfig; use cargo_metadata::{Artifact, Message, MetadataCommand}; use clap::{Args, Parser, Subcommand}; use futures_util::StreamExt; -use log::info; +use harmony::instrumentation::{self, HarmonyEvent}; +use log::{debug, info, log_enabled}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use tokio::fs; +mod cli_logger; + #[derive(Parser)] #[command(version, about, long_about = None, flatten_help = true, propagate_version = true)] struct GlobalArgs { @@ -67,12 +70,15 @@ struct AllArgs { #[tokio::main] async fn main() { env_logger::init(); + let cli_logger_handle = tokio::spawn(cli_logger::init()); let cli_args = GlobalArgs::parse(); let harmony_path = Path::new(&cli_args.harmony_path) .try_exists() .expect("couldn't check if path exists"); + instrumentation::instrument(HarmonyEvent::ProjectInitializationStarted); + let harmony_bin_path: PathBuf = match harmony_path { true => { compile_harmony( @@ -85,6 +91,8 @@ async fn main() { false => todo!("implement autodetect code"), }; + instrumentation::instrument(HarmonyEvent::ProjectInitialized); + match cli_args.command { Some(command) => match command { Commands::Check(args) => { @@ -116,19 +124,27 @@ async fn main() { } Commands::Deploy(args) => { let deploy = if args.staging { + instrumentation::instrument(HarmonyEvent::DeploymentStarted { + target: "staging".to_string(), + }); todo!("implement staging deployment") } else if args.prod { + instrumentation::instrument(HarmonyEvent::DeploymentStarted { + target: "prod".to_string(), + }); todo!("implement prod deployment") } else { + instrumentation::instrument(HarmonyEvent::DeploymentStarted { + target: "dev".to_string(), + }); Command::new(harmony_bin_path).arg("-y").arg("-a").spawn() } .expect("failed to run harmony deploy"); let deploy_output = deploy.wait_with_output().unwrap(); - println!( - "deploy output: {}", - String::from_utf8(deploy_output.stdout).expect("couldn't parse from utf8") - ); + instrumentation::instrument(HarmonyEvent::DeploymentCompleted { + details: String::from_utf8(deploy_output.stdout).unwrap(), + }); } Commands::All(_args) => todo!( "take all previous match arms and turn them into separate functions, and call them all one after the other" @@ -137,6 +153,10 @@ async fn main() { }, None => todo!("run interactively, ask for info on CLI"), } + + instrumentation::instrument(HarmonyEvent::Shutdown); + + let _ = tokio::try_join!(cli_logger_handle); } #[derive(Clone, Debug, clap::ValueEnum)] @@ -166,17 +186,30 @@ async fn compile_harmony( Some(m) => m, None => { if cargo_exists { - return compile_cargo(platform, harmony_location).await; + CompileMethod::LocalCargo } else { - return compile_docker(platform, harmony_location).await; + CompileMethod::Docker } } }; - match method { - CompileMethod::LocalCargo => return compile_cargo(platform, harmony_location).await, - CompileMethod::Docker => return compile_docker(platform, harmony_location).await, + let path = match method { + CompileMethod::LocalCargo => { + instrumentation::instrument(HarmonyEvent::ProjectCompilationStarted { + details: "compiling project with cargo".to_string(), + }); + compile_cargo(platform, harmony_location).await + } + CompileMethod::Docker => { + instrumentation::instrument(HarmonyEvent::ProjectCompilationStarted { + details: "compiling project with docker".to_string(), + }); + compile_docker(platform, harmony_location).await + } }; + + instrumentation::instrument(HarmonyEvent::ProjectCompiled); + path } // TODO: make sure this works with cargo workspaces @@ -186,6 +219,12 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf { .exec() .unwrap(); + let stderr = if log_enabled!(log::Level::Debug) { + Stdio::inherit() + } else { + Stdio::piped() + }; + let mut cargo_build = Command::new("cargo") .current_dir(&harmony_location) .args(vec![ @@ -195,6 +234,7 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf { "--message-format=json-render-diagnostics", ]) .stdout(Stdio::piped()) + .stderr(stderr) .spawn() .expect("run cargo command failed"); @@ -210,18 +250,20 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf { .expect("failed to get root package") .manifest_path { - println!("{:?}", artifact); + debug!("{:?}", artifact); artifacts.push(artifact); } } Message::BuildScriptExecuted(_script) => (), Message::BuildFinished(finished) => { - println!("{:?}", finished); + debug!("{:?}", finished); } _ => (), // Unknown message } } + cargo_build.wait().expect("run cargo command failed"); + let bin = artifacts .last() .expect("no binaries built") @@ -237,7 +279,8 @@ async fn compile_cargo(platform: String, harmony_location: String) -> PathBuf { bin_out = PathBuf::from(format!("{}/harmony", harmony_location)); let _copy_res = fs::copy(&bin, &bin_out).await; } - return bin_out; + + bin_out } async fn compile_docker(platform: String, harmony_location: String) -> PathBuf {