NOISSUE - Simplify alarms

Signed-off-by: dusan <borovcanindusan1@gmail.com>
This commit is contained in:
dusan
2026-06-29 09:11:15 +02:00
parent 45c426c8bf
commit afd2665c9c
7 changed files with 168 additions and 26 deletions
+15 -5
View File
@@ -4,20 +4,17 @@
package alarms
import (
"fmt"
"time"
"github.com/absmach/magistrala/internal/atom"
)
func alarmProjection(a Alarm) atom.Resource {
name := a.ID
if name == "" {
name = a.Cause
}
res := atom.ResourceFromFields(atom.ObjectFields{
ID: a.ID,
Kind: atom.KindAlarm,
Name: name,
Name: alarmName(a),
TenantID: a.DomainID,
OwnerID: a.AssigneeID,
Status: a.Status.String(),
@@ -47,6 +44,19 @@ func alarmProjection(a Alarm) atom.Resource {
return res
}
func alarmName(a Alarm) string {
if a.Cause != "" && a.Measurement != "" {
return fmt.Sprintf("%s: %s", a.Measurement, a.Cause)
}
if a.Cause != "" {
return a.Cause
}
if a.Measurement != "" {
return fmt.Sprintf("%s alarm", a.Measurement)
}
return a.ID
}
func alarmTimeString(ts time.Time) string {
if ts.IsZero() {
return ""
+45 -7
View File
@@ -104,7 +104,7 @@ func (repo *atomRepository) loadAlarm(ctx context.Context, alarmID, domainID str
}
func (repo *atomRepository) ListAllAlarms(ctx context.Context, pm PageMetadata) (AlarmsPage, error) {
items, err := repo.listAlarms(ctx, pm.DomainID)
items, err := repo.listAlarms(ctx, pm.DomainID, alarmPageAttributesContains(pm))
if err != nil {
return AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
@@ -147,7 +147,7 @@ func (repo *atomRepository) DeleteAlarm(ctx context.Context, id string) error {
}
func (repo *atomRepository) shouldCreateAlarm(ctx context.Context, alarm Alarm) (bool, error) {
items, err := repo.listAlarms(ctx, alarm.DomainID)
items, err := repo.listAlarms(ctx, alarm.DomainID, alarmIdentityAttributes(alarm))
if err != nil {
return false, err
}
@@ -175,15 +175,16 @@ func (repo *atomRepository) shouldCreateAlarm(ctx context.Context, alarm Alarm)
return alarm.Status == ActiveStatus && latest.Severity != alarm.Severity, nil
}
func (repo *atomRepository) listAlarms(ctx context.Context, domainID string) ([]Alarm, error) {
func (repo *atomRepository) listAlarms(ctx context.Context, domainID string, attributes atom.Attributes) ([]Alarm, error) {
var out []Alarm
var offset uint64
for {
page, err := repo.store.ListResources(ctx, atom.Query{
Kind: atom.KindAlarm,
TenantID: domainID,
Limit: atomAlarmListLimit,
Offset: offset,
Kind: atom.KindAlarm,
TenantID: domainID,
AttributesContains: attributes,
Limit: atomAlarmListLimit,
Offset: offset,
})
if err != nil {
return nil, err
@@ -209,6 +210,43 @@ func (repo *atomRepository) listAlarms(ctx context.Context, domainID string) ([]
return out, nil
}
func alarmIdentityAttributes(alarm Alarm) atom.Attributes {
attrs := atom.Attributes{}
setAttrIfNotEmpty(attrs, "rule_id", alarm.RuleID)
setAttrIfNotEmpty(attrs, "channel_id", alarm.ChannelID)
setAttrIfNotEmpty(attrs, "client_id", alarm.ClientID)
setAttrIfNotEmpty(attrs, "subtopic", alarm.Subtopic)
setAttrIfNotEmpty(attrs, "measurement", alarm.Measurement)
return attrs
}
func alarmPageAttributesContains(pm PageMetadata) atom.Attributes {
attrs := atom.Attributes{}
setAttrIfNotEmpty(attrs, "rule_id", pm.RuleID)
setAttrIfNotEmpty(attrs, "channel_id", pm.ChannelID)
setAttrIfNotEmpty(attrs, "client_id", pm.ClientID)
setAttrIfNotEmpty(attrs, "subtopic", pm.Subtopic)
setAttrIfNotEmpty(attrs, "measurement", pm.Measurement)
if pm.Status != AllStatus {
attrs["status"] = pm.Status.String()
}
if pm.Severity != math.MaxUint8 {
attrs["severity"] = pm.Severity
}
setAttrIfNotEmpty(attrs, "assignee_id", pm.AssigneeID)
setAttrIfNotEmpty(attrs, "updated_by", pm.UpdatedBy)
setAttrIfNotEmpty(attrs, "assigned_by", pm.AssignedBy)
setAttrIfNotEmpty(attrs, "acknowledged_by", pm.AcknowledgedBy)
setAttrIfNotEmpty(attrs, "resolved_by", pm.ResolvedBy)
return attrs
}
func setAttrIfNotEmpty(attrs atom.Attributes, key, value string) {
if value != "" {
attrs[key] = value
}
}
func mergeAlarmUpdate(current, update Alarm) Alarm {
if update.Status != 0 {
current.Status = update.Status
+47 -1
View File
@@ -6,6 +6,7 @@ package alarms
import (
"context"
"math"
"reflect"
"testing"
"time"
@@ -30,6 +31,16 @@ func TestAtomRepositoryCreateAlarmSuppressesDuplicateActiveSeverity(t *testing.T
if store.created.ID != "" {
t.Fatalf("duplicate alarm should not be created: %#v", store.created)
}
if len(store.queries) != 1 {
t.Fatalf("expected one duplicate lookup query, got %d", len(store.queries))
}
assertAttributesContain(t, store.queries[0].AttributesContains, atom.Attributes{
"rule_id": existing.RuleID,
"channel_id": existing.ChannelID,
"client_id": existing.ClientID,
"subtopic": existing.Subtopic,
"measurement": existing.Measurement,
})
}
func TestAtomRepositoryCreateAlarmCreatesOnSeverityChange(t *testing.T) {
@@ -49,7 +60,7 @@ func TestAtomRepositoryCreateAlarmCreatesOnSeverityChange(t *testing.T) {
if created.ID != next.ID || created.Severity != next.Severity {
t.Fatalf("unexpected created alarm: %#v", created)
}
if store.created.ID != next.ID || store.created.Kind != atom.KindAlarm || store.created.Name != next.ID {
if store.created.ID != next.ID || store.created.Kind != atom.KindAlarm || store.created.Name != alarmName(next) {
t.Fatalf("unexpected created resource: %#v", store.created)
}
}
@@ -122,6 +133,18 @@ func TestAtomRepositoryListAlarmsFiltersAndSorts(t *testing.T) {
if page.Alarms[0].ID != second.ID || page.Alarms[1].ID != first.ID {
t.Fatalf("unexpected order: %#v", page.Alarms)
}
if len(store.queries) != 1 {
t.Fatalf("expected one list query, got %d", len(store.queries))
}
assertAttributesContain(t, store.queries[0].AttributesContains, atom.Attributes{
"channel_id": "channel-1",
})
if _, ok := store.queries[0].AttributesContains["status"]; ok {
t.Fatalf("status should not be pushed for all-status query: %#v", store.queries[0].AttributesContains)
}
if _, ok := store.queries[0].AttributesContains["severity"]; ok {
t.Fatalf("severity should not be pushed for sentinel query: %#v", store.queries[0].AttributesContains)
}
}
func testAlarm(id string, createdAt time.Time) Alarm {
@@ -148,6 +171,7 @@ type alarmAtomStore struct {
created atom.Resource
updated atom.Resource
deleted string
queries []atom.Query
}
func (s *alarmAtomStore) CreateResource(_ context.Context, resource atom.Resource) (atom.Resource, error) {
@@ -177,6 +201,7 @@ func (s *alarmAtomStore) GetResource(_ context.Context, id string) (atom.Resourc
}
func (s *alarmAtomStore) ListResources(_ context.Context, q atom.Query) (atom.ResourceList, error) {
s.queries = append(s.queries, q)
var items []atom.Resource
for _, res := range s.resources {
if q.Kind != "" && res.Kind != q.Kind {
@@ -185,6 +210,9 @@ func (s *alarmAtomStore) ListResources(_ context.Context, q atom.Query) (atom.Re
if q.TenantID != "" && res.TenantID != q.TenantID {
continue
}
if !resourceAttributesContain(res.Attributes, q.AttributesContains) {
continue
}
items = append(items, res)
}
total := uint64(len(items))
@@ -199,6 +227,24 @@ func (s *alarmAtomStore) ListResources(_ context.Context, q atom.Query) (atom.Re
return atom.ResourceList{Items: items[start:end], Total: total}, nil
}
func resourceAttributesContain(attrs, contains atom.Attributes) bool {
for key, want := range contains {
if !reflect.DeepEqual(attrs[key], want) {
return false
}
}
return true
}
func assertAttributesContain(t *testing.T, got, want atom.Attributes) {
t.Helper()
for key, wantValue := range want {
if !reflect.DeepEqual(got[key], wantValue) {
t.Fatalf("attribute %q mismatch: got %#v want %#v in %#v", key, got[key], wantValue, got)
}
}
}
func (s *alarmAtomStore) DeleteResource(_ context.Context, id string) error {
s.deleted = id
for i, res := range s.resources {
+33 -1
View File
@@ -25,7 +25,7 @@ func TestAlarmProjectionBuildsAtomResource(t *testing.T) {
Status: ActiveStatus,
})
if resource.ID != "alarm-1" || resource.Kind != atom.KindAlarm || resource.Name != "alarm-1" {
if resource.ID != "alarm-1" || resource.Kind != atom.KindAlarm || resource.Name != "temperature: high temperature" {
t.Fatalf("unexpected projection: %#v", resource)
}
if resource.Attributes["rule_id"] != "rule-1" {
@@ -35,3 +35,35 @@ func TestAlarmProjectionBuildsAtomResource(t *testing.T) {
t.Fatalf("missing alarm value projection: %#v", resource.Attributes)
}
}
func TestAlarmNameFallbacks(t *testing.T) {
cases := []struct {
name string
alarm Alarm
want string
}{
{
name: "cause only",
alarm: Alarm{ID: "alarm-1", Cause: "high temperature"},
want: "high temperature",
},
{
name: "measurement only",
alarm: Alarm{ID: "alarm-1", Measurement: "temperature"},
want: "temperature alarm",
},
{
name: "id fallback",
alarm: Alarm{ID: "alarm-1"},
want: "alarm-1",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := alarmName(tc.alarm); got != tc.want {
t.Fatalf("unexpected alarm name: got %q want %q", got, tc.want)
}
})
}
}
+5 -2
View File
@@ -554,8 +554,11 @@ func (c *Client) ListResources(ctx context.Context, q Query) (ResourceList, erro
if q.Name != "" && q.Q == "" {
vars["q"] = q.Name
}
err := c.graphQL(ctx, `query Resources($q: String, $kind: String, $tenantId: ID, $limit: Int, $offset: Int) {
resources(q: $q, kind: $kind, tenantId: $tenantId, limit: $limit, offset: $offset) {
if len(q.AttributesContains) > 0 {
vars["attributesContains"] = q.AttributesContains
}
err := c.graphQL(ctx, `query Resources($q: String, $kind: String, $tenantId: ID, $attributesContains: JSON, $limit: Int, $offset: Int) {
resources(q: $q, kind: $kind, tenantId: $tenantId, attributesContains: $attributesContains, limit: $limit, offset: $offset) {
total
items { id kind name tenant_id: tenantId owner_id: ownerId attributes created_at: createdAt updated_at: updatedAt }
}
+13 -1
View File
@@ -65,14 +65,22 @@ func TestListResources(t *testing.T) {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
var payload struct {
Query string `json:"query"`
Variables map[string]any `json:"variables"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatalf("decode request: %v", err)
}
if !strings.Contains(payload.Query, "$attributesContains: JSON") || !strings.Contains(payload.Query, "attributesContains: $attributesContains") {
t.Fatalf("query does not include attributesContains: %s", payload.Query)
}
if payload.Variables["kind"] != KindRule || payload.Variables["tenantId"] != testDomainID {
t.Fatalf("unexpected variables: %+v", payload.Variables)
}
attrs, ok := payload.Variables["attributesContains"].(map[string]any)
if !ok || attrs["status"] != "active" {
t.Fatalf("unexpected attributesContains: %+v", payload.Variables)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"resources": map[string]any{
@@ -85,7 +93,11 @@ func TestListResources(t *testing.T) {
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
got, err := client.ListResources(context.Background(), Query{Kind: KindRule, TenantID: testDomainID})
got, err := client.ListResources(context.Background(), Query{
Kind: KindRule,
TenantID: testDomainID,
AttributesContains: Attributes{"status": "active"},
})
if err != nil {
t.Fatalf("list failed: %v", err)
}
+10 -9
View File
@@ -55,15 +55,16 @@ type Resource struct {
}
type Query struct {
IDs []string
Q string
Kind string
TenantID string
Name string
Route string
Status string
Limit uint64
Offset uint64
IDs []string
Q string
Kind string
TenantID string
Name string
Route string
Status string
AttributesContains Attributes
Limit uint64
Offset uint64
}
type AuthzRequest struct {