Merge pull request #17282 from SRasaikar/srasaikar/Issue_#4510

rules: add unknown state for unevaluated alerting rules
This commit is contained in:
Julius Volz 2025-10-17 11:17:31 +02:00 committed by GitHub
commit da17fe5a9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 33 additions and 6 deletions

View file

@ -50,8 +50,10 @@ const (
type AlertState int
const (
// StateUnknown is the state of an alert that has not yet been evaluated.
StateUnknown AlertState = iota
// StateInactive is the state of an alert that is neither firing nor pending.
StateInactive AlertState = iota
StateInactive
// StatePending is the state of an alert that has been active for less than
// the configured threshold duration.
StatePending
@ -62,6 +64,8 @@ const (
func (s AlertState) String() string {
switch s {
case StateUnknown:
return "unknown"
case StateInactive:
return "inactive"
case StatePending:
@ -530,10 +534,14 @@ func (r *AlertingRule) Eval(ctx context.Context, queryOffset time.Duration, ts t
}
// State returns the maximum state of alert instances for this rule.
// StateFiring > StatePending > StateInactive.
// StateFiring > StatePending > StateInactive > StateUnknown.
func (r *AlertingRule) State() AlertState {
r.activeMtx.Lock()
defer r.activeMtx.Unlock()
// Check if the rule has been evaluated
if r.evaluationTimestamp.Load().IsZero() {
return StateUnknown
}
maxState := StateInactive
for _, a := range r.active {

View file

@ -85,6 +85,8 @@ func TestAlertingRuleState(t *testing.T) {
for i, test := range tests {
rule := NewAlertingRule(test.name, nil, 0, 0, labels.EmptyLabels(), labels.EmptyLabels(), labels.EmptyLabels(), "", true, nil)
rule.active = test.active
// Set evaluation timestamp to simulate that the rule has been evaluated
rule.SetEvaluationTimestamp(time.Now())
got := rule.State()
require.Equal(t, test.want, got, "test case %d unexpected AlertState, want:%d got:%d", i, test.want, got)
}

View file

@ -1,4 +1,4 @@
type RuleState = "pending" | "firing" | "inactive";
type RuleState = "pending" | "firing" | "inactive" | "unknown";
export interface Alert {
labels: Record<string, string>;

View file

@ -45,6 +45,7 @@ type AlertsPageData = {
inactive: number;
pending: number;
firing: number;
unknown: number;
};
groups: {
name: string;
@ -55,6 +56,7 @@ type AlertsPageData = {
inactive: number;
pending: number;
firing: number;
unknown: number;
};
rules: {
rule: AlertingRule;
@ -82,6 +84,7 @@ const buildAlertsPageData = (
inactive: 0,
pending: 0,
firing: 0,
unknown: 0,
},
groups: [],
};
@ -92,6 +95,7 @@ const buildAlertsPageData = (
inactive: 0,
pending: 0,
firing: 0,
unknown: 0,
};
for (const r of group.rules) {
@ -109,6 +113,10 @@ const buildAlertsPageData = (
pageData.globalCounts.pending++;
groupCounts.pending++;
break;
case "unknown":
pageData.globalCounts.unknown++;
groupCounts.unknown++;
break;
default:
throw new Error(`Unknown rule state: ${r.state}`);
}
@ -239,6 +247,11 @@ export default function AlertsPage() {
pending ({g.counts.pending})
</Badge>
)}
{g.counts.unknown > 0 && (
<Badge className={badgeClasses.healthUnknown}>
unknown ({g.counts.unknown})
</Badge>
)}
{g.counts.inactive > 0 && (
<Badge className={badgeClasses.healthOk}>
inactive ({g.counts.inactive})
@ -285,7 +298,9 @@ export default function AlertsPage() {
? panelClasses.panelHealthErr
: r.counts.pending > 0
? panelClasses.panelHealthWarn
: panelClasses.panelHealthOk
: r.rule.state === "unknown"
? panelClasses.panelHealthUnknown
: panelClasses.panelHealthOk
}
>
<Accordion.Control
@ -401,13 +416,15 @@ export default function AlertsPage() {
<Stack mt="xs">
<Group>
<StateMultiSelect
options={["inactive", "pending", "firing"]}
options={["inactive", "pending", "firing", "unknown"]}
optionClass={(o) =>
o === "inactive"
? badgeClasses.healthOk
: o === "pending"
? badgeClasses.healthWarn
: badgeClasses.healthErr
: o === "firing"
? badgeClasses.healthErr
: badgeClasses.healthUnknown
}
optionCount={(o) =>
alertsPageData.globalCounts[