From 44e7ccd731ebd932b5ffb1683343223ba87124cf Mon Sep 17 00:00:00 2001 From: jerbly Date: Wed, 27 Mar 2024 05:48:52 -0400 Subject: [PATCH] added github issues --- CHANGELOG.md | 8 + Cargo.lock | 567 ++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 3 +- README.md | 25 ++- src/main.rs | 269 ++++++++++++++++++----- src/octo.rs | 156 ++++++++++++++ src/semconv.rs | 29 ++- 7 files changed, 992 insertions(+), 65 deletions(-) create mode 100644 src/octo.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a10b66..cc239e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.5.0 + +- GitHub Issue Generation + - The `-g` or `--github-issue` option can be used to create GitHub Issues for attribute and enum health. Provide the repo owner and name e.g. `myorg/myrepo`. You must have a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) that allows issue creation - put this in an environment variable `GITHUB_TOKEN` or a `.env` file. + + - Honey-health will create a markdown table, split over multiple comments if necessary. Here are examples for [Attributes](https://github.com/jerbly/honey-health/issues/1) and [Enums](https://github.com/jerbly/honey-health/issues/2). + + # 0.4.3 - Fixed: "Similar" suggestions were including deprecated attributes. diff --git a/Cargo.lock b/Cargo.lock index 129dc1f..7f8f09b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,23 @@ version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "async-trait" +version = "0.1.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -113,6 +130,12 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bitflags" version = "1.3.2" @@ -252,12 +275,27 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "dotenv" version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -414,6 +452,19 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "gimli" version = "0.28.0" @@ -437,7 +488,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.10", "indexmap 1.9.3", "slab", "tokio", @@ -471,7 +522,7 @@ checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "honey-health" -version = "0.4.3" +version = "0.5.0" dependencies = [ "anyhow", "clap", @@ -480,6 +531,7 @@ dependencies = [ "glob", "honeycomb-client", "indicatif", + "octocrab", "serde", "serde_yaml", "strsim", @@ -513,6 +565,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.5" @@ -520,7 +583,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", - "http", + "http 0.2.10", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -547,8 +633,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.10", + "http-body 0.4.5", "httparse", "httpdate", "itoa", @@ -560,6 +646,57 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.2.0", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.2.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -567,12 +704,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.27", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.2.0", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -654,6 +811,16 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "iri-string" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21859b667d66a4c1dacd9df0863b3efb65785474255face87f5bca39dd8407c0" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "itoa" version = "1.0.9" @@ -669,6 +836,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.5", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -753,6 +935,32 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -787,6 +995,45 @@ dependencies = [ "memchr", ] +[[package]] +name = "octocrab" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71940dbb2db7c9884d27c5f14894d14468c92c889f848e2feb4419b4dda1c13d" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.0", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", +] + [[package]] name = "once_cell" version = "1.18.0" @@ -870,12 +1117,42 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64 0.21.5", + "serde", +] + [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -900,6 +1177,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.76" @@ -933,15 +1216,15 @@ version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.10", + "http-body 0.4.5", + "hyper 0.14.27", "hyper-tls", "ipnet", "js-sys", @@ -965,6 +1248,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -984,6 +1282,60 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +dependencies = [ + "base64 0.21.5", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868e20fada228fefaf6b652e00cc73623d54f8171e7352c18bb281571f2d92da" + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.15" @@ -1005,6 +1357,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -1059,6 +1420,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1093,6 +1464,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -1108,6 +1491,27 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "snafu" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75976f4748ab44f6e5332102be424e7c2dc18daeaf7e725f2040c3ebb133512e" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b19911debfb8c2fb1107bc6cb2d61868aaf53a988449213959bb1b5b1ed95f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.4.10" @@ -1128,12 +1532,24 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "2.0.48" @@ -1179,6 +1595,57 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1234,6 +1701,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -1248,6 +1726,49 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.4.1", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -1260,10 +1781,23 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -1312,6 +1846,12 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" @@ -1321,6 +1861,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1598,3 +2139,9 @@ dependencies = [ "cfg-if", "windows-sys 0.48.0", ] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index 0572a72..194980d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "honey-health" -version = "0.4.3" +version = "0.5.0" edition = "2021" authors = ["Jeremy Blythe "] repository = "https://github.com/jerbly/honey-health" @@ -16,6 +16,7 @@ glob = "0.3.1" honeycomb-client = { git = "https://github.com/jerbly/honeycomb-client", tag = "0.2.1" } #honeycomb-client = { path = "../honeycomb-client" } indicatif = "0.17.8" +octocrab = "0.36.0" 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 aa5f1c2..b15c472 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,23 @@ The output depends on the number of datasets provided and found for analysis. If TaskId Bad WrongCase; NoNamespace ``` +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). + +## Enums + 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). +```text + Column Undefined-variants + browser.type + message.type + os.type Linux, Windows 10, Mac OS + rpc.system jsonrpc + rpc.type error + telemetry.sdk.language +``` + +## Multiple datasets If there is more that one dataset, the output is a csv file like so: @@ -53,7 +67,7 @@ $ git clone https://github.com/jerbly/honey-health.git $ cd honey-health $ cargo build --release $ ./target/release/honey-health --version -0.4.1 +0.5.0 ``` ## Usage @@ -70,6 +84,7 @@ Options: -l, --last-written-days Max last written days [default: 30] -e, --enums Enum check -s, --show-matches Show matches + -g, --github-issue GitHub issue -h, --help Print help (see more with '--help') -V, --version Print version ``` @@ -77,3 +92,9 @@ Options: You must provide `HONEYCOMB_API_KEY` as an environment variable or in a `.env` file. This api key must have access to read datasets and columns. You must provide at least one path to the model root directory of OpenTelemetry Semantic Convention compatible yaml files. Provide multiple root directories separated by spaces after `--model`. It is recommended to clone the [OpenTelemetry Semantic Conventions](https://github.com/open-telemetry/semantic-conventions) project and add this alongside your own Semantic Conventions. For example: `honey-health --model /code/semantic-conventions/model` + +### GitHub Issue Generation + +The `-g` or `--github-issue` option can be used to create GitHub Issues for attribute and enum health. Provide the repo owner and name e.g. `myorg/myrepo`. You must have a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) that allows issue creation - put this in an environment variable `GITHUB_TOKEN` or a `.env` file. + +Honey-health will create a markdown table, split over multiple comments if necessary. Here are examples for [Attributes](https://github.com/jerbly/honey-health/issues/1) and [Enums](https://github.com/jerbly/honey-health/issues/2). diff --git a/src/main.rs b/src/main.rs index 0395f9a..51d2558 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod octo; mod semconv; use std::{ @@ -69,6 +70,15 @@ impl DatasetHealth { bad: 0, } } + + fn score(&self) -> f64 { + let total = self.matching + self.missing + self.bad; + if total == 0 { + 0.0 + } else { + (self.matching as f64 / total as f64) * 100.0 + } + } } #[derive(Debug)] @@ -163,7 +173,7 @@ impl ColumnUsageMap { c.column.key_name, c.column.r#type, c.suggestion.get_name(), - c.suggestion.get_comments_string(), + c.suggestion.get_comments_string(false), c.datasets_as_string() )?; } @@ -191,12 +201,6 @@ impl ColumnUsageMap { ); for (dataset_num, dataset_slug) in self.datasets.iter().enumerate() { let dataset_health = &self.dataset_health[dataset_num]; - let total = dataset_health.matching + dataset_health.missing + dataset_health.bad; - let score = if total == 0 { - 0.0 - } else { - (dataset_health.matching as f64 / total as f64) * 100.0 - }; println!( "{:>width$} {:4} {:4} {:4} {:>5.1}%", @@ -204,7 +208,7 @@ impl ColumnUsageMap { dataset_health.matching, dataset_health.missing, dataset_health.bad, - score, + dataset_health.score(), width = longest ); } @@ -215,14 +219,8 @@ impl ColumnUsageMap { if self.datasets.len() != 1 { return; } + let longest = self.longest_column_name(); 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$} {}", @@ -257,21 +255,198 @@ impl ColumnUsageMap { } } - async fn print_enum_report(&self) -> anyhow::Result<()> { - // If there's only one dataset, print the enum comparisons + fn markdown_dataset_report(&self) -> Option<(String, Vec)> { + // If there's only one dataset, print the columns that are not matching if self.datasets.len() != 1 { - return Ok(()); + return None; } + // Build the health header + let dataset_slug = &self.datasets[0]; + let dataset_health = &self.dataset_health[0]; + let markdown_header = format!( + "## Dataset: {}\n\n - Matching: {}\n - Missing: {}\n - Bad: {}\n - Score: {:.1}%\n\n", + dataset_slug, + dataset_health.matching, + dataset_health.missing, + dataset_health.bad, + dataset_health.score(), + ); - let mut columns = self.map.values().collect::>(); - let longest = "Column".len().max( - columns - .iter() + // make a vec of tuples of column name and suggestion when not matching + let mut columns = self + .map + .values() + .filter_map(|c| { + if c.suggestion != Suggestion::Matching { + Some(( + c.column.key_name.clone(), + c.suggestion.get_name(), + c.suggestion.get_comments_string(true), + )) + } else { + None + } + }) + .collect::>(); + + if columns.is_empty() { + return None; + } + let mut markdown = vec![]; + let longest_key = "Column" + .len() + .max(columns.iter().map(|c| c.0.len()).max().unwrap_or(0)) + + 2; + + let longest_suggestion = "Suggestion" + .len() + .max(columns.iter().map(|c| c.2.len()).max().unwrap_or(0)); + + columns.sort_by(|a, b| a.0.cmp(&b.0)); + markdown.push(format!( + "| {:k_width$} | {:7} | {:s_width$} |", + "Column", + "Type", + "Suggestion", + k_width = longest_key, + s_width = longest_suggestion + )); + markdown.push(format!( + "| {:k_width$}: | :-----: | :{:s_width$} |", + "-".repeat(longest_key - 1), + "-".repeat(longest_suggestion - 1), + k_width = longest_key - 1, + s_width = longest_suggestion - 1 + )); + for c in columns { + markdown.push(format!( + "| `{:k_width$} | {:7} | {:s_width$} |", + c.0 + "`", + c.1, + c.2, + k_width = longest_key, + s_width = longest_suggestion + )); + } + Some((markdown_header, markdown)) + } + + fn longest_column_name(&self) -> usize { + "Column".len().max( + self.map + .values() .map(|c| c.column.key_name.len()) .max() .unwrap_or(0), + ) + } + + fn print_enum_report( + &self, + enum_report_rows: &Vec<(String, bool, Vec)>, + ) -> anyhow::Result<()> { + // If there's only one dataset, print the enum comparisons + if self.datasets.len() != 1 { + return Ok(()); + } + let longest = self.longest_column_name(); + + println!( + "\n{:>width$} {}", + "Column".bold(), + "Undefined-variants".bold(), + width = longest ); + for (c, allow_custom_values, found_variants) in enum_report_rows { + if found_variants.is_empty() { + println!("{:>width$}", c.green(), width = longest); + } else if *allow_custom_values { + println!( + "{:>width$} {}", + c.yellow(), + found_variants.join(", "), + width = longest + ); + } else { + println!( + "{:>width$} {}", + c.red(), + found_variants.join(", "), + width = longest + ); + } + } + + Ok(()) + } + + fn markdown_enum_report( + &self, + enum_report_rows: Vec<(String, bool, Vec)>, + ) -> anyhow::Result<(String, Vec)> { + let dataset_slug = &self.datasets[0]; + let markdown_header = format!("## Dataset: {}\n\n", dataset_slug); + + // Make the strings for each row + let mut row_strings = vec![]; + let mut c_len = "Column".len(); + let mut v_len = "Undefined-variants".len(); + for (c, allow_custom_values, found_variants) in enum_report_rows { + if !found_variants.is_empty() { + let c_name = format!("`{}`", c); + c_len = c_len.max(c_name.len()); + let variants = format!("`{}`", found_variants.join("`, `")); + v_len = v_len.max(variants.len()); + let kind = if allow_custom_values { + "Warning".to_owned() + } else { + "Error".to_owned() + }; + row_strings.push((c_name, kind, variants)); + } + } + + let mut markdown = vec![]; + markdown.push(format!( + "| {:>c_width$} | {:7} | {:v_width$} |", + "Column", + "Kind", + "Undefined-variants", + c_width = c_len, + v_width = v_len + )); + + markdown.push(format!( + "| {:c_width$}: | :-----: | :{:v_width$} |", + "-".repeat(c_len - 1), + "-".repeat(v_len - 1), + c_width = c_len - 1, + v_width = v_len - 1 + )); + + for r in row_strings { + markdown.push(format!( + "| {:c_width$} | {:7} | {:v_width$} |", + r.0, + r.1, + r.2, + c_width = c_len, + v_width = v_len + )); + } + + Ok((markdown_header, markdown)) + } + + async fn enum_report(&self) -> anyhow::Result)>> { + let mut v_results = Vec::new(); + + // If there's only one dataset, print the enum comparisons + if self.datasets.len() != 1 { + return Ok(v_results); + } + let mut columns = self.map.values().collect::>(); columns.retain(|c| { if c.suggestion == Suggestion::Matching { if let Some(Some(a)) = self.semconv.attribute_map.get(&c.column.key_name) { @@ -285,7 +460,7 @@ impl ColumnUsageMap { if columns.is_empty() { println!("\nNo columns with enum types"); - return Ok(()); + return Ok(v_results); } let column_ids = columns @@ -302,41 +477,18 @@ impl ColumnUsageMap { .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 - ); - } + v_results.push((c, atype.allow_custom_values, found_variants)); } } } - Ok(()) + Ok(v_results) } } @@ -388,6 +540,13 @@ struct Args { /// Show all matching attributes when analyzing a single dataset. #[arg(short, long, default_value_t = false)] show_matches: bool, + + /// GitHub issue + /// + /// Create a GitHub issue with the dataset report. Provide the + /// repository owner and name e.g. "jerbly/honey-health". + #[arg(short, long, required = false)] + github_issue: Option, } #[tokio::main] @@ -418,8 +577,20 @@ async fn main() -> anyhow::Result<()> { } cm.print_health(); cm.print_dataset_report(args.show_matches); + let mut enum_report_rows = vec![]; if args.enums { - cm.print_enum_report().await?; + enum_report_rows = cm.enum_report().await?; + cm.print_enum_report(&enum_report_rows)?; + } + if let Some(repo) = args.github_issue { + let (repo_owner, repo_name) = repo.split_once('/').context("Invalid repository")?; + if let Some((header, body)) = cm.markdown_dataset_report() { + octo::create_dataset_report_issue(repo_owner, repo_name, header, body).await?; + } + if !enum_report_rows.is_empty() { + let (header, body) = cm.markdown_enum_report(enum_report_rows)?; + octo::create_enum_report_issue(repo_owner, repo_name, header, body).await?; + } } Ok(()) } diff --git a/src/octo.rs b/src/octo.rs new file mode 100644 index 0000000..cb8ba07 --- /dev/null +++ b/src/octo.rs @@ -0,0 +1,156 @@ +use octocrab::{models::issues::Issue, Octocrab}; +use std::env; + +pub async fn create_dataset_report_issue( + repo_owner: &str, + repo_name: &str, + markdown_header: String, + markdown_rows: Vec, +) -> anyhow::Result<()> { + let issue_title = "Observability: Attribute names can be improved"; + let mut markdown_header = markdown_header; + markdown_header.push_str( + "This report was generated by [honey-health](https://github.com/jerbly/honey-health). \ + The table shows columns found in the dataset without matching semantic conventions. \ + This _could_ be an indication of data quality issues. \ + Suggestions are given to help improve these attributes.\n\n \ + [\"Effective trace instrumentation with semantic conventions\"](https://www.honeycomb.io/blog/effective-trace-instrumentation-semantic-conventions) \ + may help you improve your instrumentation.\n\n \ + _Note: If the report is too large, it will been split into multiple comments._\n\n", + ); + + create_table_issue( + repo_owner, + repo_name, + markdown_header, + markdown_rows, + issue_title, + ) + .await +} + +pub async fn create_enum_report_issue( + repo_owner: &str, + repo_name: &str, + markdown_header: String, + markdown_rows: Vec, +) -> anyhow::Result<()> { + let issue_title = "Observability: Enum attributes can be improved"; + let mut markdown_header = markdown_header; + markdown_header.push_str( + "This report was generated by [honey-health](https://github.com/jerbly/honey-health). \ + The table shows enum columns found in the dataset with variants undefined in semantic conventions. \ + This _could_ be an indication of data quality issues.\n\n\ + Enums defined with `allow_custom_values` are reported as warnings. These warnings should be checked - mistakes with \ + casing or typos can lead to incorrect variants.\n\n\ + _Note: If the report is too large, it will been split into multiple comments._\n\n"); + + create_table_issue( + repo_owner, + repo_name, + markdown_header, + markdown_rows, + issue_title, + ) + .await +} + +/// Given a list of markdown rows, create a GitHub issue in the provided repository. +/// The markdown rows will be split over multiple comments because of the maximum +/// comment length limit of 65536 characters. +async fn create_table_issue( + repo_owner: &str, + repo_name: &str, + markdown_header: String, + markdown_rows: Vec, + issue_title: &str, +) -> anyhow::Result<()> { + if markdown_rows.len() < 2 { + return Ok(()); + } + // Capture the top two rows for the repeated table header + let mut table_header = markdown_rows[0].clone(); + table_header.push('\n'); + table_header.push_str(&markdown_rows[1]); + table_header.push('\n'); + + let mut issue_body = markdown_header; + + let mut issue_number = None; + + for row in markdown_rows { + if issue_body.len() + row.len() > 60000 { + match issue_number { + Some(number) => { + issue_body.insert_str(0, &table_header); + add_comment_to_github_issue(repo_owner, repo_name, number, &issue_body).await?; + } + None => { + let issue = + create_github_issue(repo_owner, repo_name, issue_title, &issue_body) + .await?; + issue_number = Some(issue.number); + } + } + issue_body = row; + issue_body.push('\n'); + } else { + issue_body.push_str(&row); + issue_body.push('\n'); + } + } + if !issue_body.is_empty() { + match issue_number { + Some(number) => { + issue_body.insert_str(0, &table_header); + add_comment_to_github_issue(repo_owner, repo_name, number, &issue_body).await?; + } + None => { + create_github_issue(repo_owner, repo_name, issue_title, &issue_body).await?; + } + } + } + + Ok(()) +} + +async fn create_github_issue( + repo_owner: &str, + repo_name: &str, + issue_title: &str, + issue_body: &str, +) -> anyhow::Result { + let token = env::var("GITHUB_TOKEN")?; + let octocrab = Octocrab::builder().personal_token(token).build()?; + + let issue = octocrab + .issues(repo_owner, repo_name) + .create(issue_title) + .body(issue_body) + .labels(vec![String::from("observability")]) + .send() + .await?; + + println!("Created issue: {}", issue.html_url); + + Ok(issue) +} + +async fn add_comment_to_github_issue( + repo_owner: &str, + repo_name: &str, + issue_number: u64, + comment_body: &str, +) -> anyhow::Result<()> { + let token = env::var("GITHUB_TOKEN")?; + let octocrab = Octocrab::builder().personal_token(token).build()?; + + let comment = octocrab + .issues(repo_owner, repo_name) + .create_comment(issue_number, comment_body) + .await?; + + println!("Created comment: {}", comment.html_url); + + Ok(()) +} diff --git a/src/semconv.rs b/src/semconv.rs index 717032d..e1dca23 100644 --- a/src/semconv.rs +++ b/src/semconv.rs @@ -23,12 +23,18 @@ impl Suggestion { Suggestion::Bad(_) => "Bad".to_string(), } } - pub fn get_comments_string(&self) -> String { + pub fn get_comments_string(&self, markdown: bool) -> String { match self { Suggestion::Matching => "".to_string(), Suggestion::Missing(comments) | Suggestion::Bad(comments) => comments .iter() - .map(|x| x.to_string()) + .map(|x| { + if markdown { + x.to_markdown() + } else { + x.to_string() + } + }) .collect::>() .join("; "), } @@ -39,7 +45,12 @@ impl Display for Suggestion { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Suggestion::Matching => write!(f, "Matching"), - _ => write!(f, "{:7} {}", self.get_name(), self.get_comments_string()), + _ => write!( + f, + "{:7} {}", + self.get_name(), + self.get_comments_string(false) + ), } } } @@ -54,6 +65,18 @@ pub enum SuggestionComment { NoNamespace, } +impl SuggestionComment { + pub fn to_markdown(&self) -> String { + match self { + SuggestionComment::WrongCase => "WrongCase".to_string(), + SuggestionComment::NoNamespace => "NoNamespace".to_string(), + SuggestionComment::Similar(v) => format!("Similar to `{}`", v.join("`, `")), + SuggestionComment::Extends(s) => format!("Extends `{}`", s), + SuggestionComment::Deprecated(s) => format!("Deprecated: {}", s), + } + } +} + impl Display for SuggestionComment { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self {