NOISSUE - Add Alarms (#106)

* WIP: alarms service

* fix(alarms): remove rule entity since it is not stored here

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* test(alarms): add tests cases for invalid alarms

* feat(alarms): add authorization

* feat(alarms): add docker deployment files

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix: update go mod file

* feat(alarms): support filtering by resolved_by, updated_by and severity

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* style: fix linter errors

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): provide correct otel naming for create alarm

Fixes https://github.com/absmach/magistrala/pull/106#discussion_r2030151971

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): group routes appropriately

Resolves https://github.com/absmach/magistrala/pull/106#discussion_r2030160891

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): extract alarm id from url path rather than query params

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): add all status to help in decoding

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* style(alarms): maintain consistent import as naming for supermq api package

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* refactor(alarms): update supermq dependecy to the latest

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): Add domains gRPC service config to alarms service

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* test(alarms): all CRUD operations from the service

Return empty results instead of nil

This standardizes error responses across alarm endpoints to return empty
result structs rather than nil. Also renames entityReq to alarmReq and
adds HTTP status codes for created/deleted alarms.

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* test(alarms): fix failing tests due to introduction of context on sdk

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): remove channel id

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): standardize error handling across CRUD operations

Updated error responses to use specific repository errors for consistency

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): add assignment fields to Alarm model and database

Introduced AssignedAt and AssignedBy fields to the Alarm struct and updated the database schema accordingly. Enhanced the UpdateAlarm function to handle these new fields, ensuring proper assignment tracking in the alarms system.

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): enhance Alarm model with measurement attributes

Updated the Alarm struct to include Measurement, Value, Unit, and Cause fields. Modified the validation logic to ensure these fields are present. Adjusted logging and tracing middleware to reflect the new attributes. Updated database schema and related functions to accommodate these changes, ensuring comprehensive alarm data management.

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): consume events from pubsub for creation of alarms

Removed session dependencies from CreateAlarm method and enhanced alarm validation to ensure all required fields are present

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* style(alarms): add newline at the end of docker compose

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): Add assignee id and metadata fields when consuming messages

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): add acknowledged field

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): Add threshold value for the specific measurement

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): Add channel, thing, and subtopic fields to Alarm model

This change adds required fields for tracking alarm sources and reorganizes
alarm-related fields for better grouping. Alarms now track the channel,
thing, and subtopic that triggered them, along with domain and rule info.

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* test(alarms): add service layer tests

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): consume created at from message rather than creating it

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): ready alarm as a gob encoded object

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): read alarms from alarms queue and remove transformer

g

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): update version of supermq

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* feat(alarms): add gob transformer

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): rename thing id to client id

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): create alarms stream

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): check on logic to create new alarm

create new alarm if severity, status, subtopic changes
enhance logging with additional details for alarms management

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* remove conusmer and use pubsub

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>

* fix(alarms): use build tags for rabbitmq and nats

* fix(alarms): add health and metrics endpoint

* fix(magistrala): use supermq as build flags to see version and commit

* fix(alarms): use js config

* fix(alarms): remove validation when updating an alarm

fix authorization too

