Better alert status flow, add resolved message

This commit is contained in:
Jan Zípek 2024-03-31 20:04:48 +02:00
parent ed71f7fb13
commit 4c19e89c1c
Signed by: kamen
GPG Key ID: A17882625B33AC31
14 changed files with 398 additions and 258 deletions

View File

@ -9,6 +9,7 @@
],
"settings": {
"cSpell.words": [
"sqlx",
"tgbotapi"
]
}

View File

@ -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 && {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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=

View File

@ -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

View File

@ -3,6 +3,7 @@ package integrations
import (
"encoding/json"
"fmt"
"log"
"strconv"
"strings"
@ -35,6 +36,7 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri
case ContactPointEventAlertTriggered:
data := evt.AlertTriggeredEvent
if data.SensorValueCondition != nil {
text := fmt.Sprintf("🚨 %s is at {value}", data.Sensor.Name)
if data.Alert.CustomMessage != "" {
@ -47,6 +49,58 @@ func (s TelegramIntegration) ProcessEvent(evt *ContactPointEvent, rawConfig stri
msg.ParseMode = "Markdown"
_, err = bot.Send(msg)
return err
}
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
case ContactPointEventTest:
msg := tgbotapi.NewMessage(config.TargetChannel, "Test message from Basic Sensor Receiver")
_, err = bot.Send(msg)

View File

@ -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"
)

View File

@ -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
}

View File

@ -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
}

View File

@ -3,7 +3,8 @@ package services
import (
"basic-sensor-receiver/config"
"basic-sensor-receiver/integrations"
"database/sql"
"github.com/jmoiron/sqlx"
)
type Services struct {
@ -14,6 +15,7 @@ type Services struct {
Auth *AuthService
Dashboards *DashboardsService
Alerts *AlertsService
AlertsEvaluator *AlertsEvaluatorService
ContactPoints *ContactPointsService
}
@ -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{}