From a88f03245577e2f0f7933205629048527e1fe07c Mon Sep 17 00:00:00 2001 From: Bartek Tofel Date: Tue, 29 Sep 2026 14:45:40 +0200 Subject: [PATCH] feat: add emojis to CLI output --- grafana-alertcheck/.changeset/v0.1.5.md | 1 + grafana-alertcheck/cmd/table.go | 34 +++++++++++++----------- grafana-alertcheck/cmd/table_test.go | 15 ++++++++--- grafana-alertcheck/docs/reference/cli.md | 2 +- 4 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 grafana-alertcheck/.changeset/v0.1.5.md diff --git a/grafana-alertcheck/.changeset/v0.1.5.md b/grafana-alertcheck/.changeset/v0.1.5.md new file mode 100644 index 000000000..a37c8d3be --- /dev/null +++ b/grafana-alertcheck/.changeset/v0.1.5.md @@ -0,0 +1 @@ +- The check summary now heads the limits legend with `Legend:`, reports the Grafana version on its own line, and marks the final violations count with ✅/❌. diff --git a/grafana-alertcheck/cmd/table.go b/grafana-alertcheck/cmd/table.go index 085444b88..d756d1575 100644 --- a/grafana-alertcheck/cmd/table.go +++ b/grafana-alertcheck/cmd/table.go @@ -13,9 +13,10 @@ import ( // limitsLegend explains each LIMITS USED column in one plain sentence, so the // table needs no documentation lookup. -const limitsLegend = ` max gap without check — the longest gap between two checks we accept before we say the alert was not watched. - query failing for — how long Grafana may keep failing to run the alert's query before we stop trusting its state. - no evaluation for — how long Grafana may go without evaluating the alert before we stop trusting its state.` +const limitsLegend = `Legend: +max gap without check — the longest gap between two checks we accept before we say the alert was not watched. +query failing for — how long Grafana may keep failing to run the alert's query before we stop trusting its state. +no evaluation for — how long Grafana may go without evaluating the alert before we stop trusting its state.` // renderTable is the human table. It always writes to the writer it is given, // which the caller (runCheck) always points at stderr — stdout is reserved for @@ -32,8 +33,8 @@ const limitsLegend = ` max gap without check — the longest gap between two ch // column is named in plain words and explained by the legend below it, so // the table needs no documentation lookup. // -// The global footer then reports the extra observation time, the drain limit -// and the largest measured clock difference, also in plain words. +// The global footer reports the extra observation time, the drain limit, the +// clock difference, the Grafana version and the emoji-marked violations count. func renderTable(w io.Writer, res gate.Result) error { alertOf := make(map[string]string, len(res.Verdicts)) for _, v := range res.Verdicts { @@ -96,6 +97,7 @@ func renderTable(w io.Writer, res gate.Result) error { if err := ttw.Flush(); err != nil { return fmt.Errorf("render table: %w", err) } + fmt.Fprintln(w) fmt.Fprintln(w, limitsLegend) fmt.Fprintln(w) @@ -106,9 +108,10 @@ func renderTable(w io.Writer, res gate.Result) error { fmt.Fprintln(w, "extra watching after your window: none") } fmt.Fprintf(w, "max wait for all alerts to finish evaluating: %s\n", res.Global.DrainTimeout) - fmt.Fprintf(w, "clock difference from Grafana: %s, accurate to ±%s (checks fail above %s); Grafana %s\n", + fmt.Fprintf(w, "clock difference from Grafana: %s, accurate to ±%s (checks fail above %s)\n", res.ClockSkew.Round(time.Millisecond), res.ClockSkewBound.Round(time.Millisecond), - gate.SkewHardLimit, res.GrafanaVersion) + gate.SkewHardLimit) + fmt.Fprintf(w, "\nGrafana version: %s\n", res.GrafanaVersion) // The verdict — the single number a terminal operator reads last — sits on // its own line at the very bottom, separated from the diagnostics above and // from the shell prompt below. @@ -116,17 +119,18 @@ func renderTable(w io.Writer, res gate.Result) error { return nil } -// violationsLabel colours the "violations: N" prefix of the footer: green when -// there are none, red otherwise. The rest of the line is written uncoloured. +// violationsLabel is the footer verdict line: ✅/❌ then "violations: N", +// coloured green/red when the destination is a terminal. func violationsLabel(n int, enabled bool) string { - s := fmt.Sprintf("violations: %d", n) - if !enabled { - return s - } + mark, color := "❌", ansiRed if n == 0 { - return ansiGreen + s + ansiReset + mark, color = "✅", ansiGreen + } + s := fmt.Sprintf("violations: %d", n) + if enabled { + s = color + s + ansiReset } - return ansiRed + s + ansiReset + return mark + " " + s } // provedLabel is the table's WINDOW COVERED column: "yes" for a fully diff --git a/grafana-alertcheck/cmd/table_test.go b/grafana-alertcheck/cmd/table_test.go index acf987423..db5c68929 100644 --- a/grafana-alertcheck/cmd/table_test.go +++ b/grafana-alertcheck/cmd/table_test.go @@ -78,15 +78,24 @@ func TestRenderTable(t *testing.T) { require.Contains(t, out, "MAX GAP WITHOUT CHECK") require.Contains(t, out, "QUERY FAILING FOR") require.Contains(t, out, "NO EVALUATION FOR") + require.Contains(t, out, "Legend:") require.Contains(t, out, "the longest gap between two checks") require.Contains(t, out, "without evaluating the alert") - // The global footer in plain words. + // The global footer in plain words, with the Grafana version on its own + // line and the violations count marked with an emoji. require.Contains(t, out, "extra watching after your window: +5m0s") require.Contains(t, out, "slowest: Ape Alert (for=5m)") require.Contains(t, out, "max wait for all alerts to finish evaluating: 2m0s") - require.Contains(t, out, "clock difference from Grafana: 1.5s, accurate to ±250ms (checks fail above 1m0s); Grafana 13.1.0") - require.Contains(t, out, "violations: 2") + require.Contains(t, out, "clock difference from Grafana: 1.5s, accurate to ±250ms (checks fail above 1m0s)") + require.Contains(t, out, "Grafana version: 13.1.0") + require.Contains(t, out, "❌ violations: 2") +} + +// The footer verdict line picks its emoji by whether there are violations. +func TestViolationsLabel(t *testing.T) { + require.Equal(t, "✅ violations: 0", violationsLabel(0, false)) + require.Equal(t, "❌ violations: 2", violationsLabel(2, false)) } // The "-" case: a rule decide never asked proveCoverage about (paused before diff --git a/grafana-alertcheck/docs/reference/cli.md b/grafana-alertcheck/docs/reference/cli.md index 312ea4860..972800aba 100644 --- a/grafana-alertcheck/docs/reference/cli.md +++ b/grafana-alertcheck/docs/reference/cli.md @@ -102,7 +102,7 @@ Datasource-managed and recording rules are refused with a specific error. A name ## Output and exit codes -The human table goes to **stderr**: `RESULTS` (one row per rule, with the verdict, time broken, check cadence and whether the window was observed), `VIOLATIONS` (one per distinct rule/verdict/state/health/note signature, with an `INSTANCES` count of the instances it stands for — instance identity is only in the JSON), and `LIMITS USED` (each rule's observation limits in plain words, explained by a legend under the table, plus the extra observation time, the evaluation wait and the largest measured clock difference). The JSON outcome values are `healthy`, `new_failure`, `still_failing`, `recovered`, `unstable`, `paused`, `not_verified` and the synthetic `not_counted`. `--output json` writes the result to stdout. +The human table goes to **stderr**: `RESULTS` (one row per rule, with the verdict, time broken, check cadence and whether the window was observed), `VIOLATIONS` (one per distinct rule/verdict/state/health/note signature, with an `INSTANCES` count of the instances it stands for — instance identity is only in the JSON), and `LIMITS USED` (each rule's observation limits in plain words, explained by a legend under the table, plus the extra observation time, the evaluation wait, the largest measured clock difference and the detected Grafana version; the closing violations count is marked ✅/❌). The JSON outcome values are `healthy`, `new_failure`, `still_failing`, `recovered`, `unstable`, `paused`, `not_verified` and the synthetic `not_counted`. `--output json` writes the result to stdout. | Code | Meaning | | ---- | ------- |