---------

Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>
This commit is contained in:
b1ackd0t
2025-04-15 20:32:09 +03:00
committed by GitHub
parent 629838a571
commit b3e2f41194
59 changed files with 4170 additions and 501 deletions
+4
View File
@@ -33,3 +33,7 @@ packages:
github.com/absmach/magistrala/provision:
interfaces:
Service:
github.com/absmach/magistrala/alarms:
interfaces:
Service:
Repository:
+2 -2
View File
@@ -30,8 +30,8 @@ func main() {
"go run tools/e2e/cmd/main.go\n" +
"go run tools/e2e/cmd/main.go --host 142.93.118.47\n" +
"go run tools/e2e/cmd/main.go --host localhost --num 10 --num_of_messages 100 --prefix e2e",
Run: func(_ *cobra.Command, _ []string) {
e2e.Test(econf)
Run: func(cmd *cobra.Command, _ []string) {
e2e.Test(cmd.Context(), econf)
},
}
+29 -37
View File
@@ -66,7 +66,7 @@ type Config struct {
// - Connect client to channel
// - Publish message from HTTP, MQTT, WS and CoAP Adapters.
func Test(conf Config) {
func Test(ctx context.Context, conf Config) {
sdkConf := sdk.Config{
UsersURL: fmt.Sprintf("http://%s:%s", conf.Host, usersPort),
GroupsURL: fmt.Sprintf("http://%s:%s", conf.Host, groupsPort),
@@ -82,51 +82,51 @@ func Test(conf Config) {
magenta := color.FgLightMagenta.Render
domainID, token, err := createUser(s, conf)
domainID, token, err := createUser(ctx, s, conf)
if err != nil {
errExit(fmt.Errorf("unable to create user: %w", err))
}
color.Success.Printf("created user with token %s\n", magenta(token))
color.Success.Printf("created domain with ID %s\n", magenta(domainID))
users, err := createUsers(s, conf, token)
users, err := createUsers(ctx, s, conf, token)
if err != nil {
errExit(fmt.Errorf("unable to create users: %w", err))
}
color.Success.Printf("created users of ids:\n%s\n", magenta(getIDS(users)))
groups, err := createGroups(s, conf, domainID, token)
groups, err := createGroups(ctx, s, conf, domainID, token)
if err != nil {
errExit(fmt.Errorf("unable to create groups: %w", err))
}
color.Success.Printf("created groups of ids:\n%s\n", magenta(getIDS(groups)))
clients, err := createClients(s, conf, domainID, token)
clients, err := createClients(ctx, s, conf, domainID, token)
if err != nil {
errExit(fmt.Errorf("unable to create clients: %w", err))
}
color.Success.Printf("created clients of ids:\n%s\n", magenta(getIDS(clients)))
channels, err := createChannels(s, conf, domainID, token)
channels, err := createChannels(ctx, s, conf, domainID, token)
if err != nil {
errExit(fmt.Errorf("unable to create channels: %w", err))
}
color.Success.Printf("created channels of ids:\n%s\n", magenta(getIDS(channels)))
// List users, groups, clients and channels
if err := read(s, conf, domainID, token, users, groups, clients, channels); err != nil {
if err := read(ctx, s, conf, domainID, token, users, groups, clients, channels); err != nil {
errExit(fmt.Errorf("unable to read users, groups, clients and channels: %w", err))
}
color.Success.Println("viewed users, groups, clients and channels")
// Update users, groups, clients and channels
if err := update(s, domainID, token, users, groups, clients, channels); err != nil {
if err := update(ctx, s, domainID, token, users, groups, clients, channels); err != nil {
errExit(fmt.Errorf("unable to update users, groups, clients and channels: %w", err))
}
color.Success.Println("updated users, groups, clients and channels")
// Send messages to channels
if err := messaging(s, conf, domainID, token, clients, channels); err != nil {
if err := messaging(ctx, s, conf, domainID, token, clients, channels); err != nil {
errExit(fmt.Errorf("unable to send messages to channels: %w", err))
}
color.Success.Println("sent messages to channels")
@@ -137,7 +137,7 @@ func errExit(err error) {
os.Exit(1)
}
func createUser(s sdk.SDK, conf Config) (string, string, error) {
func createUser(ctx context.Context, s sdk.SDK, conf Config) (string, string, error) {
user := sdk.User{
FirstName: fmt.Sprintf("%s%s", conf.Prefix, namesgenerator.Generate()),
LastName: fmt.Sprintf("%s%s", conf.Prefix, namesgenerator.Generate()),
@@ -149,8 +149,8 @@ func createUser(s sdk.SDK, conf Config) (string, string, error) {
Status: sdk.EnabledStatus,
Role: "admin",
}
ctx := context.Background()
if _, err := s.CreateUser(context.Background(), user, ""); err != nil {
if _, err := s.CreateUser(ctx, user, ""); err != nil {
return "", "", fmt.Errorf("unable to create user: %w", err)
}
@@ -187,10 +187,9 @@ func createUser(s sdk.SDK, conf Config) (string, string, error) {
return domain.ID, token.AccessToken, nil
}
func createUsers(s sdk.SDK, conf Config, token string) ([]sdk.User, error) {
func createUsers(ctx context.Context, s sdk.SDK, conf Config, token string) ([]sdk.User, error) {
var err error
users := []sdk.User{}
ctx := context.Background()
for i := uint64(0); i < conf.Num; i++ {
user := sdk.User{
@@ -214,10 +213,9 @@ func createUsers(s sdk.SDK, conf Config, token string) ([]sdk.User, error) {
return users, nil
}
func createGroups(s sdk.SDK, conf Config, domainID, token string) ([]sdk.Group, error) {
func createGroups(ctx context.Context, s sdk.SDK, conf Config, domainID, token string) ([]sdk.Group, error) {
var err error
groups := []sdk.Group{}
ctx := context.Background()
for i := uint64(0); i < conf.Num; i++ {
group := sdk.Group{
@@ -235,10 +233,9 @@ func createGroups(s sdk.SDK, conf Config, domainID, token string) ([]sdk.Group,
return groups, nil
}
func createClientsInBatch(s sdk.SDK, conf Config, domainID, token string, num uint64) ([]sdk.Client, error) {
func createClientsInBatch(ctx context.Context, s sdk.SDK, conf Config, domainID, token string, num uint64) ([]sdk.Client, error) {
var err error
clients := make([]sdk.Client, num)
ctx := context.Background()
for i := uint64(0); i < num; i++ {
clients[i] = sdk.Client{
@@ -254,25 +251,25 @@ func createClientsInBatch(s sdk.SDK, conf Config, domainID, token string, num ui
return clients, nil
}
func createClients(s sdk.SDK, conf Config, domainID, token string) ([]sdk.Client, error) {
func createClients(ctx context.Context, s sdk.SDK, conf Config, domainID, token string) ([]sdk.Client, error) {
clients := []sdk.Client{}
if conf.Num > batchSize {
batches := int(conf.Num) / batchSize
for i := 0; i < batches; i++ {
ths, err := createClientsInBatch(s, conf, domainID, token, batchSize)
ths, err := createClientsInBatch(ctx, s, conf, domainID, token, batchSize)
if err != nil {
return []sdk.Client{}, fmt.Errorf("failed to create the clients: %w", err)
}
clients = append(clients, ths...)
}
ths, err := createClientsInBatch(s, conf, domainID, token, conf.Num%uint64(batchSize))
ths, err := createClientsInBatch(ctx, s, conf, domainID, token, conf.Num%uint64(batchSize))
if err != nil {
return []sdk.Client{}, fmt.Errorf("failed to create the clients: %w", err)
}
clients = append(clients, ths...)
} else {
ths, err := createClientsInBatch(s, conf, domainID, token, conf.Num)
ths, err := createClientsInBatch(ctx, s, conf, domainID, token, conf.Num)
if err != nil {
return []sdk.Client{}, fmt.Errorf("failed to create the clients: %w", err)
}
@@ -282,10 +279,9 @@ func createClients(s sdk.SDK, conf Config, domainID, token string) ([]sdk.Client
return clients, nil
}
func createChannelsInBatch(s sdk.SDK, conf Config, domainID, token string, num uint64) ([]sdk.Channel, error) {
func createChannelsInBatch(ctx context.Context, s sdk.SDK, conf Config, domainID, token string, num uint64) ([]sdk.Channel, error) {
var err error
channels := make([]sdk.Channel, num)
ctx := context.Background()
for i := uint64(0); i < num; i++ {
channels[i] = sdk.Channel{
@@ -300,25 +296,25 @@ func createChannelsInBatch(s sdk.SDK, conf Config, domainID, token string, num u
return channels, nil
}
func createChannels(s sdk.SDK, conf Config, domainID, token string) ([]sdk.Channel, error) {
func createChannels(ctx context.Context, s sdk.SDK, conf Config, domainID, token string) ([]sdk.Channel, error) {
channels := []sdk.Channel{}
if conf.Num > batchSize {
batches := int(conf.Num) / batchSize
for i := 0; i < batches; i++ {
chs, err := createChannelsInBatch(s, conf, token, domainID, batchSize)
chs, err := createChannelsInBatch(ctx, s, conf, token, domainID, batchSize)
if err != nil {
return []sdk.Channel{}, fmt.Errorf("failed to create the channels: %w", err)
}
channels = append(channels, chs...)
}
chs, err := createChannelsInBatch(s, conf, domainID, token, conf.Num%uint64(batchSize))
chs, err := createChannelsInBatch(ctx, s, conf, domainID, token, conf.Num%uint64(batchSize))
if err != nil {
return []sdk.Channel{}, fmt.Errorf("failed to create the channels: %w", err)
}
channels = append(channels, chs...)
} else {
chs, err := createChannelsInBatch(s, conf, domainID, token, conf.Num)
chs, err := createChannelsInBatch(ctx, s, conf, domainID, token, conf.Num)
if err != nil {
return []sdk.Channel{}, fmt.Errorf("failed to create the channels: %w", err)
}
@@ -328,8 +324,7 @@ func createChannels(s sdk.SDK, conf Config, domainID, token string) ([]sdk.Chann
return channels, nil
}
func read(s sdk.SDK, conf Config, domainID, token string, users []sdk.User, groups []sdk.Group, clients []sdk.Client, channels []sdk.Channel) error {
ctx := context.Background()
func read(ctx context.Context, s sdk.SDK, conf Config, domainID, token string, users []sdk.User, groups []sdk.Group, clients []sdk.Client, channels []sdk.Channel) error {
for _, user := range users {
if _, err := s.User(ctx, user.ID, token); err != nil {
return fmt.Errorf("failed to get user %w", err)
@@ -382,8 +377,7 @@ func read(s sdk.SDK, conf Config, domainID, token string, users []sdk.User, grou
return nil
}
func update(s sdk.SDK, domainID, token string, users []sdk.User, groups []sdk.Group, clients []sdk.Client, channels []sdk.Channel) error {
ctx := context.Background()
func update(ctx context.Context, s sdk.SDK, domainID, token string, users []sdk.User, groups []sdk.Group, clients []sdk.Client, channels []sdk.Channel) error {
for _, user := range users {
user.FirstName = namesgenerator.Generate()
user.Metadata = sdk.Metadata{"Update": namesgenerator.Generate()}
@@ -545,8 +539,7 @@ func update(s sdk.SDK, domainID, token string, users []sdk.User, groups []sdk.Gr
return nil
}
func messaging(s sdk.SDK, conf Config, domainID, token string, clients []sdk.Client, channels []sdk.Channel) error {
ctx := context.Background()
func messaging(ctx context.Context, s sdk.SDK, conf Config, domainID, token string, clients []sdk.Client, channels []sdk.Channel) error {
for _, c := range clients {
for _, channel := range channels {
conn := sdk.Connection{
@@ -569,7 +562,7 @@ func messaging(s sdk.SDK, conf Config, domainID, token string, clients []sdk.Cli
func(num int64, client sdk.Client, channel sdk.Channel) {
g.Go(func() error {
msg := fmt.Sprintf(msgFormat, num+1, rand.Int())
return sendHTTPMessage(s, msg, client, channel.ID)
return sendHTTPMessage(ctx, s, msg, client, channel.ID)
})
g.Go(func() error {
msg := fmt.Sprintf(msgFormat, num+2, rand.Int())
@@ -592,8 +585,7 @@ func messaging(s sdk.SDK, conf Config, domainID, token string, clients []sdk.Cli
return g.Wait()
}
func sendHTTPMessage(s sdk.SDK, msg string, client sdk.Client, chanID string) error {
ctx := context.Background()
func sendHTTPMessage(ctx context.Context, s sdk.SDK, msg string, client sdk.Client, chanID string) error {
if err := s.SendMessage(ctx, client.DomainID, chanID, msg, client.Credentials.Secret); err != nil {
return fmt.Errorf("HTTP failed to send message from client %s to channel %s: %w", client.ID, chanID, err)
}
+2 -2
View File
@@ -19,8 +19,8 @@ func main() {
Short: "provision is provisioning tool for SuperMQ",
Long: `Tool for provisioning series of SuperMQ channels and clients and connecting them together.
Complete documentation is available at https://docs.supermq.abstractmachines.fr`,
Run: func(_ *cobra.Command, _ []string) {
if err := provision.Provision(pconf); err != nil {
Run: func(cmd *cobra.Command, _ []string) {
if err := provision.Provision(cmd.Context(), pconf); err != nil {
log.Fatal(err)
}
},
+1 -2
View File
@@ -56,8 +56,7 @@ type Config struct {
}
// Provision - function that does actual provisiong.
func Provision(conf Config) error {
ctx := context.Background()
func Provision(ctx context.Context, conf Config) error {
const (
rsaBits = 4096
ttl = "2400h"