Files
supermq/pkg/sdk/sdk.go
T
b1ackd0t b3e2f41194 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>
2025-04-15 19:32:09 +02:00

351 lines
10 KiB
Go

// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package sdk
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/absmach/supermq/pkg/errors"
smqSDK "github.com/absmach/supermq/pkg/sdk"
"moul.io/http2curl"
)
var _ SDK = (*mgSDK)(nil)
type Metadata map[string]interface{}
type PageMetadata struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Metadata Metadata `json:"metadata,omitempty"`
Topic string `json:"topic,omitempty"`
Contact string `json:"contact,omitempty"`
DomainID string `json:"domain_id,omitempty"`
Level uint64 `json:"level,omitempty"`
State string `json:"state,omitempty"`
Name string `json:"name,omitempty"`
}
type MessagePageMetadata struct {
PageMetadata
Subtopic string `json:"subtopic,omitempty"`
Publisher string `json:"publisher,omitempty"`
Comparator string `json:"comparator,omitempty"`
BoolValue *bool `json:"vb,omitempty"`
StringValue string `json:"vs,omitempty"`
DataValue string `json:"vd,omitempty"`
From float64 `json:"from,omitempty"`
To float64 `json:"to,omitempty"`
Aggregation string `json:"aggregation,omitempty"`
Interval string `json:"interval,omitempty"`
Value float64 `json:"value,omitempty"`
Protocol string `json:"protocol,omitempty"`
}
// SDK contains Magistrala API.
type SDK interface {
smqSDK.SDK
// AddBootstrap add bootstrap configuration
//
// example:
// cfg := sdk.BootstrapConfig{
// ClientID: "clientID",
// Name: "bootstrap",
// ExternalID: "externalID",
// ExternalKey: "externalKey",
// Channels: []string{"channel1", "channel2"},
// }
// id, _ := sdk.AddBootstrap(ctx, cfg, "domainID", "token")
// fmt.Println(id)
AddBootstrap(ctx context.Context, cfg BootstrapConfig, domainID, token string) (string, errors.SDKError)
// View returns Client Config with given ID belonging to the user identified by the given token.
//
// example:
// bootstrap, _ := sdk.ViewBootstrap(ctx, "id", "domainID", "token")
// fmt.Println(bootstrap)
ViewBootstrap(ctx context.Context, id, domainID, token string) (BootstrapConfig, errors.SDKError)
// Update updates editable fields of the provided Config.
//
// example:
// cfg := sdk.BootstrapConfig{
// ClientID: "clientID",
// Name: "bootstrap",
// ExternalID: "externalID",
// ExternalKey: "externalKey",
// Channels: []string{"channel1", "channel2"},
// }
// err := sdk.UpdateBootstrap(ctx, cfg, "domainID", "token")
// fmt.Println(err)
UpdateBootstrap(ctx context.Context, cfg BootstrapConfig, domainID, token string) errors.SDKError
// Update bootstrap config certificates.
//
// example:
// err := sdk.UpdateBootstrapCerts(ctx, "id", "clientCert", "clientKey", "ca", "domainID", "token")
// fmt.Println(err)
UpdateBootstrapCerts(ctx context.Context, id string, clientCert, clientKey, ca string, domainID, token string) (BootstrapConfig, errors.SDKError)
// UpdateBootstrapConnection updates connections performs update of the channel list corresponding Client is connected to.
//
// example:
// err := sdk.UpdateBootstrapConnection(ctx, "id", []string{"channel1", "channel2"}, "domainID", "token")
// fmt.Println(err)
UpdateBootstrapConnection(ctx context.Context, id string, channels []string, domainID, token string) errors.SDKError
// Remove removes Config with specified token that belongs to the user identified by the given token.
//
// example:
// err := sdk.RemoveBootstrap(ctx, "id", "domainID", "token")
// fmt.Println(err)
RemoveBootstrap(ctx context.Context, id, domainID, token string) errors.SDKError
// Bootstrap returns Config to the Client with provided external ID using external key.
//
// example:
// bootstrap, _ := sdk.Bootstrap(ctx, "externalID", "externalKey")
// fmt.Println(bootstrap)
Bootstrap(ctx context.Context, externalID, externalKey string) (BootstrapConfig, errors.SDKError)
// BootstrapSecure retrieves a configuration with given external ID and encrypted external key.
//
// example:
// bootstrap, _ := sdk.BootstrapSecure(ctx, "externalID", "externalKey", "cryptoKey")
// fmt.Println(bootstrap)
BootstrapSecure(ctx context.Context, externalID, externalKey, cryptoKey string) (BootstrapConfig, errors.SDKError)
// Bootstraps retrieves a list of managed configs.
//
// example:
// pm := sdk.PageMetadata{
// Offset: 0,
// Limit: 10,
// }
// bootstraps, _ := sdk.Bootstraps(ctx, pm, "domainID", "token")
// fmt.Println(bootstraps)
Bootstraps(ctx context.Context, pm PageMetadata, domainID, token string) (BootstrapPage, errors.SDKError)
// Whitelist updates Client state Config with given ID belonging to the user identified by the given token.
//
// example:
// err := sdk.Whitelist(ctx, "clientID", 1, "domainID", "token")
// fmt.Println(err)
Whitelist(ctx context.Context, clientID string, state int, domainID, token string) errors.SDKError
// ReadMessages read messages of specified channel.
//
// example:
// pm := sdk.MessagePageMetadata{
// Offset: 0,
// Limit: 10,
// }
// msgs, _ := sdk.ReadMessages(ctx, pm,"channelID", "domainID", "token")
// fmt.Println(msgs)
ReadMessages(ctx context.Context, pm MessagePageMetadata, chanID, domainID, token string) (MessagesPage, errors.SDKError)
// CreateSubscription creates a new subscription
//
// example:
// subscription, _ := sdk.CreateSubscription(ctx, "topic", "contact", "token")
// fmt.Println(subscription)
CreateSubscription(ctx context.Context, topic, contact, token string) (string, errors.SDKError)
// ListSubscriptions list subscriptions given list parameters.
//
// example:
// pm := sdk.PageMetadata{
// Offset: 0,
// Limit: 10,
// }
// subscriptions, _ := sdk.ListSubscriptions(ctx, pm, "token")
// fmt.Println(subscriptions)
ListSubscriptions(ctx context.Context, pm PageMetadata, token string) (SubscriptionPage, errors.SDKError)
// ViewSubscription retrieves a subscription with the provided id.
//
// example:
// subscription, _ := sdk.ViewSubscription(ctx, "id", "token")
// fmt.Println(subscription)
ViewSubscription(ctx context.Context, id, token string) (Subscription, errors.SDKError)
// DeleteSubscription removes a subscription with the provided id.
//
// example:
// err := sdk.DeleteSubscription(ctx, "id", "token")
// fmt.Println(err)
DeleteSubscription(ctx context.Context, id, token string) errors.SDKError
}
type mgSDK struct {
bootstrapURL string
readersURL string
usersURL string
client *http.Client
curlFlag bool
msgContentType smqSDK.ContentType
smqSDK.SDK
}
// Config contains sdk configuration parameters.
type Config struct {
BootstrapURL string
CertsURL string
HTTPAdapterURL string
ReaderURL string
ClientsURL string
UsersURL string
GroupsURL string
ChannelsURL string
DomainsURL string
JournalURL string
HostURL string
MsgContentType smqSDK.ContentType
TLSVerification bool
CurlFlag bool
}
// NewSDK returns new supermq SDK instance.
func NewSDK(conf Config) SDK {
smqSDK := smqSDK.NewSDK(smqSDK.Config{
CertsURL: conf.CertsURL,
HTTPAdapterURL: conf.HTTPAdapterURL,
ClientsURL: conf.ClientsURL,
UsersURL: conf.UsersURL,
GroupsURL: conf.GroupsURL,
ChannelsURL: conf.ChannelsURL,
DomainsURL: conf.DomainsURL,
JournalURL: conf.JournalURL,
HostURL: conf.HostURL,
MsgContentType: conf.MsgContentType,
TLSVerification: conf.TLSVerification,
CurlFlag: conf.CurlFlag,
})
return &mgSDK{
bootstrapURL: conf.BootstrapURL,
readersURL: conf.ReaderURL,
usersURL: conf.UsersURL,
msgContentType: conf.MsgContentType,
client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: !conf.TLSVerification,
},
},
},
curlFlag: conf.CurlFlag,
SDK: smqSDK,
}
}
// processRequest creates and send a new HTTP request, and checks for errors in the HTTP response.
// It then returns the response headers, the response body, and the associated error(s) (if any).
func (sdk mgSDK) processRequest(ctx context.Context, method, reqUrl, token string, data []byte, headers map[string]string, expectedRespCodes ...int) (http.Header, []byte, errors.SDKError) {
req, err := http.NewRequestWithContext(ctx, method, reqUrl, bytes.NewReader(data))
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
// Sets a default value for the Content-Type.
// Overridden if Content-Type is passed in the headers arguments.
req.Header.Add("Content-Type", string(smqSDK.CTJSON))
for key, value := range headers {
req.Header.Add(key, value)
}
if token != "" {
if !strings.Contains(token, smqSDK.ClientPrefix) {
token = smqSDK.BearerPrefix + token
}
req.Header.Set("Authorization", token)
}
if sdk.curlFlag {
curlCommand, err := http2curl.GetCurlCommand(req)
if err != nil {
return nil, nil, errors.NewSDKError(err)
}
log.Println(curlCommand.String())
}
resp, err := sdk.client.Do(req)
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
defer resp.Body.Close()
sdkerr := errors.CheckError(resp, expectedRespCodes...)
if sdkerr != nil {
return make(http.Header), []byte{}, sdkerr
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return make(http.Header), []byte{}, errors.NewSDKError(err)
}
return resp.Header, body, nil
}
func (sdk mgSDK) withQueryParams(baseURL, endpoint string, pm PageMetadata) (string, error) {
q, err := pm.query()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%s?%s", baseURL, endpoint, q), nil
}
func (pm PageMetadata) query() (string, error) {
q := url.Values{}
if pm.Offset != 0 {
q.Add("offset", strconv.FormatUint(pm.Offset, 10))
}
if pm.Limit != 0 {
q.Add("limit", strconv.FormatUint(pm.Limit, 10))
}
if pm.Total != 0 {
q.Add("total", strconv.FormatUint(pm.Total, 10))
}
if pm.Metadata != nil {
md, err := json.Marshal(pm.Metadata)
if err != nil {
return "", errors.NewSDKError(err)
}
q.Add("metadata", string(md))
}
if pm.Topic != "" {
q.Add("topic", pm.Topic)
}
if pm.Contact != "" {
q.Add("contact", pm.Contact)
}
if pm.DomainID != "" {
q.Add("domain_id", pm.DomainID)
}
if pm.Level != 0 {
q.Add("level", strconv.FormatUint(pm.Level, 10))
}
return q.Encode(), nil
}