Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions grafana-alertcheck/.changeset/v0.1.5.md
Original file line number Diff line number Diff line change
@@ -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 ✅/❌.
34 changes: 19 additions & 15 deletions grafana-alertcheck/cmd/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -106,27 +108,29 @@ 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.
fmt.Fprintf(w, "\n%s\n\n", violationsLabel(len(res.Violations), enabled))
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
Expand Down
15 changes: 12 additions & 3 deletions grafana-alertcheck/cmd/table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion grafana-alertcheck/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ The label flags cannot be combined with `--alerts` or `--folder`, and they are r

## 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 |
| ---- | ------- |
Expand Down
Loading