From 6f7e1640c1d114b71dcfcba3ba7282ba292d9040 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Sun, 27 Jul 2025 17:41:43 -0400 Subject: [PATCH 1/7] 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 { -- 2.39.5 From 8fae9cf8c86bfc833943727d34d34d07beaa316c Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Sun, 27 Jul 2025 20:52:24 -0400 Subject: [PATCH 2/7] split instrumentation in 2 different places: harmony domain (for domain observability) & harmoy composer (for build/commands observability) --- Cargo.lock | 7 +- examples/rust/src/main.rs | 4 + harmony/src/domain/instrumentation.rs | 18 +--- harmony/src/domain/maestro/mod.rs | 9 +- harmony_cli/Cargo.toml | 4 + harmony_cli/src/cli_logger.rs | 28 +++++++ harmony_cli/src/lib.rs | 3 + harmony_cli/src/theme.rs | 22 +++++ harmony_composer/Cargo.toml | 3 +- harmony_composer/src/cli_logger.rs | 84 ------------------- .../src/harmony_composer_logger.rs | 67 +++++++++++++++ harmony_composer/src/instrumentation.rs | 51 +++++++++++ harmony_composer/src/main.rs | 29 +++---- 13 files changed, 209 insertions(+), 120 deletions(-) create mode 100644 harmony_cli/src/cli_logger.rs create mode 100644 harmony_cli/src/theme.rs delete mode 100644 harmony_composer/src/cli_logger.rs create mode 100644 harmony_composer/src/harmony_composer_logger.rs create mode 100644 harmony_composer/src/instrumentation.rs diff --git a/Cargo.lock b/Cargo.lock index 870c2ab..e0d07e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1816,10 +1816,14 @@ version = "0.1.0" dependencies = [ "assert_cmd", "clap", + "console", "env_logger", "harmony", "harmony_tui", + "indicatif", "inquire", + "lazy_static", + "log", "tokio", ] @@ -1834,10 +1838,11 @@ dependencies = [ "current_platform", "env_logger", "futures-util", - "harmony", + "harmony_cli", "indicatif", "lazy_static", "log", + "once_cell", "serde_json", "tokio", ] diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index c89a9f5..465770a 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -9,10 +9,12 @@ use harmony::{ }, topology::{K8sAnywhereTopology, Url}, }; +use harmony_cli::cli_logger; #[tokio::main] async fn main() { env_logger::init(); + let cli_logger_handle = tokio::spawn(cli_logger::init()); let topology = K8sAnywhereTopology::from_env(); let mut maestro = Maestro::initialize(Inventory::autoload(), topology) @@ -40,4 +42,6 @@ async fn main() { maestro.register_all(vec![Box::new(app)]); harmony_cli::init(maestro, None).await.unwrap(); + + let _ = tokio::try_join!(cli_logger_handle); } diff --git a/harmony/src/domain/instrumentation.rs b/harmony/src/domain/instrumentation.rs index ca5e3e9..e3b1546 100644 --- a/harmony/src/domain/instrumentation.rs +++ b/harmony/src/domain/instrumentation.rs @@ -2,29 +2,19 @@ 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(|| { +static HARMONY_EVENT_BUS: Lazy> = Lazy::new(|| { // TODO: Adjust channel capacity - let (tx, _rx) = broadcast::channel(16); + let (tx, _rx) = broadcast::channel(18); tx }); pub fn instrument(event: HarmonyEvent) { - EVENT_BUS.send(event).expect("couldn't send event"); + HARMONY_EVENT_BUS.send(event).expect("couldn't send event"); } pub async fn subscribe(name: &str, mut handler: F) @@ -32,7 +22,7 @@ where F: FnMut(HarmonyEvent) -> Fut + Send + 'static, Fut: Future + Send, { - let mut rx = EVENT_BUS.subscribe(); + let mut rx = HARMONY_EVENT_BUS.subscribe(); debug!("[{name}] Service started. Listening for events..."); loop { match rx.recv().await { diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index e97195e..d5f46b6 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -42,12 +42,9 @@ 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 { - // 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(), - // }); + 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_cli/Cargo.toml b/harmony_cli/Cargo.toml index e7d7398..1e51781 100644 --- a/harmony_cli/Cargo.toml +++ b/harmony_cli/Cargo.toml @@ -13,6 +13,10 @@ harmony_tui = { path = "../harmony_tui", optional = true } inquire.workspace = true tokio.workspace = true env_logger.workspace = true +console = "0.16.0" +indicatif = "0.18.0" +lazy_static = "1.5.0" +log.workspace = true [features] diff --git a/harmony_cli/src/cli_logger.rs b/harmony_cli/src/cli_logger.rs new file mode 100644 index 0000000..f4f0df1 --- /dev/null +++ b/harmony_cli/src/cli_logger.rs @@ -0,0 +1,28 @@ +use harmony::instrumentation::{self, HarmonyEvent}; +use indicatif::ProgressBar; +use std::sync::{Arc, Mutex}; + +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::PrepareTopologyStarted { name } => { + println!( + "{} Preparing environment: {name}...", + crate::theme::EMOJI_TOPOLOGY + ); + } + } + true + } + } + }) + .await +} diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 52b59e0..6ef9ac1 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -4,6 +4,9 @@ use harmony; use harmony::{score::Score, topology::Topology}; use inquire::Confirm; +pub mod cli_logger; // FIXME: Don't make me pub +pub mod theme; + #[cfg(feature = "tui")] use harmony_tui; diff --git a/harmony_cli/src/theme.rs b/harmony_cli/src/theme.rs new file mode 100644 index 0000000..a2215d0 --- /dev/null +++ b/harmony_cli/src/theme.rs @@ -0,0 +1,22 @@ +use console::Emoji; +use indicatif::ProgressStyle; +use lazy_static::lazy_static; + +pub static EMOJI_HARMONY: Emoji<'_, '_> = Emoji("🎼", ""); +pub static EMOJI_SUCCESS: Emoji<'_, '_> = Emoji("✅", ""); +pub static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", ""); +pub static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", ""); +pub 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()]); +} diff --git a/harmony_composer/Cargo.toml b/harmony_composer/Cargo.toml index fbe03c7..1ef37f1 100644 --- a/harmony_composer/Cargo.toml +++ b/harmony_composer/Cargo.toml @@ -15,7 +15,8 @@ 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" +once_cell = "1.21.3" +harmony_cli = { path = "../harmony_cli" } diff --git a/harmony_composer/src/cli_logger.rs b/harmony_composer/src/cli_logger.rs deleted file mode 100644 index e5db42f..0000000 --- a/harmony_composer/src/cli_logger.rs +++ /dev/null @@ -1,84 +0,0 @@ -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/harmony_composer_logger.rs b/harmony_composer/src/harmony_composer_logger.rs new file mode 100644 index 0000000..7641b51 --- /dev/null +++ b/harmony_composer/src/harmony_composer_logger.rs @@ -0,0 +1,67 @@ +use indicatif::ProgressBar; +use log::error; +use std::{ + sync::{Arc, Mutex}, + time::Duration, +}; + +use crate::instrumentation::{self, HarmonyComposerEvent}; + +pub async fn init() { + instrumentation::subscribe("Harmony Composer 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 { + HarmonyComposerEvent::ProjectInitializationStarted => { + println!( + "{} Initializing Harmony project...", + harmony_cli::theme::EMOJI_HARMONY + ); + } + HarmonyComposerEvent::ProjectInitialized => println!("\n"), + HarmonyComposerEvent::ProjectCompilationStarted { details } => { + let progress = ProgressBar::new_spinner(); + progress.set_style(harmony_cli::theme::SPINNER_STYLE.clone()); + progress.set_message(details); + progress.enable_steady_tick(Duration::from_millis(100)); + *spinner_guard = Some(progress); + } + HarmonyComposerEvent::ProjectCompiled => { + if let Some(progress) = spinner_guard.take() { + progress.set_style(harmony_cli::theme::SUCCESS_SPINNER_STYLE.clone()); + progress.finish_with_message("project compiled"); + } + } + HarmonyComposerEvent::ProjectCompilationFailed { details } => { + if let Some(progress) = spinner_guard.take() { + progress.set_style(harmony_cli::theme::ERROR_SPINNER_STYLE.clone()); + progress.finish_with_message("failed to compile project"); + error!("{details}"); + } + } + HarmonyComposerEvent::DeploymentStarted { target } => { + println!( + "{} Starting deployment to {target}...\n", + harmony_cli::theme::EMOJI_DEPLOY + ); + } + HarmonyComposerEvent::DeploymentCompleted { details } => println!("\n"), + HarmonyComposerEvent::Shutdown => { + if let Some(progress) = spinner_guard.take() { + progress.abandon(); + } + return false; + } + } + true + } + } + }) + .await +} diff --git a/harmony_composer/src/instrumentation.rs b/harmony_composer/src/instrumentation.rs new file mode 100644 index 0000000..171556f --- /dev/null +++ b/harmony_composer/src/instrumentation.rs @@ -0,0 +1,51 @@ +use log::debug; +use once_cell::sync::Lazy; +use tokio::sync::broadcast; + +#[derive(Debug, Clone)] +pub enum HarmonyComposerEvent { + ProjectInitializationStarted, + ProjectInitialized, + ProjectCompilationStarted { details: String }, + ProjectCompiled, + ProjectCompilationFailed { details: String }, + DeploymentStarted { target: String }, + DeploymentCompleted { details: String }, + Shutdown, +} + +static HARMONY_COMPOSER_EVENT_BUS: Lazy> = + Lazy::new(|| { + // TODO: Adjust channel capacity + let (tx, _rx) = broadcast::channel(16); + tx + }); + +pub fn instrument(event: HarmonyComposerEvent) { + HARMONY_COMPOSER_EVENT_BUS + .send(event) + .expect("couldn't send event"); +} + +pub async fn subscribe(name: &str, mut handler: F) +where + F: FnMut(HarmonyComposerEvent) -> Fut + Send + 'static, + Fut: Future + Send, +{ + let mut rx = HARMONY_COMPOSER_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_composer/src/main.rs b/harmony_composer/src/main.rs index 4ab5017..a2d01e0 100644 --- a/harmony_composer/src/main.rs +++ b/harmony_composer/src/main.rs @@ -7,14 +7,15 @@ use bollard::secret::HostConfig; use cargo_metadata::{Artifact, Message, MetadataCommand}; use clap::{Args, Parser, Subcommand}; use futures_util::StreamExt; -use harmony::instrumentation::{self, HarmonyEvent}; +use instrumentation::HarmonyComposerEvent; 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; +mod harmony_composer_logger; +mod instrumentation; #[derive(Parser)] #[command(version, about, long_about = None, flatten_help = true, propagate_version = true)] @@ -70,14 +71,14 @@ struct AllArgs { #[tokio::main] async fn main() { env_logger::init(); - let cli_logger_handle = tokio::spawn(cli_logger::init()); + let hc_logger_handle = tokio::spawn(harmony_composer_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); + instrumentation::instrument(HarmonyComposerEvent::ProjectInitializationStarted); let harmony_bin_path: PathBuf = match harmony_path { true => { @@ -91,7 +92,7 @@ async fn main() { false => todo!("implement autodetect code"), }; - instrumentation::instrument(HarmonyEvent::ProjectInitialized); + instrumentation::instrument(HarmonyComposerEvent::ProjectInitialized); match cli_args.command { Some(command) => match command { @@ -124,17 +125,17 @@ async fn main() { } Commands::Deploy(args) => { let deploy = if args.staging { - instrumentation::instrument(HarmonyEvent::DeploymentStarted { + instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { target: "staging".to_string(), }); todo!("implement staging deployment") } else if args.prod { - instrumentation::instrument(HarmonyEvent::DeploymentStarted { + instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { target: "prod".to_string(), }); todo!("implement prod deployment") } else { - instrumentation::instrument(HarmonyEvent::DeploymentStarted { + instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { target: "dev".to_string(), }); Command::new(harmony_bin_path).arg("-y").arg("-a").spawn() @@ -142,7 +143,7 @@ async fn main() { .expect("failed to run harmony deploy"); let deploy_output = deploy.wait_with_output().unwrap(); - instrumentation::instrument(HarmonyEvent::DeploymentCompleted { + instrumentation::instrument(HarmonyComposerEvent::DeploymentCompleted { details: String::from_utf8(deploy_output.stdout).unwrap(), }); } @@ -154,9 +155,9 @@ async fn main() { None => todo!("run interactively, ask for info on CLI"), } - instrumentation::instrument(HarmonyEvent::Shutdown); + instrumentation::instrument(HarmonyComposerEvent::Shutdown); - let _ = tokio::try_join!(cli_logger_handle); + let _ = tokio::try_join!(hc_logger_handle); } #[derive(Clone, Debug, clap::ValueEnum)] @@ -195,20 +196,20 @@ async fn compile_harmony( let path = match method { CompileMethod::LocalCargo => { - instrumentation::instrument(HarmonyEvent::ProjectCompilationStarted { + instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted { details: "compiling project with cargo".to_string(), }); compile_cargo(platform, harmony_location).await } CompileMethod::Docker => { - instrumentation::instrument(HarmonyEvent::ProjectCompilationStarted { + instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted { details: "compiling project with docker".to_string(), }); compile_docker(platform, harmony_location).await } }; - instrumentation::instrument(HarmonyEvent::ProjectCompiled); + instrumentation::instrument(HarmonyComposerEvent::ProjectCompiled); path } -- 2.39.5 From ff7801a7c18a0418d2f7c3f12706537d9850912a Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Wed, 30 Jul 2025 12:22:04 -0400 Subject: [PATCH 3/7] ensure event handlers are properly subscribed on init, extract duplicated progress functions, cleanup duplication --- Cargo.lock | 12 +++ examples/rust/src/main.rs | 3 +- harmony/src/domain/instrumentation.rs | 20 +++- harmony/src/domain/maestro/mod.rs | 21 ++-- harmony/src/domain/topology/k8s_anywhere.rs | 5 +- harmony/src/modules/application/rust.rs | 6 +- harmony_cli/Cargo.toml | 1 + harmony_cli/src/cli_logger.rs | 62 +++++++++-- harmony_cli/src/lib.rs | 1 + harmony_cli/src/progress.rs | 40 +++++++ harmony_composer/Cargo.toml | 1 + .../src/harmony_composer_logger.rs | 101 +++++++++++++----- harmony_composer/src/instrumentation.rs | 10 +- harmony_composer/src/main.rs | 29 ++--- 14 files changed, 240 insertions(+), 72 deletions(-) create mode 100644 harmony_cli/src/progress.rs diff --git a/Cargo.lock b/Cargo.lock index e0d07e6..815110a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1821,6 +1821,7 @@ dependencies = [ "harmony", "harmony_tui", "indicatif", + "indicatif-log-bridge", "inquire", "lazy_static", "log", @@ -1840,6 +1841,7 @@ dependencies = [ "futures-util", "harmony_cli", "indicatif", + "indicatif-log-bridge", "lazy_static", "log", "once_cell", @@ -2451,6 +2453,16 @@ dependencies = [ "web-time", ] +[[package]] +name = "indicatif-log-bridge" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63703cf9069b85dbe6fe26e1c5230d013dee99d3559cd3d02ba39e099ef7ab02" +dependencies = [ + "indicatif", + "log", +] + [[package]] name = "indoc" version = "2.0.6" diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index 465770a..94eb8a7 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -13,8 +13,7 @@ use harmony_cli::cli_logger; #[tokio::main] async fn main() { - env_logger::init(); - let cli_logger_handle = tokio::spawn(cli_logger::init()); + let cli_logger_handle = cli_logger::init(); let topology = K8sAnywhereTopology::from_env(); let mut maestro = Maestro::initialize(Inventory::autoload(), topology) diff --git a/harmony/src/domain/instrumentation.rs b/harmony/src/domain/instrumentation.rs index e3b1546..b116e0f 100644 --- a/harmony/src/domain/instrumentation.rs +++ b/harmony/src/domain/instrumentation.rs @@ -2,19 +2,31 @@ use log::debug; use once_cell::sync::Lazy; use tokio::sync::broadcast; +use super::interpret::InterpretStatus; + #[derive(Debug, Clone)] pub enum HarmonyEvent { - PrepareTopologyStarted { name: String }, + HarmonyStarted, + PrepareTopologyStarted { + name: String, + }, + TopologyPrepared { + name: String, + status: InterpretStatus, + }, } static HARMONY_EVENT_BUS: Lazy> = Lazy::new(|| { // TODO: Adjust channel capacity - let (tx, _rx) = broadcast::channel(18); + let (tx, _rx) = broadcast::channel(100); tx }); -pub fn instrument(event: HarmonyEvent) { - HARMONY_EVENT_BUS.send(event).expect("couldn't send event"); +pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> { + match HARMONY_EVENT_BUS.send(event) { + Ok(_) => Ok(()), + Err(_) => Err("send error: no subscribers"), + } } pub async fn subscribe(name: &str, mut handler: F) diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index d5f46b6..51fbbdf 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -44,13 +44,22 @@ impl Maestro { pub async fn prepare_topology(&self) -> Result { instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted { name: self.topology.name().to_string(), - }); + }) + .unwrap(); + + instrumentation::instrument(HarmonyEvent::TopologyPrepared { + name: self.topology.name().to_string(), + status: InterpretStatus::SUCCESS, + }) + .unwrap(); + let outcome = self.topology.ensure_ready().await?; - info!( - "Topology '{}' readiness check complete: {}", - self.topology.name(), - outcome.status - ); + + instrumentation::instrument(HarmonyEvent::TopologyPrepared { + name: self.topology.name().to_string(), + status: outcome.status.clone(), + }) + .unwrap(); self.topology_preparation_result .lock() diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index 80627c8..dc266bd 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -93,9 +93,8 @@ impl K8sAnywhereTopology { return Err("Failed to run 'helm -version'".to_string()); } - // Print the version output let version_output = String::from_utf8_lossy(&version_result.stdout); - println!("Helm version: {}", version_output.trim()); + debug!("Helm version: {}", version_output.trim()); Ok(()) } @@ -126,7 +125,7 @@ impl K8sAnywhereTopology { // TODO this deserves some refactoring, it is becoming a bit hard to figure out // be careful when making modifications here if k8s_anywhere_config.use_local_k3d { - info!("Using local k3d cluster because of use_local_k3d set to true"); + debug!("Using local k3d cluster because of use_local_k3d set to true"); } else { if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig { debug!("Loading kubeconfig {kubeconfig}"); diff --git a/harmony/src/modules/application/rust.rs b/harmony/src/modules/application/rust.rs index ff67754..69f9293 100644 --- a/harmony/src/modules/application/rust.rs +++ b/harmony/src/modules/application/rust.rs @@ -10,7 +10,7 @@ use dockerfile_builder::Dockerfile; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; use dockerfile_builder::instruction_builder::CopyBuilder; use futures_util::StreamExt; -use log::{debug, error, info}; +use log::{debug, error, info, log_enabled}; use serde::Serialize; use tar::Archive; @@ -164,10 +164,12 @@ impl RustWebapp { let docker = Docker::connect_with_socket_defaults().unwrap(); + let quiet = !log_enabled!(log::Level::Debug); + let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default() .dockerfile("Dockerfile.harmony") .t(image_name) - .q(false) + .q(quiet) .version(bollard::query_parameters::BuilderVersion::BuilderV1) .platform("linux/x86_64"); diff --git a/harmony_cli/Cargo.toml b/harmony_cli/Cargo.toml index 1e51781..227b39e 100644 --- a/harmony_cli/Cargo.toml +++ b/harmony_cli/Cargo.toml @@ -17,6 +17,7 @@ console = "0.16.0" indicatif = "0.18.0" lazy_static = "1.5.0" log.workspace = true +indicatif-log-bridge = "0.2.3" [features] diff --git a/harmony_cli/src/cli_logger.rs b/harmony_cli/src/cli_logger.rs index f4f0df1..ad8551c 100644 --- a/harmony_cli/src/cli_logger.rs +++ b/harmony_cli/src/cli_logger.rs @@ -1,28 +1,68 @@ use harmony::instrumentation::{self, HarmonyEvent}; -use indicatif::ProgressBar; -use std::sync::{Arc, Mutex}; +use indicatif::{MultiProgress, ProgressBar}; +use indicatif_log_bridge::LogWrapper; +use std::{ + collections::{HashMap, hash_map}, + sync::{Arc, Mutex}, +}; -pub async fn init() { - instrumentation::subscribe("CLI Logger", { - let current_spinner = Arc::new(Mutex::new(None::)); +pub fn init() -> tokio::task::JoinHandle<()> { + configure_logger(); + let handle = tokio::spawn(handle_events()); + + loop { + if instrumentation::instrument(HarmonyEvent::HarmonyStarted).is_ok() { + break; + } + } + + handle +} + +fn configure_logger() { + let logger = + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build(); + let level = logger.filter(); + let multi = MultiProgress::new(); + LogWrapper::new(multi.clone(), logger).try_init().unwrap(); + log::set_max_level(level); +} + +async fn handle_events() { + instrumentation::subscribe("Harmony CLI Logger", { + let progresses: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + let topology_prepare_progress = Arc::new(Mutex::new(None::)); move |event| { - let spinner_clone = Arc::clone(¤t_spinner); + let progresses_clone = Arc::clone(&progresses); + let topology_prepare_progress_clone = Arc::clone(&topology_prepare_progress); async move { - let mut spinner_guard = spinner_clone.lock().unwrap(); + let mut progresses = progresses_clone.lock().unwrap(); + let mut topology_prepare_progress = topology_prepare_progress_clone.lock().unwrap(); match event { + HarmonyEvent::HarmonyStarted => {} HarmonyEvent::PrepareTopologyStarted { name } => { - println!( + let multi_progress = crate::progress::new_section(format!( "{} Preparing environment: {name}...", - crate::theme::EMOJI_TOPOLOGY - ); + crate::theme::EMOJI_TOPOLOGY, + )); + (*progresses).insert(name, multi_progress); } + HarmonyEvent::TopologyPrepared { name, status } => match status { + harmony::interpret::InterpretStatus::SUCCESS => todo!(), + harmony::interpret::InterpretStatus::FAILURE => todo!(), + harmony::interpret::InterpretStatus::RUNNING => todo!(), + harmony::interpret::InterpretStatus::QUEUED => todo!(), + harmony::interpret::InterpretStatus::BLOCKED => todo!(), + harmony::interpret::InterpretStatus::NOOP => todo!(), + }, } true } } }) - .await + .await; } diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index 6ef9ac1..c7faccf 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -5,6 +5,7 @@ use harmony::{score::Score, topology::Topology}; use inquire::Confirm; pub mod cli_logger; // FIXME: Don't make me pub +pub mod progress; pub mod theme; #[cfg(feature = "tui")] diff --git a/harmony_cli/src/progress.rs b/harmony_cli/src/progress.rs new file mode 100644 index 0000000..cc5290a --- /dev/null +++ b/harmony_cli/src/progress.rs @@ -0,0 +1,40 @@ +use std::time::Duration; + +use indicatif::{MultiProgress, ProgressBar}; + +pub fn new_section(title: String) -> MultiProgress { + let multi_progress = MultiProgress::new(); + let _ = multi_progress.println(title); + + multi_progress +} + +pub fn add_spinner(multi_progress: &MultiProgress, message: String) -> ProgressBar { + let progress = multi_progress.add(ProgressBar::new_spinner()); + + progress.set_style(crate::theme::SPINNER_STYLE.clone()); + progress.set_message(message); + progress.enable_steady_tick(Duration::from_millis(100)); + + progress +} + +pub fn success(multi_progress: &MultiProgress, progress: Option, message: String) { + if let Some(progress) = progress { + multi_progress.remove(&progress) + } + + let progress = multi_progress.add(ProgressBar::new_spinner()); + progress.set_style(crate::theme::SUCCESS_SPINNER_STYLE.clone()); + progress.finish_with_message(message); +} + +pub fn error(multi_progress: &MultiProgress, progress: Option, message: String) { + if let Some(progress) = progress { + multi_progress.remove(&progress) + } + + let progress = multi_progress.add(ProgressBar::new_spinner()); + progress.set_style(crate::theme::ERROR_SPINNER_STYLE.clone()); + progress.finish_with_message(message); +} diff --git a/harmony_composer/Cargo.toml b/harmony_composer/Cargo.toml index 1ef37f1..df62591 100644 --- a/harmony_composer/Cargo.toml +++ b/harmony_composer/Cargo.toml @@ -20,3 +20,4 @@ console = "0.16.0" lazy_static = "1.5.0" once_cell = "1.21.3" harmony_cli = { path = "../harmony_cli" } +indicatif-log-bridge = "0.2.3" diff --git a/harmony_composer/src/harmony_composer_logger.rs b/harmony_composer/src/harmony_composer_logger.rs index 7641b51..d8003e6 100644 --- a/harmony_composer/src/harmony_composer_logger.rs +++ b/harmony_composer/src/harmony_composer_logger.rs @@ -1,61 +1,106 @@ -use indicatif::ProgressBar; +use indicatif::{MultiProgress, ProgressBar}; +use indicatif_log_bridge::LogWrapper; use log::error; use std::{ + collections::HashMap, sync::{Arc, Mutex}, - time::Duration, }; use crate::instrumentation::{self, HarmonyComposerEvent}; -pub async fn init() { +pub fn init() -> tokio::task::JoinHandle<()> { + configure_logger(); + let handle = tokio::spawn(handle_events()); + + loop { + if instrumentation::instrument(HarmonyComposerEvent::HarmonyComposerStarted).is_ok() { + break; + } + } + + handle +} + +fn configure_logger() { + let logger = + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build(); + let level = logger.filter(); + let multi = MultiProgress::new(); + LogWrapper::new(multi.clone(), logger).try_init().unwrap(); + log::set_max_level(level); +} + +pub async fn handle_events() { + const PROGRESS_SETUP: &str = "project-initialization"; + const PROGRESS_DEPLOYMENT: &str = "deployment"; + instrumentation::subscribe("Harmony Composer Logger", { - let current_spinner = Arc::new(Mutex::new(None::)); + let progresses: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + let compilation_progress = Arc::new(Mutex::new(None::)); move |event| { - let spinner_clone = Arc::clone(¤t_spinner); + let progresses_clone = Arc::clone(&progresses); + let compilation_progress_clone = Arc::clone(&compilation_progress); async move { - let mut spinner_guard = spinner_clone.lock().unwrap(); + let mut progresses_guard = progresses_clone.lock().unwrap(); + let mut compilation_progress_guard = compilation_progress_clone.lock().unwrap(); match event { + HarmonyComposerEvent::HarmonyComposerStarted => {} HarmonyComposerEvent::ProjectInitializationStarted => { - println!( + let multi_progress = harmony_cli::progress::new_section(format!( "{} Initializing Harmony project...", - harmony_cli::theme::EMOJI_HARMONY - ); + harmony_cli::theme::EMOJI_HARMONY, + )); + (*progresses_guard).insert(PROGRESS_SETUP.to_string(), multi_progress); } HarmonyComposerEvent::ProjectInitialized => println!("\n"), HarmonyComposerEvent::ProjectCompilationStarted { details } => { - let progress = ProgressBar::new_spinner(); - progress.set_style(harmony_cli::theme::SPINNER_STYLE.clone()); - progress.set_message(details); - progress.enable_steady_tick(Duration::from_millis(100)); - *spinner_guard = Some(progress); + let initialization_progress = + (*progresses_guard).get(PROGRESS_SETUP).unwrap(); + let _ = initialization_progress.clear(); + + let progress = + harmony_cli::progress::add_spinner(initialization_progress, details); + *compilation_progress_guard = Some(progress); } HarmonyComposerEvent::ProjectCompiled => { - if let Some(progress) = spinner_guard.take() { - progress.set_style(harmony_cli::theme::SUCCESS_SPINNER_STYLE.clone()); - progress.finish_with_message("project compiled"); - } + let initialization_progress = + (*progresses_guard).get(PROGRESS_SETUP).unwrap(); + + harmony_cli::progress::success( + initialization_progress, + (*compilation_progress_guard).take(), + "project compiled".to_string(), + ); } HarmonyComposerEvent::ProjectCompilationFailed { details } => { - if let Some(progress) = spinner_guard.take() { - progress.set_style(harmony_cli::theme::ERROR_SPINNER_STYLE.clone()); - progress.finish_with_message("failed to compile project"); - error!("{details}"); - } + let initialization_progress = + (*progresses_guard).get(PROGRESS_SETUP).unwrap(); + + harmony_cli::progress::error( + initialization_progress, + (*compilation_progress_guard).take(), + "failed to compile project".to_string(), + ); + + error!("{details}"); } HarmonyComposerEvent::DeploymentStarted { target } => { - println!( - "{} Starting deployment to {target}...\n", + let multi_progress = harmony_cli::progress::new_section(format!( + "{} Starting deployment to {target}...\n\n", harmony_cli::theme::EMOJI_DEPLOY - ); + )); + (*progresses_guard).insert(PROGRESS_DEPLOYMENT.to_string(), multi_progress); } HarmonyComposerEvent::DeploymentCompleted { details } => println!("\n"), HarmonyComposerEvent::Shutdown => { - if let Some(progress) = spinner_guard.take() { - progress.abandon(); + for (_, progresses) in (*progresses_guard).iter() { + progresses.clear().unwrap(); } + return false; } } diff --git a/harmony_composer/src/instrumentation.rs b/harmony_composer/src/instrumentation.rs index 171556f..f1cdc8f 100644 --- a/harmony_composer/src/instrumentation.rs +++ b/harmony_composer/src/instrumentation.rs @@ -4,6 +4,7 @@ use tokio::sync::broadcast; #[derive(Debug, Clone)] pub enum HarmonyComposerEvent { + HarmonyComposerStarted, ProjectInitializationStarted, ProjectInitialized, ProjectCompilationStarted { details: String }, @@ -21,10 +22,11 @@ static HARMONY_COMPOSER_EVENT_BUS: Lazy> tx }); -pub fn instrument(event: HarmonyComposerEvent) { - HARMONY_COMPOSER_EVENT_BUS - .send(event) - .expect("couldn't send event"); +pub fn instrument(event: HarmonyComposerEvent) -> Result<(), &'static str> { + match HARMONY_COMPOSER_EVENT_BUS.send(event) { + Ok(_) => Ok(()), + Err(_) => Err("send error: no subscribers"), + } } pub async fn subscribe(name: &str, mut handler: F) diff --git a/harmony_composer/src/main.rs b/harmony_composer/src/main.rs index a2d01e0..4ccccf6 100644 --- a/harmony_composer/src/main.rs +++ b/harmony_composer/src/main.rs @@ -70,15 +70,14 @@ struct AllArgs { #[tokio::main] async fn main() { - env_logger::init(); - let hc_logger_handle = tokio::spawn(harmony_composer_logger::init()); + let hc_logger_handle = harmony_composer_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(HarmonyComposerEvent::ProjectInitializationStarted); + instrumentation::instrument(HarmonyComposerEvent::ProjectInitializationStarted).unwrap(); let harmony_bin_path: PathBuf = match harmony_path { true => { @@ -92,7 +91,7 @@ async fn main() { false => todo!("implement autodetect code"), }; - instrumentation::instrument(HarmonyComposerEvent::ProjectInitialized); + instrumentation::instrument(HarmonyComposerEvent::ProjectInitialized).unwrap(); match cli_args.command { Some(command) => match command { @@ -127,17 +126,20 @@ async fn main() { let deploy = if args.staging { instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { target: "staging".to_string(), - }); + }) + .unwrap(); todo!("implement staging deployment") } else if args.prod { instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { target: "prod".to_string(), - }); + }) + .unwrap(); todo!("implement prod deployment") } else { instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { target: "dev".to_string(), - }); + }) + .unwrap(); Command::new(harmony_bin_path).arg("-y").arg("-a").spawn() } .expect("failed to run harmony deploy"); @@ -145,7 +147,8 @@ async fn main() { let deploy_output = deploy.wait_with_output().unwrap(); instrumentation::instrument(HarmonyComposerEvent::DeploymentCompleted { details: String::from_utf8(deploy_output.stdout).unwrap(), - }); + }) + .unwrap(); } Commands::All(_args) => todo!( "take all previous match arms and turn them into separate functions, and call them all one after the other" @@ -155,7 +158,7 @@ async fn main() { None => todo!("run interactively, ask for info on CLI"), } - instrumentation::instrument(HarmonyComposerEvent::Shutdown); + instrumentation::instrument(HarmonyComposerEvent::Shutdown).unwrap(); let _ = tokio::try_join!(hc_logger_handle); } @@ -198,18 +201,20 @@ async fn compile_harmony( CompileMethod::LocalCargo => { instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted { details: "compiling project with cargo".to_string(), - }); + }) + .unwrap(); compile_cargo(platform, harmony_location).await } CompileMethod::Docker => { instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted { details: "compiling project with docker".to_string(), - }); + }) + .unwrap(); compile_docker(platform, harmony_location).await } }; - instrumentation::instrument(HarmonyComposerEvent::ProjectCompiled); + instrumentation::instrument(HarmonyComposerEvent::ProjectCompiled).unwrap(); path } -- 2.39.5 From 49f1e565991b932ae72de528c67b0b170d7084e4 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Wed, 30 Jul 2025 21:34:27 -0400 Subject: [PATCH 4/7] add event to track progress of interprets, change a bunch of info! to debug! --- harmony/src/domain/instrumentation.rs | 14 +++- harmony/src/domain/maestro/mod.rs | 16 ++-- harmony/src/domain/topology/k8s_anywhere.rs | 25 ++---- .../features/continuous_delivery.rs | 23 +++--- harmony/src/modules/application/mod.rs | 6 +- harmony/src/modules/application/rust.rs | 24 +++--- harmony/src/modules/k3d/install.rs | 31 ++++++-- harmony_cli/src/cli_logger.rs | 77 +++++++++++++++---- harmony_cli/src/lib.rs | 3 +- harmony_cli/src/progress.rs | 10 +++ harmony_cli/src/theme.rs | 4 + k3d/src/lib.rs | 17 ++-- 12 files changed, 158 insertions(+), 92 deletions(-) diff --git a/harmony/src/domain/instrumentation.rs b/harmony/src/domain/instrumentation.rs index b116e0f..d464f05 100644 --- a/harmony/src/domain/instrumentation.rs +++ b/harmony/src/domain/instrumentation.rs @@ -2,7 +2,7 @@ use log::debug; use once_cell::sync::Lazy; use tokio::sync::broadcast; -use super::interpret::InterpretStatus; +use super::interpret::{InterpretError, InterpretStatus, Outcome}; #[derive(Debug, Clone)] pub enum HarmonyEvent { @@ -12,7 +12,17 @@ pub enum HarmonyEvent { }, TopologyPrepared { name: String, - status: InterpretStatus, + outcome: Outcome, + }, + InterpretExecutionStarted { + name: String, + topology: String, + message: String, + }, + InterpretExecutionFinished { + topology: String, + name: String, + outcome: Result, }, } diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 51fbbdf..2fbf786 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, Mutex, RwLock}; -use log::{info, warn}; +use log::{debug, info, warn}; use crate::instrumentation::{self, HarmonyEvent}; @@ -47,17 +47,11 @@ impl Maestro { }) .unwrap(); - instrumentation::instrument(HarmonyEvent::TopologyPrepared { - name: self.topology.name().to_string(), - status: InterpretStatus::SUCCESS, - }) - .unwrap(); - let outcome = self.topology.ensure_ready().await?; instrumentation::instrument(HarmonyEvent::TopologyPrepared { name: self.topology.name().to_string(), - status: outcome.status.clone(), + outcome: outcome.clone(), }) .unwrap(); @@ -93,11 +87,11 @@ impl Maestro { self.topology.name(), ); } - info!("Running score {score:?}"); + debug!("Running score {score:?}"); let interpret = score.create_interpret(); - info!("Launching interpret {interpret:?}"); + debug!("Launching interpret {interpret:?}"); let result = interpret.execute(&self.inventory, &self.topology).await; - info!("Got result {result:?}"); + debug!("Got result {result:?}"); result } diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index dc266bd..ae43b61 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -157,22 +157,13 @@ 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? :") - .with_default(false) - .prompt() - .expect("Unexpected prompt error"); - debug!("Autoinstall confirmation {confirmation}"); - - if !confirmation { - warn!( - "Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client" - ); - return Ok(None); - } + warn!( + "Installation cancelled, K8sAnywhere could not initialize a valid Kubernetes client" + ); + return Ok(None); } - info!("Starting K8sAnywhere installation"); + debug!("Starting K8sAnywhere installation"); self.try_install_k3d().await?; let k3d_score = self.get_k3d_installation_score(); // I feel like having to rely on the k3d_rs crate here is a smell @@ -185,7 +176,7 @@ impl K8sAnywhereTopology { Ok(client) => K8sState { client: Arc::new(K8sClient::new(client)), _source: K8sSource::LocalK3d, - message: "Successfully installed K3D cluster and acquired client".to_string(), + message: "K8s client ready".to_string(), }, Err(_) => todo!(), }; @@ -236,7 +227,7 @@ pub struct K8sAnywhereConfig { /// /// When enabled, autoinstall will setup a K3D cluster on the localhost. https://k3d.io/stable/ /// - /// Default: false + /// Default: true pub autoinstall: bool, /// Whether to use local k3d cluster. @@ -255,7 +246,7 @@ impl K8sAnywhereConfig { use_system_kubeconfig: std::env::var("HARMONY_USE_SYSTEM_KUBECONFIG") .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), autoinstall: std::env::var("HARMONY_AUTOINSTALL") - .map_or_else(|_| false, |v| v.parse().ok().unwrap_or(false)), + .map_or_else(|_| true, |v| v.parse().ok().unwrap_or(false)), // TODO harmony_profile should be managed at a more core level than this harmony_profile: std::env::var("HARMONY_PROFILE").map_or_else( |_| "dev".to_string(), diff --git a/harmony/src/modules/application/features/continuous_delivery.rs b/harmony/src/modules/application/features/continuous_delivery.rs index 346a749..065f0ee 100644 --- a/harmony/src/modules/application/features/continuous_delivery.rs +++ b/harmony/src/modules/application/features/continuous_delivery.rs @@ -1,7 +1,7 @@ use std::{io::Write, process::Command, sync::Arc}; use async_trait::async_trait; -use log::{error, info}; +use log::{debug, error}; use serde_yaml::Value; use tempfile::NamedTempFile; @@ -63,7 +63,7 @@ impl ContinuousDelivery { error!("TODO hardcoded k3d bin path is wrong"); let k3d_bin_path = (*HARMONY_DATA_DIR).join("k3d").join("k3d"); // --- 1. Import the container image into the k3d cluster --- - info!( + debug!( "Importing image '{}' into k3d cluster 'harmony'", image_name ); @@ -80,7 +80,7 @@ impl ContinuousDelivery { } // --- 2. Get the kubeconfig for the k3d cluster and write it to a temp file --- - info!("Retrieving kubeconfig for k3d cluster 'harmony'"); + debug!("Retrieving kubeconfig for k3d cluster 'harmony'"); let kubeconfig_output = Command::new(&k3d_bin_path) .args(["kubeconfig", "get", "harmony"]) .output() @@ -101,7 +101,7 @@ impl ContinuousDelivery { let kubeconfig_path = temp_kubeconfig.path().to_str().unwrap(); // --- 3. Install or upgrade the Helm chart in the cluster --- - info!( + debug!( "Deploying Helm chart '{}' to namespace '{}'", chart_url, app_name ); @@ -131,7 +131,7 @@ impl ContinuousDelivery { )); } - info!("Successfully deployed '{}' to local k3d cluster.", app_name); + debug!("Successfully deployed '{}' to local k3d cluster.", app_name); Ok(()) } } @@ -145,24 +145,19 @@ impl< async fn ensure_installed(&self, topology: &T) -> Result<(), String> { let image = self.application.image_name(); - // TODO - error!( - "TODO reverse helm chart packaging and docker image build. I put helm package first for faster iterations" - ); - // TODO Write CI/CD workflow files // we can autotedect the CI type using the remote url (default to github action for github // url, etc..) // Or ask for it when unknown let helm_chart = self.application.build_push_helm_package(&image).await?; - info!("Pushed new helm chart {helm_chart}"); + debug!("Pushed new helm chart {helm_chart}"); error!("TODO Make building image configurable/skippable if image already exists (prompt)"); let image = self.application.build_push_oci_image().await?; - info!("Pushed new docker image {image}"); + debug!("Pushed new docker image {image}"); - info!("Installing ContinuousDelivery feature"); + debug!("Installing ContinuousDelivery feature"); // TODO this is a temporary hack for demo purposes, the deployment target should be driven // by the topology only and we should not have to know how to perform tasks like this for // which the topology should be responsible. @@ -182,7 +177,7 @@ impl< .await?; } target => { - info!("Deploying to target {target:?}"); + debug!("Deploying to target {target:?}"); let score = ArgoHelmScore { namespace: "harmonydemo-staging".to_string(), openshift: false, diff --git a/harmony/src/modules/application/mod.rs b/harmony/src/modules/application/mod.rs index 3788217..fe98d46 100644 --- a/harmony/src/modules/application/mod.rs +++ b/harmony/src/modules/application/mod.rs @@ -5,7 +5,7 @@ mod rust; use std::sync::Arc; pub use feature::*; -use log::info; +use log::debug; pub use oci::*; pub use rust::*; @@ -36,7 +36,7 @@ impl Interpret for Application topology: &T, ) -> Result { let app_name = self.application.name(); - info!( + debug!( "Preparing {} features [{}] for application {app_name}", self.features.len(), self.features @@ -46,7 +46,7 @@ impl Interpret for Application .join(", ") ); for feature in self.features.iter() { - info!( + debug!( "Installing feature {} for application {app_name}", feature.name() ); diff --git a/harmony/src/modules/application/rust.rs b/harmony/src/modules/application/rust.rs index 69f9293..6fc72cb 100644 --- a/harmony/src/modules/application/rust.rs +++ b/harmony/src/modules/application/rust.rs @@ -10,7 +10,7 @@ use dockerfile_builder::Dockerfile; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; use dockerfile_builder::instruction_builder::CopyBuilder; use futures_util::StreamExt; -use log::{debug, error, info, log_enabled}; +use log::{debug, error, log_enabled}; use serde::Serialize; use tar::Archive; @@ -73,19 +73,19 @@ impl Application for RustWebapp { #[async_trait] impl HelmPackage for RustWebapp { async fn build_push_helm_package(&self, image_url: &str) -> Result { - info!("Starting Helm chart build and push for '{}'", self.name); + debug!("Starting Helm chart build and push for '{}'", self.name); // 1. Create the Helm chart files on disk. let chart_dir = self .create_helm_chart_files(image_url) .map_err(|e| format!("Failed to create Helm chart files: {}", e))?; - info!("Successfully created Helm chart files in {:?}", chart_dir); + debug!("Successfully created Helm chart files in {:?}", chart_dir); // 2. Package the chart into a .tgz archive. let packaged_chart_path = self .package_helm_chart(&chart_dir) .map_err(|e| format!("Failed to package Helm chart: {}", e))?; - info!( + debug!( "Successfully packaged Helm chart: {}", packaged_chart_path.to_string_lossy() ); @@ -94,7 +94,7 @@ impl HelmPackage for RustWebapp { let oci_chart_url = self .push_helm_chart(&packaged_chart_path) .map_err(|e| format!("Failed to push Helm chart: {}", e))?; - info!("Successfully pushed Helm chart to: {}", oci_chart_url); + debug!("Successfully pushed Helm chart to: {}", oci_chart_url); Ok(oci_chart_url) } @@ -107,20 +107,20 @@ impl OCICompliant for RustWebapp { async fn build_push_oci_image(&self) -> Result { // This function orchestrates the build and push process. // It's async to match the trait definition, though the underlying docker commands are blocking. - info!("Starting OCI image build and push for '{}'", self.name); + debug!("Starting OCI image build and push for '{}'", self.name); // 1. Build the image by calling the synchronous helper function. let image_tag = self.image_name(); self.build_docker_image(&image_tag) .await .map_err(|e| format!("Failed to build Docker image: {}", e))?; - info!("Successfully built Docker image: {}", image_tag); + debug!("Successfully built Docker image: {}", image_tag); // 2. Push the image to the registry. self.push_docker_image(&image_tag) .await .map_err(|e| format!("Failed to push Docker image: {}", e))?; - info!("Successfully pushed Docker image to: {}", image_tag); + debug!("Successfully pushed Docker image to: {}", image_tag); Ok(image_tag) } @@ -159,7 +159,7 @@ impl RustWebapp { &self, image_name: &str, ) -> Result> { - info!("Generating Dockerfile for '{}'", self.name); + debug!("Generating Dockerfile for '{}'", self.name); let _dockerfile_path = self.build_dockerfile()?; let docker = Docker::connect_with_socket_defaults().unwrap(); @@ -206,7 +206,7 @@ impl RustWebapp { &self, image_tag: &str, ) -> Result> { - info!("Pushing docker image {image_tag}"); + debug!("Pushing docker image {image_tag}"); let docker = Docker::connect_with_socket_defaults().unwrap(); @@ -535,7 +535,7 @@ spec: chart_dir: &PathBuf, ) -> Result> { let chart_dirname = chart_dir.file_name().expect("Should find a chart dirname"); - info!( + debug!( "Launching `helm package {}` cli with CWD {}", chart_dirname.to_string_lossy(), &self @@ -580,7 +580,7 @@ spec: let oci_push_url = format!("oci://{}/{}", *REGISTRY_URL, *REGISTRY_PROJECT); let oci_pull_url = format!("{oci_push_url}/{}-chart", self.name); - info!( + debug!( "Pushing Helm chart {} to {}", packaged_chart_path.to_string_lossy(), oci_push_url diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 18b91a0..36a0472 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -1,12 +1,13 @@ use std::path::PathBuf; use async_trait::async_trait; -use log::info; +use log::{debug, info}; use serde::Serialize; use crate::{ config::HARMONY_DATA_DIR, data::{Id, Version}, + instrumentation::{self, HarmonyEvent}, interpret::{Interpret, InterpretError, InterpretName, InterpretStatus, Outcome}, inventory::Inventory, score::Score, @@ -50,22 +51,38 @@ impl Interpret for K3dInstallationInterpret { async fn execute( &self, _inventory: &Inventory, - _topology: &T, + topology: &T, ) -> Result { + instrumentation::instrument(HarmonyEvent::InterpretExecutionStarted { + topology: topology.name().into(), + name: "k3d-installation".into(), + message: "installing k3d...".into(), + }) + .unwrap(); + let k3d = k3d_rs::K3d::new( self.score.installation_path.clone(), Some(self.score.cluster_name.clone()), ); - match k3d.ensure_installed().await { + let outcome = match k3d.ensure_installed().await { Ok(_client) => { - let msg = format!("k3d cluster {} is installed ", self.score.cluster_name); - info!("{msg}"); + let msg = format!("k3d cluster '{}' installed ", self.score.cluster_name); + debug!("{msg}"); Ok(Outcome::success(msg)) } Err(msg) => Err(InterpretError::new(format!( - "K3dInstallationInterpret failed to ensure k3d is installed : {msg}" + "failed to ensure k3d is installed : {msg}" ))), - } + }; + + instrumentation::instrument(HarmonyEvent::InterpretExecutionFinished { + topology: topology.name().into(), + name: "k3d-installation".into(), + outcome: outcome.clone(), + }) + .unwrap(); + + outcome } fn get_name(&self) -> InterpretName { InterpretName::K3dInstallation diff --git a/harmony_cli/src/cli_logger.rs b/harmony_cli/src/cli_logger.rs index ad8551c..d7f8b22 100644 --- a/harmony_cli/src/cli_logger.rs +++ b/harmony_cli/src/cli_logger.rs @@ -6,6 +6,8 @@ use std::{ sync::{Arc, Mutex}, }; +use crate::progress; + pub fn init() -> tokio::task::JoinHandle<()> { configure_logger(); let handle = tokio::spawn(handle_events()); @@ -30,35 +32,78 @@ fn configure_logger() { async fn handle_events() { instrumentation::subscribe("Harmony CLI Logger", { - let progresses: Arc>> = + let sections: Arc>> = + Arc::new(Mutex::new(HashMap::new())); + let progress_bars: Arc>> = Arc::new(Mutex::new(HashMap::new())); - let topology_prepare_progress = Arc::new(Mutex::new(None::)); move |event| { - let progresses_clone = Arc::clone(&progresses); - let topology_prepare_progress_clone = Arc::clone(&topology_prepare_progress); + let sections_clone = Arc::clone(§ions); + let progress_bars_clone = Arc::clone(&progress_bars); async move { - let mut progresses = progresses_clone.lock().unwrap(); - let mut topology_prepare_progress = topology_prepare_progress_clone.lock().unwrap(); + let mut sections = sections_clone.lock().unwrap(); + let mut progress_bars = progress_bars_clone.lock().unwrap(); match event { HarmonyEvent::HarmonyStarted => {} HarmonyEvent::PrepareTopologyStarted { name } => { - let multi_progress = crate::progress::new_section(format!( + let section = progress::new_section(format!( "{} Preparing environment: {name}...", crate::theme::EMOJI_TOPOLOGY, )); - (*progresses).insert(name, multi_progress); + (*sections).insert(name, section); + } + HarmonyEvent::TopologyPrepared { name, outcome } => { + let section = (*sections).get(&name).unwrap(); + let progress = progress::add_spinner(section, "".into()); + + match outcome.status { + harmony::interpret::InterpretStatus::SUCCESS => { + progress::success(section, Some(progress), outcome.message); + } + harmony::interpret::InterpretStatus::FAILURE => { + progress::error(section, Some(progress), outcome.message); + } + harmony::interpret::InterpretStatus::RUNNING => todo!(), + harmony::interpret::InterpretStatus::QUEUED => todo!(), + harmony::interpret::InterpretStatus::BLOCKED => todo!(), + harmony::interpret::InterpretStatus::NOOP => { + progress::skip(section, Some(progress), outcome.message); + } + } + } + HarmonyEvent::InterpretExecutionStarted { + name, + topology, + message, + } => { + let section = (*sections).get(&topology).unwrap(); + let progress_bar = progress::add_spinner(section, message); + + (*progress_bars).insert(name, progress_bar); + } + HarmonyEvent::InterpretExecutionFinished { + topology, + name, + outcome, + } => { + let section = (*sections).get(&topology).unwrap(); + let progress_bar = (*progress_bars).get(&name).cloned(); + + let _ = section.clear(); + + match outcome { + Ok(outcome) => { + progress::success(section, progress_bar, outcome.message); + } + Err(err) => { + progress::error(section, progress_bar, err.to_string()); + } + } + + (*progress_bars).remove(&name); } - HarmonyEvent::TopologyPrepared { name, status } => match status { - harmony::interpret::InterpretStatus::SUCCESS => todo!(), - harmony::interpret::InterpretStatus::FAILURE => todo!(), - harmony::interpret::InterpretStatus::RUNNING => todo!(), - harmony::interpret::InterpretStatus::QUEUED => todo!(), - harmony::interpret::InterpretStatus::BLOCKED => todo!(), - harmony::interpret::InterpretStatus::NOOP => todo!(), - }, } true } diff --git a/harmony_cli/src/lib.rs b/harmony_cli/src/lib.rs index c7faccf..4d71a6e 100644 --- a/harmony_cli/src/lib.rs +++ b/harmony_cli/src/lib.rs @@ -10,6 +10,7 @@ pub mod theme; #[cfg(feature = "tui")] use harmony_tui; +use log::debug; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -138,7 +139,7 @@ pub async fn init( // Run filtered scores for s in scores_vec { - println!("Running: {}", s.name()); + debug!("Running: {}", s.name()); maestro.interpret(s).await?; } diff --git a/harmony_cli/src/progress.rs b/harmony_cli/src/progress.rs index cc5290a..4008bc8 100644 --- a/harmony_cli/src/progress.rs +++ b/harmony_cli/src/progress.rs @@ -38,3 +38,13 @@ pub fn error(multi_progress: &MultiProgress, progress: Option, mess progress.set_style(crate::theme::ERROR_SPINNER_STYLE.clone()); progress.finish_with_message(message); } + +pub fn skip(multi_progress: &MultiProgress, progress: Option, message: String) { + if let Some(progress) = progress { + multi_progress.remove(&progress) + } + + let progress = multi_progress.add(ProgressBar::new_spinner()); + progress.set_style(crate::theme::SKIP_SPINNER_STYLE.clone()); + progress.finish_with_message(message); +} diff --git a/harmony_cli/src/theme.rs b/harmony_cli/src/theme.rs index a2215d0..ee25077 100644 --- a/harmony_cli/src/theme.rs +++ b/harmony_cli/src/theme.rs @@ -4,6 +4,7 @@ use lazy_static::lazy_static; pub static EMOJI_HARMONY: Emoji<'_, '_> = Emoji("🎼", ""); pub static EMOJI_SUCCESS: Emoji<'_, '_> = Emoji("✅", ""); +pub static EMOJI_SKIP: Emoji<'_, '_> = Emoji("⏭️", ""); pub static EMOJI_ERROR: Emoji<'_, '_> = Emoji("⚠️", ""); pub static EMOJI_DEPLOY: Emoji<'_, '_> = Emoji("🚀", ""); pub static EMOJI_TOPOLOGY: Emoji<'_, '_> = Emoji("📦", ""); @@ -16,6 +17,9 @@ lazy_static! { pub static ref SUCCESS_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE .clone() .tick_strings(&[format!("{}", EMOJI_SUCCESS).as_str()]); + pub static ref SKIP_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE + .clone() + .tick_strings(&[format!("{}", EMOJI_SKIP).as_str()]); pub static ref ERROR_SPINNER_STYLE: ProgressStyle = SPINNER_STYLE .clone() .tick_strings(&[format!("{}", EMOJI_ERROR).as_str()]); diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs index ff63704..ce1b569 100644 --- a/k3d/src/lib.rs +++ b/k3d/src/lib.rs @@ -2,7 +2,7 @@ mod downloadable_asset; use downloadable_asset::*; use kube::Client; -use log::{debug, info, warn}; +use log::{debug, warn}; use std::path::PathBuf; const K3D_BIN_FILE_NAME: &str = "k3d"; @@ -90,7 +90,7 @@ impl K3d { let latest_release = self.get_latest_release_tag().await.unwrap(); let release_binary = self.get_binary_for_current_platform(latest_release).await; - info!("Foudn K3d binary to install : {release_binary:#?}"); + debug!("Foudn K3d binary to install : {release_binary:#?}"); release_binary.download_to_path(self.base_dir.clone()).await } @@ -175,7 +175,7 @@ impl K3d { Err(_) => return Err("Could not get cluster_name, cannot initialize".to_string()), }; - info!("Initializing k3d cluster '{}'", cluster_name); + debug!("Initializing k3d cluster '{}'", cluster_name); self.create_cluster(cluster_name)?; self.create_kubernetes_client().await @@ -205,7 +205,7 @@ impl K3d { /// - `Err(String)` - Error message if any step failed pub async fn ensure_installed(&self) -> Result { if !self.is_installed() { - info!("K3d is not installed, downloading latest release"); + debug!("K3d is not installed, downloading latest release"); self.download_latest_release() .await .map_err(|e| format!("Failed to download k3d: {}", e))?; @@ -216,13 +216,13 @@ impl K3d { } if !self.is_cluster_initialized() { - info!("Cluster is not initialized, initializing now"); + debug!("Cluster is not initialized, initializing now"); return self.initialize_cluster().await; } self.start_cluster().await?; - info!("K3d and cluster are already properly set up"); + debug!("K3d and cluster are already properly set up"); self.create_kubernetes_client().await } @@ -325,12 +325,11 @@ impl K3d { return Err(format!("Failed to create cluster: {}", stderr)); } - info!("Successfully created k3d cluster '{}'", cluster_name); + debug!("Successfully created k3d cluster '{}'", cluster_name); Ok(()) } async fn create_kubernetes_client(&self) -> Result { - warn!("TODO this method is way too dumb, it should make sure that the client is connected to the k3d cluster actually represented by this instance, not just any default client"); Client::try_default() .await .map_err(|e| format!("Failed to create Kubernetes client: {}", e)) @@ -352,7 +351,7 @@ impl K3d { return Err(format!("Failed to start cluster: {}", stderr)); } - info!("Successfully started k3d cluster '{}'", cluster_name); + debug!("Successfully started k3d cluster '{}'", cluster_name); Ok(()) } } -- 2.39.5 From 68fde23f2c953d496ecec9b6e7dc8e33fbaf671c Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Wed, 30 Jul 2025 21:42:09 -0400 Subject: [PATCH 5/7] remove unused inquire dependency for Harmony --- Cargo.lock | 1 - harmony/Cargo.toml | 13 ++++++------- harmony/src/domain/topology/k8s_anywhere.rs | 1 - 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 815110a..529714b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1776,7 +1776,6 @@ dependencies = [ "helm-wrapper-rs", "hex", "http 1.3.1", - "inquire", "k3d-rs", "k8s-openapi", "kube", diff --git a/harmony/Cargo.toml b/harmony/Cargo.toml index daa9fd3..e4686ef 100644 --- a/harmony/Cargo.toml +++ b/harmony/Cargo.toml @@ -32,7 +32,6 @@ k8s-openapi.workspace = true serde_yaml.workspace = true http.workspace = true serde-value.workspace = true -inquire.workspace = true helm-wrapper-rs = "0.4.0" non-blank-string-rs = "1.0.4" k3d-rs = { path = "../k3d" } @@ -44,12 +43,12 @@ convert_case.workspace = true email_address = "0.2.9" chrono.workspace = true fqdn = { version = "0.4.6", features = [ - "domain-label-cannot-start-or-end-with-hyphen", - "domain-label-length-limited-to-63", - "domain-name-without-special-chars", - "domain-name-length-limited-to-255", - "punycode", - "serde", + "domain-label-cannot-start-or-end-with-hyphen", + "domain-label-length-limited-to-63", + "domain-name-without-special-chars", + "domain-name-length-limited-to-255", + "punycode", + "serde", ] } temp-dir = "0.1.14" dyn-clone = "1.0.19" diff --git a/harmony/src/domain/topology/k8s_anywhere.rs b/harmony/src/domain/topology/k8s_anywhere.rs index ae43b61..6719eac 100644 --- a/harmony/src/domain/topology/k8s_anywhere.rs +++ b/harmony/src/domain/topology/k8s_anywhere.rs @@ -1,7 +1,6 @@ use std::{process::Command, sync::Arc}; use async_trait::async_trait; -use inquire::Confirm; use log::{debug, info, warn}; use serde::Serialize; use tokio::sync::OnceCell; -- 2.39.5 From 507556969a43282f0e58628f3c36b1cd3831651f Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Thu, 31 Jul 2025 08:05:06 -0400 Subject: [PATCH 6/7] add k3d todo --- k3d/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/k3d/src/lib.rs b/k3d/src/lib.rs index ce1b569..99ddacc 100644 --- a/k3d/src/lib.rs +++ b/k3d/src/lib.rs @@ -330,6 +330,7 @@ impl K3d { } async fn create_kubernetes_client(&self) -> Result { + // TODO: Connect the client to the right k3d cluster (see https://git.nationtech.io/NationTech/harmony/issues/92) Client::try_default() .await .map_err(|e| format!("Failed to create Kubernetes client: {}", e)) -- 2.39.5 From 6b36b1c7e9f120bbc8a9e7f9b2feb8492a1ad718 Mon Sep 17 00:00:00 2001 From: Ian Letourneau Date: Thu, 31 Jul 2025 09:24:05 -0400 Subject: [PATCH 7/7] rename some event attributes --- harmony/src/domain/instrumentation.rs | 10 +++++----- harmony/src/domain/maestro/mod.rs | 4 ++-- harmony/src/modules/k3d/install.rs | 4 ++-- harmony_cli/src/cli_logger.rs | 11 +++++++---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/harmony/src/domain/instrumentation.rs b/harmony/src/domain/instrumentation.rs index d464f05..3d45722 100644 --- a/harmony/src/domain/instrumentation.rs +++ b/harmony/src/domain/instrumentation.rs @@ -2,26 +2,26 @@ use log::debug; use once_cell::sync::Lazy; use tokio::sync::broadcast; -use super::interpret::{InterpretError, InterpretStatus, Outcome}; +use super::interpret::{InterpretError, Outcome}; #[derive(Debug, Clone)] pub enum HarmonyEvent { HarmonyStarted, PrepareTopologyStarted { - name: String, + topology: String, }, TopologyPrepared { - name: String, + topology: String, outcome: Outcome, }, InterpretExecutionStarted { - name: String, topology: String, + interpret: String, message: String, }, InterpretExecutionFinished { topology: String, - name: String, + interpret: String, outcome: Result, }, } diff --git a/harmony/src/domain/maestro/mod.rs b/harmony/src/domain/maestro/mod.rs index 2fbf786..a7f2b60 100644 --- a/harmony/src/domain/maestro/mod.rs +++ b/harmony/src/domain/maestro/mod.rs @@ -43,14 +43,14 @@ impl Maestro { /// Delegates the readiness check and potential setup actions to the Topology. pub async fn prepare_topology(&self) -> Result { instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted { - name: self.topology.name().to_string(), + topology: self.topology.name().to_string(), }) .unwrap(); let outcome = self.topology.ensure_ready().await?; instrumentation::instrument(HarmonyEvent::TopologyPrepared { - name: self.topology.name().to_string(), + topology: self.topology.name().to_string(), outcome: outcome.clone(), }) .unwrap(); diff --git a/harmony/src/modules/k3d/install.rs b/harmony/src/modules/k3d/install.rs index 36a0472..6441305 100644 --- a/harmony/src/modules/k3d/install.rs +++ b/harmony/src/modules/k3d/install.rs @@ -55,7 +55,7 @@ impl Interpret for K3dInstallationInterpret { ) -> Result { instrumentation::instrument(HarmonyEvent::InterpretExecutionStarted { topology: topology.name().into(), - name: "k3d-installation".into(), + interpret: "k3d-installation".into(), message: "installing k3d...".into(), }) .unwrap(); @@ -77,7 +77,7 @@ impl Interpret for K3dInstallationInterpret { instrumentation::instrument(HarmonyEvent::InterpretExecutionFinished { topology: topology.name().into(), - name: "k3d-installation".into(), + interpret: "k3d-installation".into(), outcome: outcome.clone(), }) .unwrap(); diff --git a/harmony_cli/src/cli_logger.rs b/harmony_cli/src/cli_logger.rs index d7f8b22..ebe8f09 100644 --- a/harmony_cli/src/cli_logger.rs +++ b/harmony_cli/src/cli_logger.rs @@ -47,14 +47,17 @@ async fn handle_events() { match event { HarmonyEvent::HarmonyStarted => {} - HarmonyEvent::PrepareTopologyStarted { name } => { + HarmonyEvent::PrepareTopologyStarted { topology: name } => { let section = progress::new_section(format!( "{} Preparing environment: {name}...", crate::theme::EMOJI_TOPOLOGY, )); (*sections).insert(name, section); } - HarmonyEvent::TopologyPrepared { name, outcome } => { + HarmonyEvent::TopologyPrepared { + topology: name, + outcome, + } => { let section = (*sections).get(&name).unwrap(); let progress = progress::add_spinner(section, "".into()); @@ -74,7 +77,7 @@ async fn handle_events() { } } HarmonyEvent::InterpretExecutionStarted { - name, + interpret: name, topology, message, } => { @@ -85,7 +88,7 @@ async fn handle_events() { } HarmonyEvent::InterpretExecutionFinished { topology, - name, + interpret: name, outcome, } => { let section = (*sections).get(&topology).unwrap(); -- 2.39.5