Skip to content

Commit 58142d3

Browse files
committed
chore: address CR
1 parent 870a86d commit 58142d3

4 files changed

Lines changed: 37 additions & 11 deletions

File tree

‎grafana-alertcheck/.changeset/v0.1.10.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
- Datasource-managed identity is a separate rule key; `uid` stays empty for these rules. The log gains additive `key`, `source_kind`, `datasource_uid`, `datasource_name`, `file` and `rule_key` fields, with `schema_version` still `1`. An old v1 log without `rule_key` remains readable; a new log read by an old binary fails closed on the unresolved key.
44
- Datasource recovery semantics: because the Prometheus API returns only active instances, an instance leaving the active set is treated as a real recovery (`cleared`). Documented weaker guarantee: a vanished series is indistinguishable from a resolution.
55
- Pause is not observable for datasource-managed rules (no `isPaused` signal), so check 7 is skipped with an explicit note. `health: err` normalizes to `error` and still triggers check 4.
6-
- Datasource-managed rules record the backend's `keep_firing_for` (`keep_firing_for_ms` in the log). The datasource API has no `recovering` state — it keeps an alert firing through its keep-firing-for and then drops it — so the recovery observation applies to Grafana-managed rules only.
6+
- Datasource-managed rules record the backend's keep-firing-for (`keep_firing_for` on vmalert, `keepFiringFor` on Prometheus/Mimir; `keep_firing_for_ms` in the log). The datasource API has no `recovering` state — it keeps an alert firing through its keep-firing-for and then drops it — so the recovery observation applies to Grafana-managed rules only. A datasource rule with an unrecognized `type` is now a hard error rather than silently dropped.
77
- The token now needs `datasources:read` plus datasource query permission even for Grafana-only runs; discovery failures name the missing permission.
88
- A backend can serve two distinct datasource-managed rules under one identity (same datasource/group/name/file, differing by labels/query). Loading and `list` accept them, but a selection that includes such a rule fails closed: the state query cannot tell the siblings apart, so narrowing to one does not make it observable. `uid:` selectors no longer trigger a bulk datasource read, and a datasource rule's `lastError` is recorded.
99
- A datasource-managed rule's name can itself contain `/` (e.g. `devex-cicd/prod/griddle-github: ContainersNotReady`). The full name is now matched exactly and used as the `rule_name[]` fetch filter, instead of splitting it into `Group/Title` segments and filtering by the last segment.

‎grafana-alertcheck/docs/reference/log-format.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Field notes:
102102
- `grafana_now` is the response's `Date` header — never the runner clock.
103103
- `skew_ms`/`skew_bound_ms` are the per-poll clock-skew estimate and its uncertainty (RTT/2), in milliseconds for compactness only.
104104
- `found: false` is an authoritative `2xx` in which this rule was absent — a transport failure is retried and never becomes a poll.
105-
- `keep_firing_for_ms` is the rule's recovery period as reported by this response; `0`/absent means no instance can be `recovering`. A datasource rule reports it from the backend's `keep_firing_for`, but such a rule never reaches `recovering` (the backend keeps it firing, then drops it).
105+
- `keep_firing_for_ms` is the rule's recovery period as reported by this response; `0`/absent means no instance can be `recovering`. A datasource rule reports it from the backend's keep-firing-for (`keep_firing_for` on vmalert, `keepFiringFor` on Prometheus/Mimir), but such a rule never reaches `recovering` (the backend keeps it firing, then drops it).
106106
- `state`, `health`, `last_error` are raw rule-level strings, reporting-only.
107107
- `histogram` is a verbatim copy of the response `totals`; written, never analysed.
108108
- `reasons` counts non-empty instance reasons (`NoData`, `Error`, `KeepLast`, …); composite states stay visible only here.

