| @@ -5,6 +5,7 @@ import ( | |||||
| "encoding/json" | "encoding/json" | ||||
| "log/slog" | "log/slog" | ||||
| "sync" | "sync" | ||||
| "time" | |||||
| "github.com/AFASystems/presence/internal/pkg/bridge" | "github.com/AFASystems/presence/internal/pkg/bridge" | ||||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | "github.com/AFASystems/presence/internal/pkg/common/appcontext" | ||||
| @@ -13,6 +14,7 @@ import ( | |||||
| "github.com/AFASystems/presence/internal/pkg/logger" | "github.com/AFASystems/presence/internal/pkg/logger" | ||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| mqtt "github.com/eclipse/paho.mqtt.golang" | mqtt "github.com/eclipse/paho.mqtt.golang" | ||||
| "github.com/segmentio/kafka-go" | |||||
| ) | ) | ||||
| // BridgeApp holds dependencies for the bridge service (MQTT <-> Kafka). | // BridgeApp holds dependencies for the bridge service (MQTT <-> Kafka). | ||||
| @@ -71,10 +73,26 @@ func (a *BridgeApp) Run(ctx context.Context) { | |||||
| go kafkaclient.Consume(a.KafkaManager.GetReader("alert"), a.ChAlert, ctx, &a.wg) | go kafkaclient.Consume(a.KafkaManager.GetReader("alert"), a.ChAlert, ctx, &a.wg) | ||||
| go kafkaclient.Consume(a.KafkaManager.GetReader("mqtt"), a.ChMqtt, ctx, &a.wg) | go kafkaclient.Consume(a.KafkaManager.GetReader("mqtt"), a.ChMqtt, ctx, &a.wg) | ||||
| healthTicker := time.NewTicker(config.LARGE_TICKER_INTERVAL) | |||||
| defer healthTicker.Stop() | |||||
| for { | for { | ||||
| select { | select { | ||||
| case <-ctx.Done(): | case <-ctx.Done(): | ||||
| return | return | ||||
| case <-healthTicker.C: | |||||
| health, err := a.AppState.GetBridgeHealth(a.KafkaManager) | |||||
| if err != nil { | |||||
| slog.Error("getting bridge health", "err", err) | |||||
| continue | |||||
| } | |||||
| m := kafka.Message{ | |||||
| Value: health, | |||||
| } | |||||
| if err := kafkaclient.Write(ctx, a.KafkaManager.GetWriter("healthbridge"), m); err != nil { | |||||
| slog.Error("writing bridge health", "err", err) | |||||
| continue | |||||
| } | |||||
| case msg := <-a.ChApi: | case msg := <-a.ChApi: | ||||
| switch msg.Method { | switch msg.Method { | ||||
| case "POST": | case "POST": | ||||
| @@ -4,6 +4,7 @@ import ( | |||||
| "context" | "context" | ||||
| "log/slog" | "log/slog" | ||||
| "sync" | "sync" | ||||
| "time" | |||||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | "github.com/AFASystems/presence/internal/pkg/common/appcontext" | ||||
| "github.com/AFASystems/presence/internal/pkg/config" | "github.com/AFASystems/presence/internal/pkg/config" | ||||
| @@ -11,6 +12,7 @@ import ( | |||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | "github.com/AFASystems/presence/internal/pkg/kafkaclient" | ||||
| "github.com/AFASystems/presence/internal/pkg/logger" | "github.com/AFASystems/presence/internal/pkg/logger" | ||||
| "github.com/AFASystems/presence/internal/pkg/model" | "github.com/AFASystems/presence/internal/pkg/model" | ||||
| "github.com/segmentio/kafka-go" | |||||
| ) | ) | ||||
| // DecoderApp holds dependencies for the decoder service. | // DecoderApp holds dependencies for the decoder service. | ||||
| @@ -60,10 +62,27 @@ func (a *DecoderApp) Run(ctx context.Context) { | |||||
| go kafkaclient.Consume(a.KafkaManager.GetReader("rawbeacons"), a.ChRaw, ctx, &a.wg) | go kafkaclient.Consume(a.KafkaManager.GetReader("rawbeacons"), a.ChRaw, ctx, &a.wg) | ||||
| go kafkaclient.Consume(a.KafkaManager.GetReader("parser"), a.ChParser, ctx, &a.wg) | go kafkaclient.Consume(a.KafkaManager.GetReader("parser"), a.ChParser, ctx, &a.wg) | ||||
| healthTicker := time.NewTicker(config.LARGE_TICKER_INTERVAL) | |||||
| defer healthTicker.Stop() | |||||
| for { | for { | ||||
| select { | select { | ||||
| case <-ctx.Done(): | case <-ctx.Done(): | ||||
| return | return | ||||
| case <-healthTicker.C: | |||||
| health, err := a.AppState.GetDecoderHealth(a.KafkaManager) | |||||
| slog.Info("decoder health", "health", string(health)) | |||||
| if err != nil { | |||||
| slog.Error("getting decoder health", "err", err) | |||||
| continue | |||||
| } | |||||
| m := kafka.Message{ | |||||
| Value: health, | |||||
| } | |||||
| if err := kafkaclient.Write(ctx, a.KafkaManager.GetWriter("healthdecoder"), m); err != nil { | |||||
| slog.Error("writing decoder health", "err", err) | |||||
| continue | |||||
| } | |||||
| case msg := <-a.ChRaw: | case msg := <-a.ChRaw: | ||||
| decoder.ProcessIncoming(msg, a.AppState, a.KafkaManager.GetWriter("alertbeacons"), a.ParserRegistry) | decoder.ProcessIncoming(msg, a.AppState, a.KafkaManager.GetWriter("alertbeacons"), a.ParserRegistry) | ||||
| case msg := <-a.ChParser: | case msg := <-a.ChParser: | ||||
| @@ -62,10 +62,26 @@ func (a *LocationApp) Run(ctx context.Context) { | |||||
| locTicker := time.NewTicker(config.LARGE_TICKER_INTERVAL) | locTicker := time.NewTicker(config.LARGE_TICKER_INTERVAL) | ||||
| defer locTicker.Stop() | defer locTicker.Stop() | ||||
| healthTicker := time.NewTicker(config.LARGE_TICKER_INTERVAL) | |||||
| defer healthTicker.Stop() | |||||
| for { | for { | ||||
| select { | select { | ||||
| case <-ctx.Done(): | case <-ctx.Done(): | ||||
| return | return | ||||
| case <-healthTicker.C: | |||||
| health, err := a.AppState.GetLocationHealth(a.KafkaManager) | |||||
| if err != nil { | |||||
| slog.Error("getting location health", "err", err) | |||||
| continue | |||||
| } | |||||
| m := kafka.Message{ | |||||
| Value: health, | |||||
| } | |||||
| if err := kafkaclient.Write(ctx, a.KafkaManager.GetWriter("healthlocation"), m); err != nil { | |||||
| slog.Error("writing location health", "err", err) | |||||
| continue | |||||
| } | |||||
| case <-locTicker.C: | case <-locTicker.C: | ||||
| settings := a.AppState.GetSettings() | settings := a.AppState.GetSettings() | ||||
| slog.Info("current algorithm", "algorithm", settings.CurrentAlgorithm) | slog.Info("current algorithm", "algorithm", settings.CurrentAlgorithm) | ||||
| @@ -22,6 +22,12 @@ func RunEventLoop(ctx context.Context, a *ServerApp) { | |||||
| select { | select { | ||||
| case <-ctx.Done(): | case <-ctx.Done(): | ||||
| return | return | ||||
| case msg := <-a.ChHealthLocation: | |||||
| slog.Info("location health", "health", msg) | |||||
| case msg := <-a.ChHealthDecoder: | |||||
| slog.Info("decoder health", "health", msg) | |||||
| case msg := <-a.ChHealthBridge: | |||||
| slog.Info("bridge health", "health", msg) | |||||
| case msg := <-a.ChLoc: | case msg := <-a.ChLoc: | ||||
| switch msg.Method { | switch msg.Method { | ||||
| case "Standard": | case "Standard": | ||||
| @@ -4,7 +4,6 @@ import ( | |||||
| "fmt" | "fmt" | ||||
| "log/slog" | "log/slog" | ||||
| "os" | "os" | ||||
| "sync" | |||||
| "time" | "time" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | "github.com/AFASystems/presence/internal/pkg/kafkaclient" | ||||
| @@ -13,14 +12,12 @@ import ( | |||||
| // AppState provides centralized access to application state | // AppState provides centralized access to application state | ||||
| type AppState struct { | type AppState struct { | ||||
| beacons BeaconsList | |||||
| settings Settings | |||||
| beaconEvents BeaconEventList | |||||
| beaconsLookup BeaconsLookup | |||||
| locationHealth LocationHealth | |||||
| decoderHealth DecoderHealth | |||||
| bridgeHealth BridgeHealth | |||||
| startTime time.Time | |||||
| beacons BeaconsList | |||||
| settings Settings | |||||
| beaconEvents BeaconEventList | |||||
| beaconsLookup BeaconsLookup | |||||
| health Health | |||||
| startTime time.Time | |||||
| } | } | ||||
| func getEnv(key, def string) string { | func getEnv(key, def string) string { | ||||
| @@ -54,58 +51,58 @@ func NewAppState() *AppState { | |||||
| beaconsLookup: BeaconsLookup{ | beaconsLookup: BeaconsLookup{ | ||||
| Lookup: make(map[string]string), | Lookup: make(map[string]string), | ||||
| }, | }, | ||||
| locationHealth: LocationHealth{ | |||||
| BaseHealth: BaseHealth{ | |||||
| Lock: sync.RWMutex{}, | |||||
| Uptime: 0, | |||||
| ActiveReaders: []string{}, | |||||
| ActiveWriters: []string{}, | |||||
| ActiveBeacons: []string{}, | |||||
| health: Health{ | |||||
| ID: "1", | |||||
| Location: LocationHealth{ | |||||
| BaseHealth: BaseHealth{ | |||||
| Uptime: 0, | |||||
| ActiveReaders: []string{}, | |||||
| ActiveWriters: []string{}, | |||||
| ActiveBeacons: []string{}, | |||||
| }, | |||||
| }, | }, | ||||
| }, | |||||
| decoderHealth: DecoderHealth{ | |||||
| BaseHealth: BaseHealth{ | |||||
| Lock: sync.RWMutex{}, | |||||
| Uptime: 0, | |||||
| ActiveReaders: []string{}, | |||||
| ActiveWriters: []string{}, | |||||
| ActiveBeacons: []string{}, | |||||
| Decoder: DecoderHealth{ | |||||
| BaseHealth: BaseHealth{ | |||||
| Uptime: 0, | |||||
| ActiveReaders: []string{}, | |||||
| ActiveWriters: []string{}, | |||||
| ActiveBeacons: []string{}, | |||||
| }, | |||||
| }, | }, | ||||
| }, | |||||
| bridgeHealth: BridgeHealth{ | |||||
| BaseHealth: BaseHealth{ | |||||
| Lock: sync.RWMutex{}, | |||||
| Uptime: 0, | |||||
| ActiveReaders: []string{}, | |||||
| ActiveWriters: []string{}, | |||||
| ActiveBeacons: []string{}, | |||||
| Bridge: BridgeHealth{ | |||||
| BaseHealth: BaseHealth{ | |||||
| Uptime: 0, | |||||
| ActiveReaders: []string{}, | |||||
| ActiveWriters: []string{}, | |||||
| ActiveBeacons: []string{}, | |||||
| }, | |||||
| }, | }, | ||||
| }, | }, | ||||
| } | } | ||||
| } | } | ||||
| func (m *AppState) GetLocationHealth(k *kafkaclient.KafkaManager) *LocationHealth { | |||||
| m.locationHealth.GetUptime(m.startTime) | |||||
| m.locationHealth.GetActiveReaders(k) | |||||
| m.locationHealth.GetActiveWriters(k) | |||||
| m.locationHealth.GetActiveBeacons(m) | |||||
| return &m.locationHealth | |||||
| func (m *AppState) GetLocationHealth(k *kafkaclient.KafkaManager) ([]byte, error) { | |||||
| m.health.Location.GetUptime(m.startTime) | |||||
| m.health.Location.GetActiveReaders(k) | |||||
| m.health.Location.GetActiveWriters(k) | |||||
| m.health.Location.GetActiveBeacons(m) | |||||
| return m.health.Location.Marshal() | |||||
| } | } | ||||
| func (m *AppState) GetDecoderHealth(k *kafkaclient.KafkaManager) *DecoderHealth { | |||||
| m.decoderHealth.GetUptime(m.startTime) | |||||
| m.decoderHealth.GetActiveReaders(k) | |||||
| m.decoderHealth.GetActiveWriters(k) | |||||
| m.decoderHealth.GetActiveBeacons(m) | |||||
| return &m.decoderHealth | |||||
| func (m *AppState) GetDecoderHealth(k *kafkaclient.KafkaManager) ([]byte, error) { | |||||
| m.health.Decoder.GetUptime(m.startTime) | |||||
| m.health.Decoder.GetActiveReaders(k) | |||||
| m.health.Decoder.GetActiveWriters(k) | |||||
| m.health.Decoder.GetActiveBeacons(m) | |||||
| return m.health.Decoder.Marshal() | |||||
| } | } | ||||
| func (m *AppState) GetBridgeHealth(k *kafkaclient.KafkaManager) *BridgeHealth { | |||||
| m.bridgeHealth.GetUptime(m.startTime) | |||||
| m.bridgeHealth.GetActiveReaders(k) | |||||
| m.bridgeHealth.GetActiveWriters(k) | |||||
| m.bridgeHealth.GetActiveBeacons(m) | |||||
| return &m.bridgeHealth | |||||
| func (m *AppState) GetBridgeHealth(k *kafkaclient.KafkaManager) ([]byte, error) { | |||||
| m.health.Bridge.GetUptime(m.startTime) | |||||
| m.health.Bridge.GetActiveReaders(k) | |||||
| m.health.Bridge.GetActiveWriters(k) | |||||
| m.health.Bridge.GetActiveBeacons(m) | |||||
| return m.health.Bridge.Marshal() | |||||
| } | } | ||||
| // GetBeacons returns thread-safe access to beacons list | // GetBeacons returns thread-safe access to beacons list | ||||
| @@ -2,18 +2,16 @@ package appcontext | |||||
| import ( | import ( | ||||
| "encoding/json" | "encoding/json" | ||||
| "sync" | |||||
| "time" | "time" | ||||
| "github.com/AFASystems/presence/internal/pkg/kafkaclient" | "github.com/AFASystems/presence/internal/pkg/kafkaclient" | ||||
| ) | ) | ||||
| type BaseHealth struct { | type BaseHealth struct { | ||||
| Lock sync.RWMutex | |||||
| Uptime time.Duration `json:"uptime"` | Uptime time.Duration `json:"uptime"` | ||||
| ActiveReaders []string `json:"activeReaders"` | |||||
| ActiveWriters []string `json:"activeWriters"` | |||||
| ActiveBeacons []string `json:"activeBeacons"` | |||||
| ActiveReaders []string `json:"activeReaders" gorm:"type:jsonb"` | |||||
| ActiveWriters []string `json:"activeWriters" gorm:"type:jsonb"` | |||||
| ActiveBeacons []string `json:"activeBeacons" gorm:"type:jsonb"` | |||||
| } | } | ||||
| type DecoderHealth struct { | type DecoderHealth struct { | ||||
| @@ -23,38 +21,37 @@ type DecoderHealth struct { | |||||
| type LocationHealth struct { | type LocationHealth struct { | ||||
| BaseHealth | BaseHealth | ||||
| ActiveSettings []Settings `json:"activeSettings"` | |||||
| ActiveSettings []Settings `json:"activeSettings" gorm:"type:jsonb"` | |||||
| } | } | ||||
| type BridgeHealth struct { | type BridgeHealth struct { | ||||
| BaseHealth | BaseHealth | ||||
| } | } | ||||
| type Health struct { | |||||
| ID string `json:"id" gorm:"primaryKey"` | |||||
| Location LocationHealth `gorm:"embedded;embeddedPrefix:loc_"` | |||||
| Decoder DecoderHealth `gorm:"embedded;embeddedPrefix:dec_"` | |||||
| Bridge BridgeHealth `gorm:"embedded;embeddedPrefix:brg_"` | |||||
| } | |||||
| func (b *BaseHealth) GetUptime(startTime time.Time) { | func (b *BaseHealth) GetUptime(startTime time.Time) { | ||||
| b.Lock.Lock() | |||||
| b.Uptime = time.Since(startTime) | b.Uptime = time.Since(startTime) | ||||
| b.Lock.Unlock() | |||||
| } | } | ||||
| func (b *BaseHealth) GetActiveReaders(m *kafkaclient.KafkaManager) { | func (b *BaseHealth) GetActiveReaders(m *kafkaclient.KafkaManager) { | ||||
| b.Lock.Lock() | |||||
| b.ActiveReaders = m.GetReaders() | b.ActiveReaders = m.GetReaders() | ||||
| b.Lock.Unlock() | |||||
| } | } | ||||
| func (b *BaseHealth) GetActiveWriters(m *kafkaclient.KafkaManager) { | func (b *BaseHealth) GetActiveWriters(m *kafkaclient.KafkaManager) { | ||||
| b.Lock.Lock() | |||||
| b.ActiveWriters = m.GetWriters() | b.ActiveWriters = m.GetWriters() | ||||
| b.Lock.Unlock() | |||||
| } | } | ||||
| func (b *BaseHealth) GetActiveBeacons(m *AppState) { | func (b *BaseHealth) GetActiveBeacons(m *AppState) { | ||||
| b.Lock.Lock() | |||||
| beacons := m.GetAllBeacons() | beacons := m.GetAllBeacons() | ||||
| for beacon := range beacons { | for beacon := range beacons { | ||||
| b.ActiveBeacons = append(b.ActiveBeacons, beacon) | b.ActiveBeacons = append(b.ActiveBeacons, beacon) | ||||
| } | } | ||||
| b.Lock.Unlock() | |||||
| } | } | ||||
| func (d *DecoderHealth) Marshal() ([]byte, error) { | func (d *DecoderHealth) Marshal() ([]byte, error) { | ||||
| @@ -26,7 +26,7 @@ func Connect(cfg *config.Config) (*gorm.DB, error) { | |||||
| return nil, err | return nil, err | ||||
| } | } | ||||
| if err := db.AutoMigrate(&model.Gateway{}, model.Zone{}, model.TrackerZones{}, model.Tracker{}, model.Config{}, appcontext.Settings{}, model.Tracks{}, &model.Alert{}); err != nil { | |||||
| if err := db.AutoMigrate(&model.Gateway{}, model.Zone{}, model.TrackerZones{}, model.Tracker{}, model.Config{}, appcontext.Settings{}, model.Tracks{}, &model.Alert{}, appcontext.Health{}); err != nil { | |||||
| return nil, err | return nil, err | ||||
| } | } | ||||