Compare commits
2 commits
657c00e326
...
70a5dd4509
Author | SHA1 | Date | |
---|---|---|---|
70a5dd4509 | |||
9e4144545a |
10 changed files with 95 additions and 79 deletions
36
pkg/alerting/rules/alertrules.go
Normal file
36
pkg/alerting/rules/alertrules.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/internal/ruletime"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlertRules []AlertRule
|
||||||
|
|
||||||
|
func (ars *AlertRules) Apply(t time.Time, coversOpts ...ruletime.CoversOption) float64 {
|
||||||
|
final := 1.0
|
||||||
|
|
||||||
|
for _, ar := range *ars {
|
||||||
|
if ar.MatchTime(t, coversOpts...) {
|
||||||
|
final *= float64(ar.ScoreMultiplier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertRule struct {
|
||||||
|
Times []ruletime.RuleTime `json:"times"`
|
||||||
|
ScoreMultiplier float32 `json:"mult"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *AlertRule) MatchTime(t time.Time, coversOpts ...ruletime.CoversOption) bool {
|
||||||
|
for _, at := range ar.Times {
|
||||||
|
if at.Covers(t, coversOpts...) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package talkgroups_test
|
package rules_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -8,6 +9,7 @@ import (
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/ruletime"
|
"dynatron.me/x/stillbox/internal/ruletime"
|
||||||
"dynatron.me/x/stillbox/internal/trending"
|
"dynatron.me/x/stillbox/internal/trending"
|
||||||
|
"dynatron.me/x/stillbox/pkg/alerting/rules"
|
||||||
"dynatron.me/x/stillbox/pkg/talkgroups"
|
"dynatron.me/x/stillbox/pkg/talkgroups"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -20,14 +22,14 @@ func TestAlertConfig(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
tg talkgroups.ID
|
tg talkgroups.ID
|
||||||
conf string
|
conf string
|
||||||
compare []talkgroups.AlertRule
|
compare rules.AlertRules
|
||||||
expectErr error
|
expectErr error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "base case",
|
name: "base case",
|
||||||
tg: talkgroups.TG(197, 3),
|
tg: talkgroups.TG(197, 3),
|
||||||
conf: `[{"times":["7:00+2h","01:00+1h","16:00+1h","19:00+4h"],"mult":0.2},{"times":["11:00+1h","15:00+30m","16:03+20m"],"mult":2.0}]`,
|
conf: `[{"times":["7:00+2h","01:00+1h","16:00+1h","19:00+4h"],"mult":0.2},{"times":["11:00+1h","15:00+30m","16:03+20m"],"mult":2.0}]`,
|
||||||
compare: []talkgroups.AlertRule{
|
compare: rules.AlertRules{
|
||||||
{
|
{
|
||||||
Times: []ruletime.RuleTime{
|
Times: []ruletime.RuleTime{
|
||||||
ruletime.Must(ruletime.New("7:00+2h")),
|
ruletime.Must(ruletime.New("7:00+2h")),
|
||||||
|
@ -57,11 +59,13 @@ func TestAlertConfig(t *testing.T) {
|
||||||
|
|
||||||
for _, tc := range parseTests {
|
for _, tc := range parseTests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
err := ac.UnmarshalTGRules(tc.tg, []byte(tc.conf))
|
var ar rules.AlertRules
|
||||||
|
err := json.Unmarshal([]byte(tc.conf), &ar)
|
||||||
if tc.expectErr != nil {
|
if tc.expectErr != nil {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), tc.expectErr.Error())
|
assert.Contains(t, err.Error(), tc.expectErr.Error())
|
||||||
} else {
|
} else {
|
||||||
|
ac.Add(tc.tg, ar)
|
||||||
assert.Equal(t, tc.compare, ac.GetRules(tc.tg))
|
assert.Equal(t, tc.compare, ac.GetRules(tc.tg))
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -7,6 +7,7 @@ package database
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/pkg/alerting/rules"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
@ -92,7 +93,7 @@ type Talkgroup struct {
|
||||||
Metadata []byte `json:"metadata"`
|
Metadata []byte `json:"metadata"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Alert bool `json:"alert"`
|
Alert bool `json:"alert"`
|
||||||
AlertConfig []byte `json:"alert_config"`
|
AlertConfig rules.AlertRules `json:"alert_config"`
|
||||||
Weight float32 `json:"weight"`
|
Weight float32 `json:"weight"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"dynatron.me/x/stillbox/pkg/alerting/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
const bulkSetTalkgroupTags = `-- name: BulkSetTalkgroupTags :exec
|
const bulkSetTalkgroupTags = `-- name: BulkSetTalkgroupTags :exec
|
||||||
|
@ -497,7 +499,7 @@ type UpdateTalkgroupParams struct {
|
||||||
Metadata []byte `json:"metadata"`
|
Metadata []byte `json:"metadata"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
Alert *bool `json:"alert"`
|
Alert *bool `json:"alert"`
|
||||||
AlertConfig []byte `json:"alert_config"`
|
AlertConfig rules.AlertRules `json:"alert_config"`
|
||||||
Weight *float32 `json:"weight"`
|
Weight *float32 `json:"weight"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
const getTalkgroupWithLearnedByPackedIDsTest = `-- name: GetTalkgroupWithLearnedByPackedIDs :many
|
const getTalkgroupsWithLearnedByPackedIDsTest = `-- name: GetTalkgroupsWithLearnedByPackedIDs :many
|
||||||
SELECT
|
SELECT
|
||||||
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
|
tg.id, tg.system_id, tg.tgid, tg.name, tg.alpha_tag, tg.tg_group, tg.frequency, tg.metadata, tg.tags, tg.alert, tg.alert_config, tg.weight, sys.id, sys.name,
|
||||||
FALSE learned
|
FALSE learned
|
||||||
|
@ -59,7 +59,7 @@ TRUE, NULL::JSONB, 1.0, sys.id, sys.name,
|
||||||
TRUE learned
|
TRUE learned
|
||||||
FROM talkgroups_learned tgl
|
FROM talkgroups_learned tgl
|
||||||
JOIN systems sys ON tgl.system_id = sys.id
|
JOIN systems sys ON tgl.system_id = sys.id
|
||||||
WHERE tg.system_id = $1 AND ignored IS NOT TRUE
|
WHERE tgl.system_id = $1 AND ignored IS NOT TRUE
|
||||||
`
|
`
|
||||||
|
|
||||||
const getTalkgroupsWithLearnedTest = `-- name: GetTalkgroupsWithLearned :many
|
const getTalkgroupsWithLearnedTest = `-- name: GetTalkgroupsWithLearned :many
|
||||||
|
@ -81,7 +81,7 @@ WHERE ignored IS NOT TRUE
|
||||||
`
|
`
|
||||||
|
|
||||||
func TestQueryColumnsMatch(t *testing.T) {
|
func TestQueryColumnsMatch(t *testing.T) {
|
||||||
require.Equal(t, getTalkgroupsWithLearnedByPackedIDsTest, getTalkgroupWithLearnedByPackedIDs)
|
require.Equal(t, getTalkgroupsWithLearnedByPackedIDsTest, getTalkgroupsWithLearnedByPackedIDs)
|
||||||
require.Equal(t, getTalkgroupWithLearnedTest, getTalkgroupWithLearned)
|
require.Equal(t, getTalkgroupWithLearnedTest, getTalkgroupWithLearned)
|
||||||
require.Equal(t, getTalkgroupsWithLearnedBySystemTest, getTalkgroupsWithLearnedBySystem)
|
require.Equal(t, getTalkgroupsWithLearnedBySystemTest, getTalkgroupsWithLearnedBySystem)
|
||||||
require.Equal(t, getTalkgroupsWithLearnedTest, getTalkgroupsWithLearned)
|
require.Equal(t, getTalkgroupsWithLearnedTest, getTalkgroupsWithLearned)
|
||||||
|
|
|
@ -43,7 +43,6 @@ type errResponse struct {
|
||||||
func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error {
|
||||||
switch e.Code {
|
switch e.Code {
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
case http.StatusBadRequest:
|
|
||||||
default:
|
default:
|
||||||
log.Error().Str("path", r.URL.Path).Err(e.Err).Int("code", e.Code).Str("msg", e.Error).Msg("request failed")
|
log.Error().Str("path", r.URL.Path).Err(e.Err).Int("code", e.Code).Str("msg", e.Error).Msg("request failed")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +1,39 @@
|
||||||
package talkgroups
|
package talkgroups
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"dynatron.me/x/stillbox/internal/ruletime"
|
"dynatron.me/x/stillbox/internal/ruletime"
|
||||||
|
"dynatron.me/x/stillbox/pkg/alerting/rules"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertConfig struct {
|
type AlertConfig struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
m map[ID][]AlertRule
|
m map[ID]rules.AlertRules
|
||||||
}
|
|
||||||
|
|
||||||
type AlertRule struct {
|
|
||||||
Times []ruletime.RuleTime `json:"times"`
|
|
||||||
ScoreMultiplier float32 `json:"mult"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAlertConfig() AlertConfig {
|
func NewAlertConfig() AlertConfig {
|
||||||
return AlertConfig{
|
return AlertConfig{
|
||||||
m: make(map[ID][]AlertRule),
|
m: make(map[ID]rules.AlertRules),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AlertConfig) GetRules(tg ID) []AlertRule {
|
func (ac *AlertConfig) Add(tg ID, r rules.AlertRules) error {
|
||||||
|
ac.Lock()
|
||||||
|
defer ac.Unlock()
|
||||||
|
|
||||||
|
ac.m[tg] = r
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac *AlertConfig) GetRules(tg ID) rules.AlertRules {
|
||||||
ac.RLock()
|
ac.RLock()
|
||||||
defer ac.RUnlock()
|
defer ac.RUnlock()
|
||||||
|
|
||||||
return ac.m[tg]
|
return ac.m[tg]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AlertConfig) UnmarshalTGRules(tg ID, confBytes []byte) error {
|
|
||||||
ac.Lock()
|
|
||||||
defer ac.Unlock()
|
|
||||||
|
|
||||||
if len(confBytes) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var rules []AlertRule
|
|
||||||
err := json.Unmarshal(confBytes, &rules)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ac.m[tg] = rules
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac *AlertConfig) ApplyAlertRules(id ID, t time.Time, coversOpts ...ruletime.CoversOption) float64 {
|
func (ac *AlertConfig) ApplyAlertRules(id ID, t time.Time, coversOpts ...ruletime.CoversOption) float64 {
|
||||||
ac.RLock()
|
ac.RLock()
|
||||||
s, has := ac.m[id]
|
s, has := ac.m[id]
|
||||||
|
@ -57,15 +42,7 @@ func (ac *AlertConfig) ApplyAlertRules(id ID, t time.Time, coversOpts ...ruletim
|
||||||
return 1.0
|
return 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
final := 1.0
|
return s.Apply(t, coversOpts...)
|
||||||
|
|
||||||
for _, ar := range s {
|
|
||||||
if ar.MatchTime(t, coversOpts...) {
|
|
||||||
final *= float64(ar.ScoreMultiplier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return final
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac *AlertConfig) Invalidate() {
|
func (ac *AlertConfig) Invalidate() {
|
||||||
|
@ -74,13 +51,3 @@ func (ac *AlertConfig) Invalidate() {
|
||||||
|
|
||||||
clear(ac.m)
|
clear(ac.m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ar *AlertRule) MatchTime(t time.Time, coversOpts ...ruletime.CoversOption) bool {
|
|
||||||
for _, at := range ar.Times {
|
|
||||||
if at.Covers(t, coversOpts...) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
|
@ -139,7 +139,9 @@ func (t *cache) add(rec *Talkgroup) error {
|
||||||
t.tgs[tg] = rec
|
t.tgs[tg] = rec
|
||||||
t.systems[int32(rec.System.ID)] = rec.System.Name
|
t.systems[int32(rec.System.ID)] = rec.System.Name
|
||||||
|
|
||||||
return t.AlertConfig.UnmarshalTGRules(tg, rec.Talkgroup.AlertConfig)
|
t.AlertConfig.Add(tg, rec.AlertConfig)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type row interface {
|
type row interface {
|
||||||
|
|
|
@ -27,3 +27,8 @@ sql:
|
||||||
go_type: "time.Time"
|
go_type: "time.Time"
|
||||||
- db_type: "pg_catalog.text"
|
- db_type: "pg_catalog.text"
|
||||||
go_type: "string"
|
go_type: "string"
|
||||||
|
- column: "talkgroups.alert_config"
|
||||||
|
go_type:
|
||||||
|
import: "dynatron.me/x/stillbox/pkg/alerting/rules"
|
||||||
|
type: "AlertRules"
|
||||||
|
nullable: true
|
||||||
|
|
Loading…
Reference in a new issue