‎grafana-alertcheck/internal/gate/parse_datasource.go‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ func parseDatasourceRule(raw json.RawMessage, dsUID, group, file string, interva
8585
if err := req(m, "type", &ruleType); err != nil {
8686
return StateRule{}, fmt.Errorf("rule %q: %w", name, err)
8787
}
88+
// Reject an unrecognized type rather than dropping it: a schema change must
89+
// not silently shrink the rule set the run proceeds over.
90+
if ruleType != "alerting" && ruleType != "recording" {
91+
return StateRule{}, fmt.Errorf("rule %q: unknown rule type %q (want alerting or recording)", name, ruleType)
92+
}
8893

8994
r := StateRule{
9095
Key: ruleKey(dsUID, group, name, file, ""),
@@ -136,13 +141,17 @@ func parseDatasourceRule(raw json.RawMessage, dsUID, group, file string, interva
136141
if err := opt(m, "lastError", &r.LastError); err != nil {
137142
return StateRule{}, fmt.Errorf("rule %q: %w", name, err)
138143
}
139-
// vmalert reports the alerting rule's keep-firing-for in seconds under the
140-
// snake_case key. Unlike Grafana it has no recovering state: the alert stays
141-
// firing for this long and is then dropped, so this is informational.
144+
// The keep-firing-for period, in seconds. vmalert spells it keep_firing_for,
145+
// Prometheus and Mimir keepFiringFor. Unlike Grafana there is no recovering
146+
// state: the alert stays firing for this long and is then dropped, so this
147+
// is informational.
142148
var keepFiringForSeconds float64
143149
if err := opt(m, "keep_firing_for", &keepFiringForSeconds); err != nil {
144150
return StateRule{}, fmt.Errorf("rule %q: %w", name, err)
145151
}
152+
if err := opt(m, "keepFiringFor", &keepFiringForSeconds); err != nil {
153+
return StateRule{}, fmt.Errorf("rule %q: %w", name, err)
154+
}
146155
r.KeepFiringFor = time.Duration(keepFiringForSeconds * float64(time.Second))
147156

148157
if err := opt(m, "state", &r.State); err != nil {

‎grafana-alertcheck/internal/gate/parse_datasource_test.go‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,31 @@ func TestParseDatasourceRules_LastError(t *testing.T) {
7070
require.Equal(t, "query failed: bad", rules[0].LastError)
7171
}
7272

73-
// vmalert's keep-firing-for is snake_case and in seconds; the alert stays
74-
// firing for it, so it is recorded but never a recovering state.
73+
// The keep-firing-for is in seconds and spelled keep_firing_for by vmalert and
74+
// keepFiringFor by Prometheus/Mimir; the alert stays firing for it, so it is
75+
// recorded but never a recovering state.
7576
func TestParseDatasourceRules_KeepFiringFor(t *testing.T) {
77+
for name, body := range map[string][]byte{
78+
"vmalert": []byte(`{"status":"success","data":{"groups":[{"name":"g","rules":[
79+
{"name":"A","type":"alerting","health":"ok","state":"firing","keep_firing_for":300}]}]}}`),
80+
"prometheus": []byte(`{"status":"success","data":{"groups":[{"name":"g","rules":[
81+
{"name":"A","type":"alerting","health":"ok","state":"firing","keepFiringFor":300}]}]}}`),
82+
} {
83+
t.Run(name, func(t *testing.T) {
84+
rules, err := ParseDatasourceRules(body, "d")
85+
require.NoError(t, err)
86+
require.Equal(t, 5*time.Minute, rules[0].KeepFiringFor)
87+
})
88+
}
89+
}
90+
91+
// An unknown rule type must fail closed, not be dropped from the inventory.
92+
func TestParseDatasourceRules_UnknownTypeIsError(t *testing.T) {
7693
body := []byte(`{"status":"success","data":{"groups":[{"name":"g","rules":[
77-
{"name":"A","type":"alerting","health":"ok","state":"firing","keep_firing_for":300}]}]}}`)
78-
rules, err := ParseDatasourceRules(body, "d")
79-
require.NoError(t, err)
80-
require.Equal(t, 5*time.Minute, rules[0].KeepFiringFor)
94+
{"name":"A","type":"future","health":"ok","state":"firing"}]}]}}`)
95+
_, err := ParseDatasourceRules(body, "d")
96+
require.Error(t, err)
97+
require.Contains(t, err.Error(), "unknown rule type")
8198
}
8299

83100
func TestParseDatasourceRules_ZeroLastEvaluationAllowed(t *testing.T) {

0 commit comments

Comments
 (0)