diff --git a/alerts/alerts.go b/alerts/alerts.go index f11004dbe..a14d460b6 100644 --- a/alerts/alerts.go +++ b/alerts/alerts.go @@ -66,14 +66,19 @@ type ( } AlertsOpts struct { - Offset int - Limit int + Offset int + Limit int + Severity Severity } AlertsResponse struct { - Alerts []Alert `json:"alerts"` - HasMore bool `json:"hasMore"` - Total int `json:"total"` + Alerts []Alert `json:"alerts"` + HasMore bool `json:"hasMore"` + Total int `json:"total"` + TotalInfo int `json:"totalInfo"` + TotalWarning int `json:"totalWarning"` + TotalError int `json:"totalError"` + TotalCritical int `json:"totalCritical"` } ) @@ -93,15 +98,8 @@ func (s Severity) String() string { } } -// MarshalJSON implements the json.Marshaler interface. -func (s Severity) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf(`%q`, s.String())), nil -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (s *Severity) UnmarshalJSON(b []byte) error { - status := strings.Trim(string(b), `"`) - switch status { +func (s *Severity) LoadString(str string) error { + switch str { case severityInfoStr: *s = SeverityInfo case severityWarningStr: @@ -111,11 +109,21 @@ func (s *Severity) UnmarshalJSON(b []byte) error { case severityCriticalStr: *s = SeverityCritical default: - return fmt.Errorf("unrecognized severity: %v", status) + return fmt.Errorf("unrecognized severity: %v", str) } return nil } +// MarshalJSON implements the json.Marshaler interface. +func (s Severity) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`%q`, s.String())), nil +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (s *Severity) UnmarshalJSON(b []byte) error { + return s.LoadString(strings.Trim(string(b), `"`)) +} + // RegisterAlert implements the Alerter interface. func (m *Manager) RegisterAlert(ctx context.Context, alert Alert) error { if alert.ID == (types.Hash256{}) { @@ -176,9 +184,7 @@ func (m *Manager) Alerts(_ context.Context, opts AlertsOpts) (AlertsResponse, er defer m.mu.Unlock() offset, limit := opts.Offset, opts.Limit - resp := AlertsResponse{ - Total: len(m.alerts), - } + resp := AlertsResponse{} if offset >= len(m.alerts) { return resp, nil @@ -188,6 +194,19 @@ func (m *Manager) Alerts(_ context.Context, opts AlertsOpts) (AlertsResponse, er alerts := make([]Alert, 0, len(m.alerts)) for _, a := range m.alerts { + resp.Total++ + if a.Severity == SeverityInfo { + resp.TotalInfo++ + } else if a.Severity == SeverityWarning { + resp.TotalWarning++ + } else if a.Severity == SeverityError { + resp.TotalError++ + } else if a.Severity == SeverityCritical { + resp.TotalCritical++ + } + if opts.Severity != 0 && a.Severity != opts.Severity { + continue // filter by severity + } alerts = append(alerts, a) } sort.Slice(alerts, func(i, j int) bool { diff --git a/bus/bus.go b/bus/bus.go index e7e6ddaac..4106bc231 100644 --- a/bus/bus.go +++ b/bus/bus.go @@ -1739,6 +1739,7 @@ func (b *bus) handleGETAlerts(jc jape.Context) { return } offset, limit := 0, -1 + var severity alerts.Severity if jc.DecodeForm("offset", &offset) != nil { return } else if jc.DecodeForm("limit", &limit) != nil { @@ -1746,8 +1747,14 @@ func (b *bus) handleGETAlerts(jc jape.Context) { } else if offset < 0 { jc.Error(errors.New("offset must be non-negative"), http.StatusBadRequest) return + } else if jc.DecodeForm("severity", &severity) != nil { + return } - ar, err := b.alertMgr.Alerts(jc.Request.Context(), alerts.AlertsOpts{Offset: offset, Limit: limit}) + ar, err := b.alertMgr.Alerts(jc.Request.Context(), alerts.AlertsOpts{ + Offset: offset, + Limit: limit, + Severity: severity, + }) if jc.Check("failed to fetch alerts", err) != nil { return } diff --git a/bus/client/alerts.go b/bus/client/alerts.go index 7eceaeaed..28c3b9a84 100644 --- a/bus/client/alerts.go +++ b/bus/client/alerts.go @@ -16,6 +16,9 @@ func (c *Client) Alerts(ctx context.Context, opts alerts.AlertsOpts) (resp alert if opts.Limit != 0 { values.Set("limit", fmt.Sprint(opts.Limit)) } + if opts.Severity != 0 { + values.Set("severity", opts.Severity.String()) + } err = c.c.WithContext(ctx).GET("/alerts?"+values.Encode(), &resp) return } diff --git a/internal/testing/cluster_test.go b/internal/testing/cluster_test.go index f30a0906a..69b318f66 100644 --- a/internal/testing/cluster_test.go +++ b/internal/testing/cluster_test.go @@ -1974,6 +1974,44 @@ func TestAlerts(t *testing.T) { if len(foundAlerts) != 1 || foundAlerts[0].ID != alert2.ID { t.Fatal("wrong alert") } + + // register more alerts + for severity := alerts.SeverityInfo; severity <= alerts.SeverityCritical; severity++ { + for j := 0; j < 3*int(severity); j++ { + tt.OK(b.RegisterAlert(context.Background(), alerts.Alert{ + ID: frand.Entropy256(), + Severity: severity, + Message: "test", + Data: map[string]interface{}{ + "origin": "test", + }, + Timestamp: time.Now(), + })) + } + } + for severity := alerts.SeverityInfo; severity <= alerts.SeverityCritical; severity++ { + ar, err = b.Alerts(context.Background(), alerts.AlertsOpts{Severity: severity}) + tt.OK(err) + if ar.Total != 32 { + t.Fatal("expected 32 alerts", ar.Total) + } else if ar.TotalInfo != 3 { + t.Fatal("expected 3 info alerts", ar.TotalInfo) + } else if ar.TotalWarning != 6 { + t.Fatal("expected 6 warning alerts", ar.TotalWarning) + } else if ar.TotalError != 9 { + t.Fatal("expected 9 error alerts", ar.TotalError) + } else if ar.TotalCritical != 14 { + t.Fatal("expected 14 critical alerts", ar.TotalCritical) + } else if severity == alerts.SeverityInfo && len(ar.Alerts) != ar.TotalInfo { + t.Fatalf("expected %v info alerts, got %v", ar.TotalInfo, len(ar.Alerts)) + } else if severity == alerts.SeverityWarning && len(ar.Alerts) != ar.TotalWarning { + t.Fatalf("expected %v warning alerts, got %v", ar.TotalWarning, len(ar.Alerts)) + } else if severity == alerts.SeverityError && len(ar.Alerts) != ar.TotalError { + t.Fatalf("expected %v error alerts, got %v", ar.TotalError, len(ar.Alerts)) + } else if severity == alerts.SeverityCritical && len(ar.Alerts) != ar.TotalCritical { + t.Fatalf("expected %v critical alerts, got %v", ar.TotalCritical, len(ar.Alerts)) + } + } } func TestMultipartUploads(t *testing.T) {