Introduce a way to instrument what happens within Harmony and around Harmony (e.g. in the CLI or in Composer). The goal is to provide visual feedback to the end users and inform them of the progress of their tasks (e.g. deployment) as clearly as possible. It is important to also let them know of the outcome of their tasks (what was created, where to access stuff, etc.). <img src="https://media.discordapp.net/attachments/1295353830300713062/1400289618636574741/demo.gif?ex=688c18d5&is=688ac755&hm=2c70884aacb08f7bd15cbb65a7562a174846906718aa15294bbb238e64febbce&=" /> ## Changes ### Instrumentation architecture Extensibility and ease of use is key here, while preserving type safety as much as possible. The proposed API is quite simple: ```rs // Emit an event instrumentation::instrument( HarmonyEvent::TopologyPrepared { topology: "k8s-anywhere", outcome: Outcome::success("yay") } ); // Consume events instrumentation::subscribe("Harmony CLI Logger", async |event| { match event { HarmonyEvent::TopologyPrepared { name, outcome } => todo!(), } }); ``` #### Current limitations * this API is not very extensible, but it could be easily changed to allow end users to define custom events in addition to Harmony core events * we use a tokio broadcast channel behind the scene so only in process communication can happen, but it could be easily changed to a more flexible communication mechanism as implementation details are hidden ### `harmony_composer` VS `harmony_cli` As Harmony Composer launches commands from Harmony (CLI), they both live in different processes. And because of this, we cannot easily make all the logging happens in one place (Harmony Composer) and get rid of Harmony CLI. At least not without introducing additional complexity such as communication through a server, unix socket, etc. So for the time being, it was decided to preserve both `harmony_composer` and `harmony_cli` and let them independently log their stuff and handle their own responsibilities: * `harmony_composer`: takes care only of setting up & packaging a project, delegates everything else to `harmony_cli` * `harmony_cli`: takes care of configuring & running Harmony ### Logging & prompts * [indicatif](https://github.com/console-rs/indicatif) is used to create progress bars and track progress within Harmony, Harmony CLI, and Harmony Composer * [inquire](https://github.com/mikaelmello/inquire) is preserved, but was removed from `harmony` (core) as UI concerns shouldn't go that deep * note: for now the only prompt we had was simply deleted, we'll have to find a better way to prompt stuff in the future ## Todos * [ ] Update/Create ADRs * [ ] Continue instrumentation for missing branches * [ ] Allow instrumentation to emit and subscribe to custom events Co-authored-by: Ian Letourneau <letourneau.ian@gmail.com> Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/91 Reviewed-by: johnride <jg@nationtech.io>
113 lines
4.5 KiB
Rust
113 lines
4.5 KiB
Rust
use indicatif::{MultiProgress, ProgressBar};
|
|
use indicatif_log_bridge::LogWrapper;
|
|
use log::error;
|
|
use std::{
|
|
collections::HashMap,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
use crate::instrumentation::{self, HarmonyComposerEvent};
|
|
|
|
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 progresses: Arc<Mutex<HashMap<String, MultiProgress>>> =
|
|
Arc::new(Mutex::new(HashMap::new()));
|
|
let compilation_progress = Arc::new(Mutex::new(None::<ProgressBar>));
|
|
|
|
move |event| {
|
|
let progresses_clone = Arc::clone(&progresses);
|
|
let compilation_progress_clone = Arc::clone(&compilation_progress);
|
|
|
|
async move {
|
|
let mut progresses_guard = progresses_clone.lock().unwrap();
|
|
let mut compilation_progress_guard = compilation_progress_clone.lock().unwrap();
|
|
|
|
match event {
|
|
HarmonyComposerEvent::HarmonyComposerStarted => {}
|
|
HarmonyComposerEvent::ProjectInitializationStarted => {
|
|
let multi_progress = harmony_cli::progress::new_section(format!(
|
|
"{} Initializing Harmony project...",
|
|
harmony_cli::theme::EMOJI_HARMONY,
|
|
));
|
|
(*progresses_guard).insert(PROGRESS_SETUP.to_string(), multi_progress);
|
|
}
|
|
HarmonyComposerEvent::ProjectInitialized => println!("\n"),
|
|
HarmonyComposerEvent::ProjectCompilationStarted { details } => {
|
|
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 => {
|
|
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 } => {
|
|
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 } => {
|
|
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 => {
|
|
for (_, progresses) in (*progresses_guard).iter() {
|
|
progresses.clear().unwrap();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
}
|