fix(cli): reduce noise & better track progress within Harmony #91

Merged
letian merged 8 commits from better-cli into master 2025-07-31 19:35:36 +00:00
14 changed files with 240 additions and 72 deletions
Showing only changes of commit ff7801a7c1 - Show all commits

12
Cargo.lock generated
View File

@ -1821,6 +1821,7 @@ dependencies = [
"harmony", "harmony",
"harmony_tui", "harmony_tui",
"indicatif", "indicatif",
"indicatif-log-bridge",
"inquire", "inquire",
"lazy_static", "lazy_static",
"log", "log",
@ -1840,6 +1841,7 @@ dependencies = [
"futures-util", "futures-util",
"harmony_cli", "harmony_cli",
"indicatif", "indicatif",
"indicatif-log-bridge",
"lazy_static", "lazy_static",
"log", "log",
"once_cell", "once_cell",
@ -2451,6 +2453,16 @@ dependencies = [
"web-time", "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]] [[package]]
name = "indoc" name = "indoc"
version = "2.0.6" version = "2.0.6"

View File

@ -13,8 +13,7 @@ use harmony_cli::cli_logger;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); let cli_logger_handle = cli_logger::init();
let cli_logger_handle = tokio::spawn(cli_logger::init());
let topology = K8sAnywhereTopology::from_env(); let topology = K8sAnywhereTopology::from_env();
let mut maestro = Maestro::initialize(Inventory::autoload(), topology) let mut maestro = Maestro::initialize(Inventory::autoload(), topology)

View File

@ -2,19 +2,31 @@ use log::debug;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use super::interpret::InterpretStatus;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum HarmonyEvent { pub enum HarmonyEvent {
PrepareTopologyStarted { name: String }, HarmonyStarted,
PrepareTopologyStarted {
name: String,
},
TopologyPrepared {
name: String,
status: InterpretStatus,
},
} }
static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| { static HARMONY_EVENT_BUS: Lazy<broadcast::Sender<HarmonyEvent>> = Lazy::new(|| {
// TODO: Adjust channel capacity // TODO: Adjust channel capacity
let (tx, _rx) = broadcast::channel(18); let (tx, _rx) = broadcast::channel(100);
tx tx
}); });
pub fn instrument(event: HarmonyEvent) { pub fn instrument(event: HarmonyEvent) -> Result<(), &'static str> {
HARMONY_EVENT_BUS.send(event).expect("couldn't send event"); match HARMONY_EVENT_BUS.send(event) {
Ok(_) => Ok(()),
Err(_) => Err("send error: no subscribers"),
}
} }
pub async fn subscribe<F, Fut>(name: &str, mut handler: F) pub async fn subscribe<F, Fut>(name: &str, mut handler: F)

View File

