diff --git a/CHANGELOG.md b/CHANGELOG.md index b363cac..41faef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.4.0 + +- For single datasets you can now use the `-e` or `--enums` switch. This compares enum variants defined in semantic conventions with discovered variants used in tracing. Additional variants will be reported. If the attribute's enum definition has `allow_custom_values` set `true`, this is an _open enum_ and additional variants are "allowed". Honey-health still reports additional variants but as a warning (highlighted in yellow). +- Added progress bars. Some Honeycomb operations can take a while so this provides better feedback. + # 0.3.4 - Uses `cargo-dist` for build and release. diff --git a/Cargo.lock b/Cargo.lock index 9f7568e..62ba8b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,7 +67,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -77,7 +77,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -164,7 +164,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -220,7 +220,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", ] [[package]] @@ -245,6 +258,12 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -267,7 +286,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -460,6 +479,7 @@ dependencies = [ "dotenv", "glob", "honeycomb-client", + "indicatif", "serde", "serde_yaml", "strsim", @@ -468,11 +488,13 @@ dependencies = [ [[package]] name = "honeycomb-client" -version = "0.2.0" +version = "0.2.1" +source = "git+https://github.com/jerbly/honeycomb-client?tag=0.2.1#24a22bdfba45f8450271bcf2d201f7247b9de315" dependencies = [ "anyhow", "chrono", "futures", + "indicatif", "openssl", "reqwest", "serde", @@ -604,6 +626,28 @@ dependencies = [ "hashbrown 0.14.2", ] +[[package]] +name = "indicatif" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +dependencies = [ + "console", + "instant", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -688,7 +732,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -728,6 +772,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.32.1" @@ -817,7 +867,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -844,6 +894,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "proc-macro2" version = "1.0.76" @@ -925,7 +981,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -940,7 +996,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1069,7 +1125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1120,7 +1176,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1154,7 +1210,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1244,6 +1300,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + [[package]] name = "unsafe-libyaml" version = "0.2.10" @@ -1392,7 +1454,7 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1401,7 +1463,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", ] [[package]] @@ -1410,13 +1481,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -1425,42 +1511,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "winreg" version = "0.50.0" @@ -1468,5 +1596,5 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] diff --git a/Cargo.toml b/Cargo.toml index f863b2e..5104f12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,9 @@ clap = { version = "4.5.1", features = ["derive"] } colored = "2.1.0" dotenv = "0.15.0" glob = "0.3.1" -#honeycomb-client = { git = "https://github.com/jerbly/honeycomb-client", tag = "v0.2.0" } -honeycomb-client = { path = "../honeycomb-client" } +honeycomb-client = { git = "https://github.com/jerbly/honeycomb-client", tag = "0.2.1" } +#honeycomb-client = { path = "../honeycomb-client" } +indicatif = "0.17.8" serde = { version = "1.0.197", features = ["derive"] } serde_yaml = "0.9.32" strsim = "0.11.0" diff --git a/README.md b/README.md index 18f88c7..4fa6350 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Generates reports on the health of your [Honeycomb](https://honeycomb.io) datase Provide it with OpenTelemetry Semantic Convention compatible files to find mismatches and suggestions. Compare all, or a limited set of your datasets, to find commonly used attributes that may benefit from being codified into Semantic Conventions. -The output depends on the number of datasets provided and found for analysis. If a single dataset is analysed, then a csv comparison file is NOT produced (there's no other dataset to compare against!) Instead you will see output in the console like so: +The output depends on the number of datasets provided and found for analysis. If a single dataset is analyzed, then a csv comparison file is NOT produced (there's no other dataset to compare against!) Instead you will see output in the console like so: ```text Dataset Match Miss Bad Score @@ -16,6 +16,8 @@ The output depends on the number of datasets provided and found for analysis. If TaskId Bad WrongCase; NoNamespace ``` +For single datasets you can also use the `-e` or `--enums` switch. This compares enum variants defined in semantic conventions with discovered variants used in tracing. Additional variants will be reported. If the attribute's enum definition has `allow_custom_values` set `true`, this is an _open enum_ and additional variants are "allowed". Honey-health still reports additional variants but as a warning (highlighted in yellow). + You will always see the top section showing the number of Matching, Missing and Bad attributes. The Score is the proportion of Matching attributes (those which have defined Semantic Conventions). If there is more that one dataset, the output is a csv file like so: @@ -29,9 +31,9 @@ If there is more that one dataset, the output is a csv file like so: This example report is pointing out the following: -- `aws.s3.bucket.name` has not been found in the provided semantic conventions. However, there is a namespace `aws.s3` that this attribute would extend. Also, there is an attribute in the model with a similar name: `aws.s3.bucket`. The application delivering to `dataset3` should have its instrumention adjusted to the standard. +- `aws.s3.bucket.name` has not been found in the provided semantic conventions. However, there is a namespace `aws.s3` that this attribute would extend. Also, there is an attribute in the model with a similar name: `aws.s3.bucket`. The application delivering to `dataset3` should have its instrumentation adjusted to the standard. - `aws.s3.key` is in use by `dataset3` and matches a semantic convention in the provided models. -- `task.id` is missing from the provided model but is used by 2 datasets: `dataset1` and `dataset3`. Perhaps this is a good candidate to standardise into your own semantic conventions? +- `task.id` is missing from the provided model but is used by 2 datasets: `dataset1` and `dataset3`. Perhaps this is a good candidate to standardize into your own semantic conventions? - `TaskId` is in CamelCase which does not follow the recommended standard for attribute naming. Also, this is a top-level name with no namespace - this will pollute the namespace tree. > **Note** @@ -48,7 +50,7 @@ $ git clone https://github.com/jerbly/honey-health.git $ cd honey-health $ cargo build --release $ ./target/release/honey-health --version -0.3.0 +0.4.0 ``` ## Usage @@ -63,6 +65,7 @@ Options: -d, --dataset [...] Datasets -o, --output Output file path [default: hh_report.csv] -l, --last-written-days Max last written days [default: 30] + -e, --enums Enum check -h, --help Print help (see more with '--help') -V, --version Print version ``` diff --git a/src/main.rs b/src/main.rs index f8a2f31..9c49b6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ use anyhow::{Context, Ok}; use clap::Parser; use colored::Colorize; use honeycomb_client::honeycomb::Column; +use indicatif::ProgressBar; use semconv::{SemanticConventions, Suggestion}; // For each dataset get all the columns and put them in a map of column_name -> ColumnUsage @@ -92,22 +93,26 @@ impl ColumnUsageMap { dataset_health: vec![], semconv: sc, }; - let hc = match honeycomb_client::get_honeycomb(&["columns", "createDatasets"]).await? { - Some(hclient) => hclient, - None => { - anyhow::bail!("API key does not have required access"); - } - }; + let hc = honeycomb_client::get_honeycomb(&["columns", "createDatasets"]) + .await? + .context("API key does not have required access")?; let dataset_slugs = hc .get_dataset_slugs(max_last_written_days as i64, include_datasets) .await?; cm.datasets = dataset_slugs; - eprint!("Reading {} datasets ", cm.datasets.len()); + let bar = ProgressBar::new(cm.datasets.len() as u64) + .with_style( + indicatif::ProgressStyle::default_bar() + .template("[{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}") + .unwrap(), + ) + .with_message("Reading datasets..."); + bar.inc(0); let mut dataset_num = 0; hc.process_datasets_columns(max_last_written_days as i64, &cm.datasets, |_, columns| { - eprint!("."); + bar.inc(1); let mut dataset_health = DatasetHealth::new(); for column in columns { let health: Suggestion; @@ -137,7 +142,7 @@ impl ColumnUsageMap { }) .await?; - eprintln!(); + bar.finish_and_clear(); Ok(cm) } @@ -207,41 +212,42 @@ impl ColumnUsageMap { fn print_dataset_report(&self) { // If there's only one dataset, print the columns that are not matching - if self.datasets.len() == 1 { - let mut columns = self.map.values().collect::>(); - let longest = "Column".len().max( - columns - .iter() - .map(|c| c.column.key_name.len()) - .max() - .unwrap_or(0), - ); - columns.sort_by(|a, b| a.column.key_name.cmp(&b.column.key_name)); - println!( - "\n{:>width$} {}", - "Column".bold(), - "Suggestion".bold(), - width = longest - ); - for c in columns { - match c.suggestion { - Suggestion::Matching => {} - Suggestion::Missing(_) => { - println!( - "{:>width$} {}", - c.column.key_name.yellow(), - c.suggestion, - width = longest - ); - } - _ => { - println!( - "{:>width$} {}", - c.column.key_name.red(), - c.suggestion, - width = longest - ); - } + if self.datasets.len() != 1 { + return; + } + let mut columns = self.map.values().collect::>(); + let longest = "Column".len().max( + columns + .iter() + .map(|c| c.column.key_name.len()) + .max() + .unwrap_or(0), + ); + columns.sort_by(|a, b| a.column.key_name.cmp(&b.column.key_name)); + println!( + "\n{:>width$} {}", + "Column".bold(), + "Suggestion".bold(), + width = longest + ); + for c in columns { + match c.suggestion { + Suggestion::Matching => {} + Suggestion::Missing(_) => { + println!( + "{:>width$} {}", + c.column.key_name.yellow(), + c.suggestion, + width = longest + ); + } + _ => { + println!( + "{:>width$} {}", + c.column.key_name.red(), + c.suggestion, + width = longest + ); } } } @@ -249,81 +255,83 @@ impl ColumnUsageMap { async fn print_enum_report(&self) -> anyhow::Result<()> { // If there's only one dataset, print the enum comparisons - if self.datasets.len() == 1 { - let hc = - match honeycomb_client::get_honeycomb(&["columns", "createDatasets", "queries"]) - .await? - { - Some(hclient) => hclient, - None => { - anyhow::bail!("API key does not have required access"); - } - }; - - let mut columns = self.map.values().collect::>(); - let longest = "Column".len().max( - columns - .iter() - .map(|c| c.column.key_name.len()) - .max() - .unwrap_or(0), - ); + if self.datasets.len() != 1 { + return Ok(()); + } - println!( - "\n{:>width$} {}", - "Column".bold(), - "Undefined-variants".bold(), - width = longest - ); + let mut columns = self.map.values().collect::>(); + let longest = "Column".len().max( + columns + .iter() + .map(|c| c.column.key_name.len()) + .max() + .unwrap_or(0), + ); - columns.retain(|c| { - if c.suggestion == Suggestion::Matching { - if let Some(Some(a)) = self.semconv.attribute_map.get(&c.column.key_name) { - if let Some(semconv::Type::Complex(_)) = &a.r#type { - return true; - } + columns.retain(|c| { + if c.suggestion == Suggestion::Matching { + if let Some(Some(a)) = self.semconv.attribute_map.get(&c.column.key_name) { + if let Some(semconv::Type::Complex(_)) = &a.r#type { + return true; } } - false - }); + } + false + }); - let column_ids = columns - .iter() - .map(|c| c.column.key_name.clone()) - .collect::>(); - - let mut results = hc - .get_all_group_by_variants(&self.datasets[0], &column_ids) - .await?; - results.sort(); - - for (c, mut found_variants) in results { - if let Some(Some(a)) = self.semconv.attribute_map.get(&c) { - if let Some(semconv::Type::Complex(atype)) = &a.r#type { - let defined_variants = atype.get_simple_variants(); - // remove all defined enums from found_enums - found_variants.retain(|e| !defined_variants.contains(e)); - if found_variants.is_empty() { - println!("{:>width$}", c.green(), width = longest); - } else if atype.allow_custom_values { - println!( - "{:>width$} {}", - c.yellow(), - found_variants.join(", "), - width = longest - ); - } else { - println!( - "{:>width$} {}", - c.red(), - found_variants.join(", "), - width = longest - ); - } + if columns.is_empty() { + println!("\nNo columns with enum types"); + return Ok(()); + } + + let column_ids = columns + .iter() + .map(|c| c.column.key_name.clone()) + .collect::>(); + + let hc = honeycomb_client::get_honeycomb(&["columns", "createDatasets", "queries"]) + .await? + .context("API key does not have required access")?; + + let mut results = hc + .get_all_group_by_variants(&self.datasets[0], &column_ids) + .await?; + results.sort(); + + println!( + "\n{:>width$} {}", + "Column".bold(), + "Undefined-variants".bold(), + width = longest + ); + + for (c, mut found_variants) in results { + if let Some(Some(a)) = self.semconv.attribute_map.get(&c) { + if let Some(semconv::Type::Complex(atype)) = &a.r#type { + let defined_variants = atype.get_simple_variants(); + // remove all defined enums from found_enums + found_variants.retain(|e| !defined_variants.contains(e)); + if found_variants.is_empty() { + println!("{:>width$}", c.green(), width = longest); + } else if atype.allow_custom_values { + println!( + "{:>width$} {}", + c.yellow(), + found_variants.join(", "), + width = longest + ); + } else { + println!( + "{:>width$} {}", + c.red(), + found_variants.join(", "), + width = longest + ); } } } } + Ok(()) } }