mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
NOISSUE - Simplify alarms
Signed-off-by: dusan <borovcanindusan1@gmail.com>
This commit is contained in:
+15
-5
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user