@ -44,13 +44,22 @@ impl<T: Topology> Maestro<T> {
pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> { pub async fn prepare_topology(&self) -> Result<Outcome, InterpretError> {
instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted { instrumentation::instrument(HarmonyEvent::PrepareTopologyStarted {
name: self.topology.name().to_string(), 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?; let outcome = self.topology.ensure_ready().await?;
info!(
"Topology '{}' readiness check complete: {}", instrumentation::instrument(HarmonyEvent::TopologyPrepared {
self.topology.name(), name: self.topology.name().to_string(),
outcome.status status: outcome.status.clone(),
); })
.unwrap();
self.topology_preparation_result self.topology_preparation_result
.lock() .lock()

View File

@ -93,9 +93,8 @@ impl K8sAnywhereTopology {
return Err("Failed to run 'helm -version'".to_string()); return Err("Failed to run 'helm -version'".to_string());
} }
// Print the version output
let version_output = String::from_utf8_lossy(&version_result.stdout); let version_output = String::from_utf8_lossy(&version_result.stdout);
println!("Helm version: {}", version_output.trim()); debug!("Helm version: {}", version_output.trim());
Ok(()) Ok(())
} }
@ -126,7 +125,7 @@ impl K8sAnywhereTopology {
// TODO this deserves some refactoring, it is becoming a bit hard to figure out // TODO this deserves some refactoring, it is becoming a bit hard to figure out
// be careful when making modifications here // be careful when making modifications here
if k8s_anywhere_config.use_local_k3d { 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 { } else {
if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig { if let Some(kubeconfig) = &k8s_anywhere_config.kubeconfig {
debug!("Loading kubeconfig {kubeconfig}"); debug!("Loading kubeconfig {kubeconfig}");

View File

@ -10,7 +10,7 @@ use dockerfile_builder::Dockerfile;
use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR}; use dockerfile_builder::instruction::{CMD, COPY, ENV, EXPOSE, FROM, RUN, USER, WORKDIR};
use dockerfile_builder::instruction_builder::CopyBuilder; use dockerfile_builder::instruction_builder::CopyBuilder;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{debug, error, info}; use log::{debug, error, info, log_enabled};
use serde::Serialize; use serde::Serialize;
use tar::Archive; use tar::Archive;
@ -164,10 +164,12 @@ impl RustWebapp {
let docker = Docker::connect_with_socket_defaults().unwrap(); let docker = Docker::connect_with_socket_defaults().unwrap();
let quiet = !log_enabled!(log::Level::Debug);
let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default() let build_image_options = bollard::query_parameters::BuildImageOptionsBuilder::default()
.dockerfile("Dockerfile.harmony") .dockerfile("Dockerfile.harmony")
.t(image_name) .t(image_name)
.q(false) .q(quiet)
.version(bollard::query_parameters::BuilderVersion::BuilderV1) .version(bollard::query_parameters::BuilderVersion::BuilderV1)
.platform("linux/x86_64"); .platform("linux/x86_64");

View File

@ -17,6 +17,7 @@ console = "0.16.0"
indicatif = "0.18.0" indicatif = "0.18.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
log.workspace = true log.workspace = true
indicatif-log-bridge = "0.2.3"
[features] [features]

View File

@ -1,28 +1,68 @@
use harmony::instrumentation::{self, HarmonyEvent}; use harmony::instrumentation::{self, HarmonyEvent};
use indicatif::ProgressBar; use indicatif::{MultiProgress, ProgressBar};
use std::sync::{Arc, Mutex}; use indicatif_log_bridge::LogWrapper;
use std::{
collections::{HashMap, hash_map},
sync::{Arc, Mutex},
};
pub async fn init() { pub fn init() -> tokio::task::JoinHandle<()> {
instrumentation::subscribe("CLI Logger", { configure_logger();
let current_spinner = Arc::new(Mutex::new(None::<ProgressBar>)); 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<Mutex<HashMap<String, MultiProgress>>> =
Arc::new(Mutex::new(HashMap::new()));
let topology_prepare_progress = Arc::new(Mutex::new(None::<ProgressBar>));
move |event| { move |event| {
let spinner_clone = Arc::clone(&current_spinner); let progresses_clone = Arc::clone(&progresses);
let topology_prepare_progress_clone = Arc::clone(&topology_prepare_progress);
async move { 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 { match event {
HarmonyEvent::HarmonyStarted => {}
HarmonyEvent::PrepareTopologyStarted { name } => { HarmonyEvent::PrepareTopologyStarted { name } => {
println!( let multi_progress = crate::progress::new_section(format!(
"{} Preparing environment: {name}...", "{} 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 true
} }
} }
}) })
.await .await;
} }

View File

@ -5,6 +5,7 @@ use harmony::{score::Score, topology::Topology};
use inquire::Confirm; use inquire::Confirm;
pub mod cli_logger; // FIXME: Don't make me pub pub mod cli_logger; // FIXME: Don't make me pub
Review

See the PR comment above (in examples/rust/main.rs) for more info

See the PR comment above (in `examples/rust/main.rs`) for more info
pub mod progress;
pub mod theme; pub mod theme;
#[cfg(feature = "tui")] #[cfg(feature = "tui")]

View File

@ -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<ProgressBar>, 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<ProgressBar>, 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);
}

View File

@ -20,3 +20,4 @@ console = "0.16.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
once_cell = "1.21.3" once_cell = "1.21.3"
harmony_cli = { path = "../harmony_cli" } harmony_cli = { path = "../harmony_cli" }
indicatif-log-bridge = "0.2.3"

View File

@ -1,61 +1,106 @@
use indicatif::ProgressBar; use indicatif::{MultiProgress, ProgressBar};
use indicatif_log_bridge::LogWrapper;
use log::error; use log::error;
use std::{ use std::{
collections::HashMap,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Duration,
}; };
use crate::instrumentation::{self, HarmonyComposerEvent}; 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", { instrumentation::subscribe("Harmony Composer Logger", {
let current_spinner = Arc::new(Mutex::new(None::<ProgressBar>)); let progresses: Arc<Mutex<HashMap<String, MultiProgress>>> =
Arc::new(Mutex::new(HashMap::new()));
let compilation_progress = Arc::new(Mutex::new(None::<ProgressBar>));
move |event| { move |event| {
let spinner_clone = Arc::clone(&current_spinner); let progresses_clone = Arc::clone(&progresses);
let compilation_progress_clone = Arc::clone(&compilation_progress);
async move { 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 { match event {
HarmonyComposerEvent::HarmonyComposerStarted => {}
HarmonyComposerEvent::ProjectInitializationStarted => { HarmonyComposerEvent::ProjectInitializationStarted => {
println!( let multi_progress = harmony_cli::progress::new_section(format!(
"{} Initializing Harmony project...", "{} 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::ProjectInitialized => println!("\n"),
HarmonyComposerEvent::ProjectCompilationStarted { details } => { HarmonyComposerEvent::ProjectCompilationStarted { details } => {
let progress = ProgressBar::new_spinner(); let initialization_progress =
progress.set_style(harmony_cli::theme::SPINNER_STYLE.clone()); (*progresses_guard).get(PROGRESS_SETUP).unwrap();
progress.set_message(details); let _ = initialization_progress.clear();
progress.enable_steady_tick(Duration::from_millis(100));
*spinner_guard = Some(progress); let progress =
harmony_cli::progress::add_spinner(initialization_progress, details);
*compilation_progress_guard = Some(progress);
} }
HarmonyComposerEvent::ProjectCompiled => { HarmonyComposerEvent::ProjectCompiled => {
if let Some(progress) = spinner_guard.take() { let initialization_progress =
progress.set_style(harmony_cli::theme::SUCCESS_SPINNER_STYLE.clone()); (*progresses_guard).get(PROGRESS_SETUP).unwrap();
progress.finish_with_message("project compiled");
} harmony_cli::progress::success(
initialization_progress,
(*compilation_progress_guard).take(),
"project compiled".to_string(),
);
} }
HarmonyComposerEvent::ProjectCompilationFailed { details } => { HarmonyComposerEvent::ProjectCompilationFailed { details } => {
if let Some(progress) = spinner_guard.take() { let initialization_progress =
progress.set_style(harmony_cli::theme::ERROR_SPINNER_STYLE.clone()); (*progresses_guard).get(PROGRESS_SETUP).unwrap();
progress.finish_with_message("failed to compile project");
error!("{details}"); harmony_cli::progress::error(
} initialization_progress,
(*compilation_progress_guard).take(),
"failed to compile project".to_string(),
);
error!("{details}");
} }
HarmonyComposerEvent::DeploymentStarted { target } => { HarmonyComposerEvent::DeploymentStarted { target } => {
println!( let multi_progress = harmony_cli::progress::new_section(format!(
"{} Starting deployment to {target}...\n", "{} Starting deployment to {target}...\n\n",
harmony_cli::theme::EMOJI_DEPLOY harmony_cli::theme::EMOJI_DEPLOY
); ));
(*progresses_guard).insert(PROGRESS_DEPLOYMENT.to_string(), multi_progress);
} }
HarmonyComposerEvent::DeploymentCompleted { details } => println!("\n"), HarmonyComposerEvent::DeploymentCompleted { details } => println!("\n"),
HarmonyComposerEvent::Shutdown => { HarmonyComposerEvent::Shutdown => {
if let Some(progress) = spinner_guard.take() { for (_, progresses) in (*progresses_guard).iter() {
progress.abandon(); progresses.clear().unwrap();
} }
return false; return false;
} }
} }

View File

@ -4,6 +4,7 @@ use tokio::sync::broadcast;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum HarmonyComposerEvent { pub enum HarmonyComposerEvent {
HarmonyComposerStarted,
ProjectInitializationStarted, ProjectInitializationStarted,
ProjectInitialized, ProjectInitialized,
ProjectCompilationStarted { details: String }, ProjectCompilationStarted { details: String },
@ -21,10 +22,11 @@ static HARMONY_COMPOSER_EVENT_BUS: Lazy<broadcast::Sender<HarmonyComposerEvent>>
tx tx
}); });
pub fn instrument(event: HarmonyComposerEvent) { pub fn instrument(event: HarmonyComposerEvent) -> Result<(), &'static str> {
HARMONY_COMPOSER_EVENT_BUS match HARMONY_COMPOSER_EVENT_BUS.send(event) {
.send(event) Ok(_) => Ok(()),
.expect("couldn't send event"); Err(_) => Err("send error: no subscribers"),
}
} }
pub async fn subscribe<F, Fut>(name: &str, mut handler: F) pub async fn subscribe<F, Fut>(name: &str, mut handler: F)

View File

@ -70,15 +70,14 @@ struct AllArgs {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
env_logger::init(); let hc_logger_handle = harmony_composer_logger::init();
let hc_logger_handle = tokio::spawn(harmony_composer_logger::init());
let cli_args = GlobalArgs::parse(); let cli_args = GlobalArgs::parse();
let harmony_path = Path::new(&cli_args.harmony_path) let harmony_path = Path::new(&cli_args.harmony_path)
.try_exists() .try_exists()
.expect("couldn't check if path 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 { let harmony_bin_path: PathBuf = match harmony_path {
true => { true => {
@ -92,7 +91,7 @@ async fn main() {
false => todo!("implement autodetect code"), false => todo!("implement autodetect code"),
}; };
instrumentation::instrument(HarmonyComposerEvent::ProjectInitialized); instrumentation::instrument(HarmonyComposerEvent::ProjectInitialized).unwrap();
match cli_args.command { match cli_args.command {
Some(command) => match command { Some(command) => match command {
@ -127,17 +126,20 @@ async fn main() {
let deploy = if args.staging { let deploy = if args.staging {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "staging".to_string(), target: "staging".to_string(),
}); })
.unwrap();
todo!("implement staging deployment") todo!("implement staging deployment")
} else if args.prod { } else if args.prod {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "prod".to_string(), target: "prod".to_string(),
}); })
.unwrap();
todo!("implement prod deployment") todo!("implement prod deployment")
} else { } else {
instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted { instrumentation::instrument(HarmonyComposerEvent::DeploymentStarted {
target: "dev".to_string(), target: "dev".to_string(),
}); })
.unwrap();
Command::new(harmony_bin_path).arg("-y").arg("-a").spawn() Command::new(harmony_bin_path).arg("-y").arg("-a").spawn()
} }
.expect("failed to run harmony deploy"); .expect("failed to run harmony deploy");
@ -145,7 +147,8 @@ async fn main() {
let deploy_output = deploy.wait_with_output().unwrap(); let deploy_output = deploy.wait_with_output().unwrap();
instrumentation::instrument(HarmonyComposerEvent::DeploymentCompleted { instrumentation::instrument(HarmonyComposerEvent::DeploymentCompleted {
details: String::from_utf8(deploy_output.stdout).unwrap(), details: String::from_utf8(deploy_output.stdout).unwrap(),
}); })
.unwrap();
} }
Commands::All(_args) => todo!( Commands::All(_args) => todo!(
"take all previous match arms and turn them into separate functions, and call them all one after the other" "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"), 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); let _ = tokio::try_join!(hc_logger_handle);
} }
@ -198,18 +201,20 @@ async fn compile_harmony(
CompileMethod::LocalCargo => { CompileMethod::LocalCargo => {
instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted { instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted {
details: "compiling project with cargo".to_string(), details: "compiling project with cargo".to_string(),
}); })
.unwrap();
compile_cargo(platform, harmony_location).await compile_cargo(platform, harmony_location).await
} }
CompileMethod::Docker => { CompileMethod::Docker => {
instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted { instrumentation::instrument(HarmonyComposerEvent::ProjectCompilationStarted {
details: "compiling project with docker".to_string(), details: "compiling project with docker".to_string(),
}); })
.unwrap();
compile_docker(platform, harmony_location).await compile_docker(platform, harmony_location).await
} }
}; };
instrumentation::instrument(HarmonyComposerEvent::ProjectCompiled); instrumentation::instrument(HarmonyComposerEvent::ProjectCompiled).unwrap();
path path
} }