Repository: prometheus/common
Pull request #618 (2024-04-12 11:45:36)

Add StatusAt method for Alert struct
This commit adds the StatusAt method for the Alert struct. It calls the ResolvedAt method while Status calls the Resolved method.

This method will be used in Alertmanager to fix issue prometheus/alertmanager#3351.


Patch:
diff --git a/model/alert.go b/model/alert.go
index 178fdba..80d1fe9 100644
--- a/model/alert.go
+++ b/model/alert.go
@@ -75,7 +75,12 @@ func (a *Alert) ResolvedAt(ts time.Time) bool {
 
 // Status returns the status of the alert.
 func (a *Alert) Status() AlertStatus {
-	if a.Resolved() {
+	return a.StatusAt(time.Now())
+}
+
+// StatusAt returns the status of the alert at the given timestamp.
+func (a *Alert) StatusAt(ts time.Time) AlertStatus {
+	if a.ResolvedAt(ts) {
 		return AlertResolved
 	}
 	return AlertFiring
@@ -127,6 +132,17 @@ func (as Alerts) HasFiring() bool {
 	return false
 }
 
+// HasFiringAt returns true iff one of the alerts is not resolved
+// at the time ts.
+func (as Alerts) HasFiringAt(ts time.Time) bool {
+	for _, a := range as {
+		if !a.ResolvedAt(ts) {
+			return true
+		}
+	}
+	return false
+}
+
 // Status returns StatusFiring iff at least one of the alerts is firing.
 func (as Alerts) Status() AlertStatus {
 	if as.HasFiring() {
@@ -134,3 +150,12 @@ func (as Alerts) Status() AlertStatus {
 	}
 	return AlertResolved
 }
+
+// StatusAt returns StatusFiring iff at least one of the alerts is firing
+// at the time ts.
+func (as Alerts) StatusAt(ts time.Time) AlertStatus {
+	if as.HasFiringAt(ts) {
+		return AlertFiring
+	}
+	return AlertResolved
+}


Test patch:
diff --git a/model/alert_test.go b/model/alert_test.go
index c140b15..2a8d7bb 100644
--- a/model/alert_test.go
+++ b/model/alert_test.go
@@ -133,8 +133,8 @@ func TestAlert(t *testing.T) {
 		t.Errorf("expected %s, but got %s", expected, actual)
 	}
 
-	actualStatus := string(alert.Status())
-	expectedStatus := "firing"
+	actualStatus := alert.Status()
+	expectedStatus := AlertStatus("firing")
 
 	if actualStatus != expectedStatus {
 		t.Errorf("expected alertStatus %s, but got %s", expectedStatus, actualStatus)
@@ -150,6 +150,10 @@ func TestAlert(t *testing.T) {
 		EndsAt:   ts2,
 	}
 
+	if !alert.Resolved() {
+		t.Error("expected alert to be resolved, but it was not")
+	}
+
 	actual = fmt.Sprint(alert)
 	expected = "[d181d0f][resolved]"
 
@@ -157,12 +161,44 @@ func TestAlert(t *testing.T) {
 		t.Errorf("expected %s, but got %s", expected, actual)
 	}
 
-	actualStatus = string(alert.Status())
+	actualStatus = alert.Status()
 	expectedStatus = "resolved"
 
 	if actualStatus != expectedStatus {
 		t.Errorf("expected alertStatus %s, but got %s", expectedStatus, actualStatus)
 	}
+
+	// Verifying that ResolvedAt works for different times
+	if alert.ResolvedAt(ts1) {
+		t.Error("unexpected alert was resolved at start time")
+	}
+	if alert.ResolvedAt(ts2.Add(-time.Millisecond)) {
+		t.Error("unexpected alert was resolved before it ended")
+	}
+	if !alert.ResolvedAt(ts2) {
+		t.Error("expected alert to be resolved at end time")
+	}
+	if !alert.ResolvedAt(ts2.Add(time.Millisecond)) {
+		t.Error("expected alert to be resolved after it ended")
+	}
+
+	// Verifying that StatusAt works for different times
+	actualStatus = alert.StatusAt(ts1)
+	if actualStatus != "firing" {
+		t.Errorf("expected alert to be firing at start time, but got %s", actualStatus)
+	}
+	actualStatus = alert.StatusAt(ts1.Add(-time.Millisecond))
+	if actualStatus != "firing" {
+		t.Errorf("expected alert to be firing before it ended, but got %s", actualStatus)
+	}
+	actualStatus = alert.StatusAt(ts2)
+	if actualStatus != "resolved" {
+		t.Errorf("expected alert to be resolved at end time, but got %s", actualStatus)
+	}
+	actualStatus = alert.StatusAt(ts2.Add(time.Millisecond))
+	if actualStatus != "resolved" {
+		t.Errorf("expected alert to be resolved after it ended, but got %s", actualStatus)
+	}
 }
 
 func TestSortAlerts(t *testing.T) {
@@ -228,18 +264,19 @@ func TestSortAlerts(t *testing.T) {
 }
 
 func TestAlertsStatus(t *testing.T) {
+	ts := time.Now()
 	firingAlerts := Alerts{
 		{
 			Labels: LabelSet{
 				"foo": "bar",
 			},
-			StartsAt: time.Now(),
+			StartsAt: ts,
 		},
 		{
 			Labels: LabelSet{
 				"bar": "baz",
 			},
-			StartsAt: time.Now(),
+			StartsAt: ts,
 		},
 	}
 
@@ -250,7 +287,12 @@ func TestAlertsStatus(t *testing.T) {
 		t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus)
 	}
 
-	ts := time.Now()
+	actualStatus = firingAlerts.StatusAt(ts)
+	if actualStatus != expectedStatus {
+		t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus)
+	}
+
+	ts = time.Now()
 	resolvedAlerts := Alerts{
 		{
 			Labels: LabelSet{
@@ -270,7 +312,48 @@ func TestAlertsStatus(t *testing.T) {
 
 	actualStatus = resolvedAlerts.Status()
 	expectedStatus = AlertResolved
+	if actualStatus != expectedStatus {
+		t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus)
+	}
+
+	actualStatus = resolvedAlerts.StatusAt(ts)
+	expectedStatus = AlertResolved
+	if actualStatus != expectedStatus {
+		t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus)
+	}
 
+	ts = time.Now()
+	mixedAlerts := Alerts{
+		{
+			Labels: LabelSet{
+				"foo": "bar",
+			},
+			StartsAt: ts.Add(-1 * time.Minute),
+			EndsAt:   ts.Add(5 * time.Minute),
+		},
+		{
+			Labels: LabelSet{
+				"bar": "baz",
+			},
+			StartsAt: ts.Add(-1 * time.Minute),
+			EndsAt:   ts,
+		},
+	}
+
+	actualStatus = mixedAlerts.Status()
+	expectedStatus = AlertFiring
+	if actualStatus != expectedStatus {
+		t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus)
+	}
+
+	actualStatus = mixedAlerts.StatusAt(ts)
+	expectedStatus = AlertFiring
+	if actualStatus != expectedStatus {
+		t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus)
+	}
+
+	actualStatus = mixedAlerts.StatusAt(ts.Add(5 * time.Minute))
+	expectedStatus = AlertResolved
 	if actualStatus != expectedStatus {
 		t.Errorf("expected status %s, but got %s", expectedStatus, actualStatus)
 	}