diff --git a/basic-sensor-receiver.code-workspace b/basic-sensor-receiver.code-workspace index 57ccc2b..b6fe676 100644 --- a/basic-sensor-receiver.code-workspace +++ b/basic-sensor-receiver.code-workspace @@ -9,6 +9,7 @@ ], "settings": { "cSpell.words": [ + "sqlx", "tgbotapi" ] } diff --git a/client/src/pages/alerts/components/AlertsTable/components/AlertFormModal.tsx b/client/src/pages/alerts/components/AlertsTable/components/AlertFormModal.tsx index 5030926..51b3560 100644 --- a/client/src/pages/alerts/components/AlertsTable/components/AlertFormModal.tsx +++ b/client/src/pages/alerts/components/AlertsTable/components/AlertFormModal.tsx @@ -31,8 +31,8 @@ export const AlertFormModal = ({ alert, open, onClose }: Props) => { triggerInterval: 0, ...(alert && { name: alert.name, - contactPointId: alert.contactPointId, customMessage: alert.customMessage, + contactPointId: alert.contactPointId, triggerInterval: alert.triggerInterval, }), ...(parsedCondition && { diff --git a/server/app/alerts.go b/server/app/alerts.go index 58dde79..dbcf4a3 100644 --- a/server/app/alerts.go +++ b/server/app/alerts.go @@ -6,11 +6,11 @@ import ( ) func (s *Server) StartAlerts() { - ticker := time.NewTicker(time.Second * 10) + ticker := time.NewTicker(time.Second * 1) go func() { for { - err := s.Services.Alerts.EvaluateAlerts() + err := s.Services.AlertsEvaluator.EvaluateAlerts() if err != nil { fmt.Println("Error evaluating alerts: ", err) } diff --git a/server/app/server.go b/server/app/server.go index 9af696b..35eb54d 100644 --- a/server/app/server.go +++ b/server/app/server.go @@ -4,11 +4,12 @@ import ( "basic-sensor-receiver/config" "basic-sensor-receiver/database" "basic-sensor-receiver/services" - "database/sql" + + "github.com/jmoiron/sqlx" ) type Server struct { - DB *sql.DB + DB *sqlx.DB Config *config.Config Services *services.Services } diff --git a/server/database/database.go b/server/database/database.go index 1b33faa..9a7f96d 100644 --- a/server/database/database.go +++ b/server/database/database.go @@ -1,13 +1,14 @@ package database import ( - "database/sql" "fmt" "time" + + "github.com/jmoiron/sqlx" ) -func Initialize(databaseUrl string) (*sql.DB, error) { - db, err := sql.Open("sqlite3", databaseUrl) +func Initialize(databaseUrl string) (*sqlx.DB, error) { + db, err := sqlx.Open("sqlite3", databaseUrl) if err != nil { return nil, fmt.Errorf("failed to open database connection: %w", err) diff --git a/server/database/migrations.go b/server/database/migrations.go index fab0c82..e1464a0 100644 --- a/server/database/migrations.go +++ b/server/database/migrations.go @@ -1,18 +1,19 @@ package database import ( - "database/sql" "embed" "fmt" "io/fs" "sort" "strings" + + "github.com/jmoiron/sqlx" ) //go:embed migrations/*.sql var migrationStorage embed.FS -func getAppliedMigrationsSet(db *sql.DB) (map[string]bool, error) { +func getAppliedMigrationsSet(db *sqlx.DB) (map[string]bool, error) { set := make(map[string]bool) rows, err := db.Query("SELECT id FROM migrations") @@ -56,10 +57,6 @@ func getMigrations() ([]string, error) { } } - if err != nil { - return nil, err - } - sort.Strings(items) return items, nil @@ -75,7 +72,7 @@ func getMigrationContent(id string) (string, error) { return string(content), nil } -func getPendingMigrations(db *sql.DB) ([]string, error) { +func getPendingMigrations(db *sqlx.DB) ([]string, error) { var migrationsToRun []string migrationSet, err := getAppliedMigrationsSet(db) diff --git a/server/go.mod b/server/go.mod index eeb3725..df34dac 100644 --- a/server/go.mod +++ b/server/go.mod @@ -17,6 +17,7 @@ require ( github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect github.com/goccy/go-json v0.9.10 // indirect github.com/golobby/cast v1.3.0 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/server/go.sum b/server/go.sum index 0de950a..5dfd436 100644 --- a/server/go.sum +++ b/server/go.sum @@ -14,6 +14,7 @@ github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM= @@ -27,6 +28,8 @@ github.com/golobby/dotenv v1.3.1 h1:BvQyNuOQITmIXNHpQ/FUG2gZcUGmcGMyODMeUfiKkeU= github.com/golobby/dotenv v1.3.1/go.mod h1:EWUdOzuDlA1g4hdjo++WD37DhNZw33Oce8ryH3liZTQ= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -39,8 +42,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLvBGw= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= diff --git a/server/integrations/integrations.go b/server/integrations/integrations.go index 7d0b75e..9db5585 100644 --- a/server/integrations/integrations.go +++ b/server/integrations/integrations.go @@ -6,6 +6,7 @@ type ContactPointMessageType string const ( ContactPointEventAlertTriggered = "alert_triggered" + ContactPointEventAlertResolved = "alert_resolved" ContactPointEventTest = "test" ) @@ -13,6 +14,7 @@ type ContactPointEvent struct { Type ContactPointMessageType AlertTriggeredEvent *ContactPointAlertTriggeredEvent + AlertResolvedEvent *ContactPointAlertResolvedEvent } type ContactPointAlertTriggeredEvent struct { @@ -23,6 +25,14 @@ type ContactPointAlertTriggeredEvent struct { LastValue float64 } +type ContactPointAlertResolvedEvent struct { + Alert *models.AlertItem + Sensor *models.SensorItem + SensorValueCondition *models.AlertConditionSensorValue + SensorLastContactCondition *models.AlertConditionSensorLastContact + LastValue float64 +} + type ContactPointIntegration interface { ProcessEvent(evt *ContactPointEvent, rawConfig string) error ValidateConfig(rawConfig string) error diff --git a/server/integrations/telegram.go b/server/integrations/telegram.go index 4c5dbc3..e66b49b 100644 --- a/server/integrations/telegram.go +++ b/server/integrations/telegram.go @@ -3,6 +3,7 @@ package integrations import ( "encoding/json" "fmt" + "log" "strconv" "strings" @@ -35,18 +36,71 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri case ContactPointEventAlertTriggered: data := evt.AlertTriggeredEvent - text := fmt.Sprintf("🚨 %s is at {value}", data.Sensor.Name) + if data.SensorValueCondition != nil { + text := fmt.Sprintf("🚨 %s is at {value}", data.Sensor.Name) - if data.Alert.CustomMessage != "" { - text = data.Alert.CustomMessage + if data.Alert.CustomMessage != "" { + text = data.Alert.CustomMessage + } + + text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1) + + msg := tgbotapi.NewMessage(config.TargetChannel, text) + msg.ParseMode = "Markdown" + _, err = bot.Send(msg) + return err } - text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1) + if data.SensorLastContactCondition != nil { + text := fmt.Sprintf("🚨 %s has not reported in for last %s %s", data.Sensor.Name, strconv.FormatFloat(data.SensorLastContactCondition.Value, 'f', -1, 64), data.SensorLastContactCondition.ValueUnit) + if data.Alert.CustomMessage != "" { + text = data.Alert.CustomMessage + } + + msg := tgbotapi.NewMessage(config.TargetChannel, text) + msg.ParseMode = "Markdown" + _, err = bot.Send(msg) + return err + } + + log.Println("No condition found for alert triggered event") + + return nil + + case ContactPointEventAlertResolved: + data := evt.AlertResolvedEvent + + if data.SensorValueCondition != nil { + text := fmt.Sprintf("✅ %s is at {value}", data.Sensor.Name) + + if data.Alert.CustomMessage != "" { + text = data.Alert.CustomMessage + } + + text = strings.Replace(text, "{value}", strconv.FormatFloat(data.LastValue, 'f', -1, 64), -1) + + msg := tgbotapi.NewMessage(config.TargetChannel, text) + msg.ParseMode = "Markdown" + _, err = bot.Send(msg) + return err + } + + if data.SensorLastContactCondition != nil { + text := fmt.Sprintf("✅ %s has reported in last %s %s", data.Sensor.Name, strconv.FormatFloat(data.SensorLastContactCondition.Value, 'f', -1, 64), data.SensorLastContactCondition.ValueUnit) + if data.Alert.CustomMessage != "" { + text = data.Alert.CustomMessage + } + + msg := tgbotapi.NewMessage(config.TargetChannel, text) + msg.ParseMode = "Markdown" + _, err = bot.Send(msg) + return err + } + + log.Println("No condition found for alert triggered event") + + return nil - msg := tgbotapi.NewMessage(config.TargetChannel, text) - msg.ParseMode = "Markdown" - _, err = bot.Send(msg) - return err case ContactPointEventTest: msg := tgbotapi.NewMessage(config.TargetChannel, "Test message from Basic Sensor Receiver") _, err = bot.Send(msg) diff --git a/server/models/alerts.go b/server/models/alerts.go index 2ac4fa0..da63303 100644 --- a/server/models/alerts.go +++ b/server/models/alerts.go @@ -1,19 +1,19 @@ package models type AlertItem struct { - Id int64 `json:"id"` - ContactPointId int64 `json:"contactPointId"` - Name string `json:"name"` - Condition string `json:"condition"` - CustomMessage string `json:"customMessage"` + Id int64 `json:"id" db:"id"` + ContactPointId int64 `json:"contactPointId" db:"contact_point_id"` + Name string `json:"name" db:"name"` + Condition string `json:"condition" db:"condition"` + CustomMessage string `json:"customMessage" db:"custom_message"` /* how long does the condition have to be true for the alert to go off */ - TriggerInterval int64 `json:"triggerInterval"` + TriggerInterval int64 `json:"triggerInterval" db:"trigger_interval"` /* current alert status, possible values: good, pending, alerting */ - LastStatus string `json:"lastStatus"` + LastStatus AlertStatus `json:"lastStatus" db:"last_status"` /* time at which was status last changed */ - LastStatusAt int64 `json:"lastStatusAt"` + LastStatusAt int64 `json:"lastStatusAt" db:"last_status_at"` } type AlertConditionSensorValue struct { @@ -27,3 +27,19 @@ type AlertConditionSensorLastContact struct { Value float64 `json:"value"` ValueUnit string `json:"valueUnit"` } + +type AlertConditionType string + +const ( + AlertConditionSensorValueType = "sensor_value" + AlertConditionSensorLastContactType = "sensor_last_contact" +) + +type AlertStatus string + +const ( + AlertStatusOk = "ok" + AlertStatusAlertPending = "alert_pending" + AlertStatusAlerting = "alerting" + AlertStatusOkPending = "ok_pending" +) diff --git a/server/services/alerts_evaluator_service.go b/server/services/alerts_evaluator_service.go new file mode 100644 index 0000000..06d9ccb --- /dev/null +++ b/server/services/alerts_evaluator_service.go @@ -0,0 +1,213 @@ +package services + +import ( + "basic-sensor-receiver/integrations" + "basic-sensor-receiver/models" + "database/sql" + "encoding/json" + "fmt" + "time" +) + +type AlertsEvaluatorService struct { + ctx *Context +} + +func evaluateSensorValueCondition(condition *models.AlertConditionSensorValue, value *sensorValue) bool { + switch condition.Condition { + case "less": + return value.Value < condition.Value + case "more": + return value.Value > condition.Value + case "equal": + return value.Value == condition.Value + } + + return false +} + +func (s *AlertsEvaluatorService) EvaluateAlerts() error { + alerts, err := s.ctx.Services.Alerts.GetList() + + if err != nil { + return err + } + + for _, alert := range alerts { + err := s.EvaluateAlert(&alert) + + if err != nil { + return err + } + } + + return nil + +} + +func unitToSeconds(unit string) float64 { + switch unit { + case "s": + return 1 + case "m": + return 60 + case "h": + return 3600 + case "d": + return 86400 + } + + return 0 +} + +func (s *AlertsEvaluatorService) EvaluateAlert(alert *models.AlertItem) error { + condition := map[string]interface{}{} + err := json.Unmarshal([]byte(alert.Condition), &condition) + if err != nil { + return err + } + + // TODO: This flow is a bit cumbersome, consider refactoring + newStatus := alert.LastStatus + lastValue := float64(0) + conditionMet := false + sensorId := int64(-1) + var sensorValueCondition *models.AlertConditionSensorValue + var sensorLastContactCondition *models.AlertConditionSensorLastContact + + switch condition["type"].(string) { + case models.AlertConditionSensorValueType: + { + sensorValueCondition = &models.AlertConditionSensorValue{ + SensorId: int64(condition["sensorId"].(float64)), + Condition: condition["condition"].(string), + Value: condition["value"].(float64), + } + + value, err := s.ctx.Services.SensorValues.GetLatest(sensorValueCondition.SensorId, time.Now().Unix()) + lastValue = value.Value + sensorId = int64(sensorValueCondition.SensorId) + + if err != nil { + if err == sql.ErrNoRows { + lastValue = float64(0) + conditionMet = false + } else { + return fmt.Errorf("error getting sensor value: %v", err) + } + } else { + conditionMet = evaluateSensorValueCondition(sensorValueCondition, value) + } + + break + } + + case models.AlertConditionSensorLastContactType: + { + sensorLastContactCondition = &models.AlertConditionSensorLastContact{ + SensorId: int64(condition["sensorId"].(float64)), + Value: condition["value"].(float64), + ValueUnit: condition["valueUnit"].(string), + } + + value, err := s.ctx.Services.SensorValues.GetLatest(sensorLastContactCondition.SensorId, time.Now().Unix()) + lastValue = float64(value.Timestamp) + sensorId = sensorLastContactCondition.SensorId + + if err != nil { + if err == sql.ErrNoRows { + lastValue = float64(0) + } else { + return fmt.Errorf("error getting sensor last contact value: %v", err) + } + } + + conditionInSec := int64(sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit)) + + conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec + + break + } + + } + + /* + Status flow: + ok -> alert_pending + alert_pending -> alerting OR alert_pending -> ok + alerting -> ok_pending + ok_pending -> ok OR ok_pending -> alerting + */ + if conditionMet { + if alert.LastStatus == models.AlertStatusOk { + newStatus = models.AlertStatusAlertPending + } else if alert.LastStatus == models.AlertStatusAlertPending { + if time.Now().Unix()-alert.LastStatusAt >= alert.TriggerInterval { + newStatus = models.AlertStatusAlerting + } + } + } else { + if alert.LastStatus == models.AlertStatusAlerting { + newStatus = models.AlertStatusOkPending + } else if alert.LastStatus == models.AlertStatusOkPending { + if time.Now().Unix()-alert.LastStatusAt >= alert.TriggerInterval { + newStatus = models.AlertStatusOk + } + } + } + + if newStatus != alert.LastStatus || newStatus == models.AlertStatusOk { + s.ctx.DB.Exec("UPDATE alerts SET last_status = ?, last_status_at = ? WHERE id = ?", newStatus, time.Now().Unix(), alert.Id) + + if newStatus == models.AlertStatusAlerting || newStatus == models.AlertStatusOk { + sensor, err := s.ctx.Services.Sensors.GetById(sensorId) + if err != nil { + return fmt.Errorf("error getting sensor detail (%d): %v", sensorId, err) + } + + contactPoint, err := s.ctx.Services.ContactPoints.GetById(alert.ContactPointId) + if err != nil { + return fmt.Errorf("error getting contact point detail: %v", err) + } + + dispatchService, err := contactPoint.getService(s.ctx) + if err != nil { + return fmt.Errorf("error getting dispatch service: %v", err) + } + + // Dispatch alert resolved event when we are transitioning ok_pending -> ok + if newStatus == models.AlertStatusOk && alert.LastStatus == models.AlertStatusOkPending { + err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{ + Type: integrations.ContactPointEventAlertResolved, + AlertResolvedEvent: &integrations.ContactPointAlertResolvedEvent{ + Alert: alert, + Sensor: sensor, + SensorValueCondition: sensorValueCondition, + SensorLastContactCondition: sensorLastContactCondition, + LastValue: lastValue, + }, + }, contactPoint.TypeConfig) + } + + // Dispatch alert triggered event when we are transitioning alert_pending -> alerting + if newStatus == models.AlertStatusAlerting && alert.LastStatus == models.AlertStatusAlertPending { + err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{ + Type: integrations.ContactPointEventAlertTriggered, + AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{ + Alert: alert, + Sensor: sensor, + SensorValueCondition: sensorValueCondition, + SensorLastContactCondition: sensorLastContactCondition, + LastValue: lastValue, + }, + }, contactPoint.TypeConfig) + } + + if err != nil { + return fmt.Errorf("error dispatching alert: %v", err) + } + } + } + + return nil +} diff --git a/server/services/alerts_service.go b/server/services/alerts_service.go index 31f07b8..a9d2b21 100644 --- a/server/services/alerts_service.go +++ b/server/services/alerts_service.go @@ -1,9 +1,7 @@ package services import ( - "basic-sensor-receiver/integrations" "basic-sensor-receiver/models" - "encoding/json" "time" ) @@ -11,194 +9,15 @@ type AlertsService struct { ctx *Context } -func evaluateSensorValueCondition(condition *models.AlertConditionSensorValue, value *sensorValue) bool { - switch condition.Condition { - case "less": - return value.Value < condition.Value - case "more": - return value.Value > condition.Value - case "equal": - return value.Value == condition.Value - } +func (s *AlertsService) GetList() ([]models.AlertItem, error) { + alerts := []models.AlertItem{} - return false -} + err := s.ctx.DB.Select(&alerts, ` + SELECT id, contact_point_id, name, custom_message, condition, trigger_interval, last_status, last_status_at + FROM alerts + ORDER BY name ASC + `) -func (s *AlertsService) EvaluateAlerts() error { - alerts, err := s.GetList() - - if err != nil { - return err - } - - for _, alert := range alerts { - err := s.EvaluateAlert(alert) - - if err != nil { - return err - } - } - - return nil - -} - -func unitToSeconds(unit string) float64 { - switch unit { - case "s": - return 1 - case "m": - return 60 - case "h": - return 3600 - case "d": - return 86400 - } - - return 0 -} - -func (s *AlertsService) EvaluateAlert(alert *models.AlertItem) error { - condition := map[string]interface{}{} - err := json.Unmarshal([]byte(alert.Condition), &condition) - if err != nil { - return err - } - - newStatus := alert.LastStatus - lastValue := float64(0) - conditionMet := false - sensorId := int64(-1) - sensorValueCondition := models.AlertConditionSensorValue{} - sensorLastContactCondition := models.AlertConditionSensorLastContact{} - - switch condition["type"].(string) { - case "sensor_value": - { - sensorValueCondition = models.AlertConditionSensorValue{ - SensorId: int64(condition["sensorId"].(float64)), - Condition: condition["condition"].(string), - Value: condition["value"].(float64), - } - - value, err := s.ctx.Services.SensorValues.GetLatest(sensorValueCondition.SensorId, time.Now().Unix()) - lastValue = value.Value - sensorId = int64(sensorValueCondition.SensorId) - - if err != nil { - return err - } - - conditionMet = evaluateSensorValueCondition(&sensorValueCondition, value) - - break - } - - case "sensor_last_contact": - { - sensorLastContactCondition := models.AlertConditionSensorLastContact{ - SensorId: int64(condition["sensorId"].(float64)), - Value: condition["value"].(float64), - ValueUnit: condition["valueUnit"].(string), - } - - value, err := s.ctx.Services.SensorValues.GetLatest(sensorLastContactCondition.SensorId, time.Now().Unix()) - lastValue = float64(value.Timestamp) - - if err != nil { - return err - } - - conditionInSec := int64(sensorLastContactCondition.Value * unitToSeconds(sensorLastContactCondition.ValueUnit)) - - conditionMet = time.Now().Unix()-value.Timestamp > conditionInSec - - break - } - - } - - if conditionMet { - if alert.LastStatus == "good" { - newStatus = "pending" - } else if alert.LastStatus == "pending" { - if time.Now().Unix()-alert.LastStatusAt >= alert.TriggerInterval { - newStatus = "alerting" - } - } - } else { - if alert.LastStatus == "alerting" { - newStatus = "pending" - } else if alert.LastStatus == "pending" { - if time.Now().Unix()-alert.LastStatusAt >= alert.TriggerInterval { - newStatus = "good" - } - } - } - - if newStatus != alert.LastStatus { - s.ctx.DB.Exec("UPDATE alerts SET last_status = ?, last_status_at = ? WHERE id = ?", newStatus, time.Now().Unix(), alert.Id) - - if newStatus == "alerting" { - sensor, err := s.ctx.Services.Sensors.GetById(sensorId) - if err != nil { - return err - } - - contactPoint, err := s.ctx.Services.ContactPoints.GetById(alert.ContactPointId) - if err != nil { - return err - } - - dispatchService, err := contactPoint.getService(s.ctx) - if err != nil { - return err - } - - err = dispatchService.ProcessEvent(&integrations.ContactPointEvent{ - Type: integrations.ContactPointEventAlertTriggered, - AlertTriggeredEvent: &integrations.ContactPointAlertTriggeredEvent{ - Alert: alert, - Sensor: sensor, - SensorValueCondition: &sensorValueCondition, - SensorLastContactCondition: &sensorLastContactCondition, - LastValue: lastValue, - }, - }, contactPoint.TypeConfig) - - if err != nil { - return err - } - } - } - - return nil -} - -func (s *AlertsService) GetList() ([]*models.AlertItem, error) { - rows, err := s.ctx.DB.Query("SELECT id, contact_point_id, name, condition, trigger_interval, last_status, last_status_at FROM alerts ORDER BY name ASC") - - if err != nil { - return nil, err - } - - defer rows.Close() - - alerts := []*models.AlertItem{} - - for rows.Next() { - alert := models.AlertItem{} - - err := rows.Scan(&alert.Id, &alert.ContactPointId, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt) - - if err != nil { - return nil, err - } - - alerts = append(alerts, &alert) - } - - err = rows.Err() if err != nil { return nil, err } @@ -209,8 +28,14 @@ func (s *AlertsService) GetList() ([]*models.AlertItem, error) { func (s *AlertsService) GetById(id int64) (*models.AlertItem, error) { alert := models.AlertItem{} - row := s.ctx.DB.QueryRow("SELECT id, contact_point_id, name, condition, trigger_interval, last_status, last_status_at FROM alerts WHERE id = ?", id) - err := row.Scan(&alert.Id, &alert.ContactPointId, &alert.Name, &alert.Condition, &alert.TriggerInterval, &alert.LastStatus, &alert.LastStatusAt) + err := s.ctx.DB.Get(&alert, + ` + SELECT id, contact_point_id, name, custom_message, condition, trigger_interval, last_status, last_status_at + FROM alerts + WHERE id = $1 + `, + id, + ) if err != nil { return nil, err @@ -226,13 +51,17 @@ func (s *AlertsService) Create(contactPointId int64, name string, condition stri Condition: condition, TriggerInterval: triggerInterval, CustomMessage: customMessage, - LastStatus: "good", - LastStatusAt: 0, + LastStatus: models.AlertStatusOk, + LastStatusAt: time.Now().Unix(), } - res, err := s.ctx.DB.Exec( - "INSERT INTO alerts (contact_point_id, name, condition, trigger_interval, last_status, last_status_at, custom_message) VALUES (?, ?, ?, ?, ?, ?, ?)", - alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.LastStatus, alert.LastStatusAt, alert.CustomMessage, + res, err := s.ctx.DB.NamedExec( + ` + INSERT INTO alerts + (contact_point_id, name, condition, trigger_interval, last_status, last_status_at, custom_message) VALUES + (:contact_point_id, :name, :condition, :trigger_interval, :last_status, :last_status_at, :custom_message) + `, + alert, ) if err != nil { @@ -248,6 +77,37 @@ func (s *AlertsService) Create(contactPointId int64, name string, condition stri return &alert, nil } +func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64, customMessage string) (*models.AlertItem, error) { + alert := models.AlertItem{ + Id: id, + ContactPointId: contactPointId, + Name: name, + Condition: condition, + TriggerInterval: triggerInterval, + CustomMessage: customMessage, + } + + _, err := s.ctx.DB.NamedExec( + ` + UPDATE alerts + SET + name = :name, + contact_point_id = :contact_point_id, + condition = :condition, + trigger_interval = :trigger_interval, + custom_message = :custom_message + WHERE id = :id + `, + alert, + ) + + if err != nil { + return nil, err + } + + return &alert, nil +} + func (s *AlertsService) DeleteById(id int64) error { _, err := s.ctx.DB.Exec("DELETE FROM alerts WHERE id = ?", id) @@ -257,25 +117,3 @@ func (s *AlertsService) DeleteById(id int64) error { return nil } - -func (s *AlertsService) Update(id int64, contactPointId int64, name string, condition string, triggerInterval int64, customMessage string) (*models.AlertItem, error) { - alert := models.AlertItem{ - Id: id, - ContactPointId: contactPointId, - Name: name, - Condition: condition, - TriggerInterval: triggerInterval, - CustomMessage: customMessage, - } - - _, err := s.ctx.DB.Exec( - "UPDATE alerts SET contact_point_id = ?, name = ?, condition = ?, trigger_interval = ?, custom_message = ? WHERE id = ?", - alert.ContactPointId, alert.Name, alert.Condition, alert.TriggerInterval, alert.CustomMessage, alert.Id, - ) - - if err != nil { - return nil, err - } - - return &alert, nil -} diff --git a/server/services/services.go b/server/services/services.go index 07060b8..1b805ef 100644 --- a/server/services/services.go +++ b/server/services/services.go @@ -3,18 +3,20 @@ package services import ( "basic-sensor-receiver/config" "basic-sensor-receiver/integrations" - "database/sql" + + "github.com/jmoiron/sqlx" ) type Services struct { - SensorConfig *SensorConfigService - SensorValues *SensorValuesService - Sensors *SensorsService - Sessions *SessionsService - Auth *AuthService - Dashboards *DashboardsService - Alerts *AlertsService - ContactPoints *ContactPointsService + SensorConfig *SensorConfigService + SensorValues *SensorValuesService + Sensors *SensorsService + Sessions *SessionsService + Auth *AuthService + Dashboards *DashboardsService + Alerts *AlertsService + AlertsEvaluator *AlertsEvaluatorService + ContactPoints *ContactPointsService } type Integrations struct { @@ -22,7 +24,7 @@ type Integrations struct { } type Context struct { - DB *sql.DB + DB *sqlx.DB Config *config.Config Services *Services Integrations *Integrations @@ -40,6 +42,7 @@ func InitializeServices(ctx *Context) *Services { services.Auth = &AuthService{ctx: ctx} services.Dashboards = &DashboardsService{ctx: ctx} services.Alerts = &AlertsService{ctx: ctx} + services.AlertsEvaluator = &AlertsEvaluatorService{ctx: ctx} services.ContactPoints = &ContactPointsService{ctx: ctx} ctx.Integrations = &Integrations{}