first
This commit is contained in:
243
backend/cmd/server/main.go
Normal file
243
backend/cmd/server/main.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"mqtt_explorer/internal/api"
|
||||
"mqtt_explorer/internal/config"
|
||||
"mqtt_explorer/internal/filters"
|
||||
"mqtt_explorer/internal/mqtt"
|
||||
"mqtt_explorer/internal/settings"
|
||||
"mqtt_explorer/internal/storage"
|
||||
"mqtt_explorer/internal/sysinfo"
|
||||
"mqtt_explorer/internal/topics"
|
||||
"mqtt_explorer/internal/ws"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
store, err := storage.Open(cfg.SQLitePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tree := topics.NewTree()
|
||||
hub := ws.NewHub()
|
||||
filterStore := filters.NewStore()
|
||||
sysStore := sysinfo.NewStore()
|
||||
settingsStore := settings.NewStore(cfg.SettingsFile)
|
||||
defaultSettings := settings.Settings{
|
||||
Theme: "dark-monokai",
|
||||
RepoURL: "https://gitea.maison43.duckdns.org/gilles/mqtt_explorer",
|
||||
TTLDays: cfg.TTLDays,
|
||||
MaxPayloadBytes: 1024 * 100,
|
||||
AutoPurgePayloads: false,
|
||||
AutoPurgePayloadBytes: 1024 * 250,
|
||||
AutoExpandDepth: 2,
|
||||
ImageDetection: true,
|
||||
HighlightMs: 300,
|
||||
MQTTProfiles: []settings.MQTTProfile{
|
||||
{
|
||||
ID: "default",
|
||||
Name: "Broker local",
|
||||
Host: "10.0.0.3",
|
||||
Port: 1883,
|
||||
Username: "",
|
||||
Password: "",
|
||||
IsDefault: true,
|
||||
},
|
||||
},
|
||||
ActiveProfileID: "default",
|
||||
ApplyViewFilter: true,
|
||||
ExpandTreeOnStart: false,
|
||||
TopicFilters: []settings.TopicFilter{},
|
||||
UIFontSize: 13,
|
||||
TopicFontSize: 12,
|
||||
PayloadFontSize: 12,
|
||||
StatsRefreshMs: 5000,
|
||||
ResizeHandlePx: 8,
|
||||
}
|
||||
loadedSettings, err := settingsStore.Load(defaultSettings)
|
||||
if err != nil {
|
||||
log.Printf("settings load error: %v", err)
|
||||
} else {
|
||||
filterStore.Update(toFilterRules(loadedSettings.TopicFilters))
|
||||
}
|
||||
settingsRuntime := settings.NewRuntime(loadedSettings)
|
||||
|
||||
mqttMgr, err := mqtt.NewManager(
|
||||
cfg.MQTTBroker,
|
||||
cfg.MQTTUsername,
|
||||
cfg.MQTTPassword,
|
||||
cfg.MQTTClientID,
|
||||
func(topic string, payload []byte, qos byte, retained bool) {
|
||||
if cfg.MQTTDebug {
|
||||
preview := string(payload)
|
||||
if len(preview) > 256 {
|
||||
preview = preview[:256] + "..."
|
||||
}
|
||||
log.Printf("mqtt message topic=%s qos=%d retained=%t size=%d payload=%q", topic, qos, retained, len(payload), preview)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(topic, "$SYS/") {
|
||||
sysStore.Update(topic, string(payload))
|
||||
return
|
||||
}
|
||||
|
||||
msg := topics.Message{
|
||||
ID: uuid.NewString(),
|
||||
Topic: topic,
|
||||
Payload: string(payload),
|
||||
QOS: qos,
|
||||
Retained: retained,
|
||||
Timestamp: time.Now().UTC(),
|
||||
Size: len(payload),
|
||||
}
|
||||
|
||||
tree.AddMessage(msg)
|
||||
if rule, ok := filterStore.Match(topic); !ok || rule.Save {
|
||||
currentSettings := settingsRuntime.Get()
|
||||
skipStore := currentSettings.AutoPurgePayloads && currentSettings.AutoPurgePayloadBytes > 0 && len(payload) >= currentSettings.AutoPurgePayloadBytes
|
||||
if !skipStore {
|
||||
_ = store.InsertMessage(storage.Message{
|
||||
ID: msg.ID,
|
||||
Topic: msg.Topic,
|
||||
Payload: msg.Payload,
|
||||
QOS: msg.QOS,
|
||||
Retained: msg.Retained,
|
||||
Timestamp: msg.Timestamp,
|
||||
Size: msg.Size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
hub.Broadcast(ws.Event{Type: "message", Data: msg})
|
||||
},
|
||||
cfg.MQTTSubscribe,
|
||||
cfg.MQTTQOS,
|
||||
cfg.SysSubscribe,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go startTTLJob(store, settingsRuntime)
|
||||
go startOversizeJob(store, settingsRuntime)
|
||||
go startDBSizeJob(store, 400*1024*1024)
|
||||
go startStatsJob(store, hub)
|
||||
|
||||
staticDir := resolveStaticDir()
|
||||
server := api.NewServer(store, tree, hub, mqttMgr, filterStore, settingsStore, settingsRuntime, defaultSettings, sysStore, staticDir)
|
||||
|
||||
go func() {
|
||||
addr := fmt.Sprintf(":%s", cfg.HTTPPort)
|
||||
if err := server.Run(addr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
mqttMgr.Disconnect()
|
||||
}
|
||||
|
||||
func toFilterRules(filtersIn []settings.TopicFilter) []filters.Rule {
|
||||
out := make([]filters.Rule, 0, len(filtersIn))
|
||||
for _, entry := range filtersIn {
|
||||
out = append(out, filters.Rule{
|
||||
Topic: entry.Topic,
|
||||
Save: entry.Save,
|
||||
View: entry.View,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func resolveStaticDir() string {
|
||||
candidates := []string{
|
||||
"./frontend/dist",
|
||||
"./dist",
|
||||
}
|
||||
|
||||
for _, path := range candidates {
|
||||
if info, err := os.Stat(path); err == nil && info.IsDir() {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err == nil {
|
||||
return abs
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func startTTLJob(store *storage.Store, settingsRuntime *settings.Runtime) {
|
||||
ticker := time.NewTicker(10 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
ttlDays := settingsRuntime.Get().TTLDays
|
||||
if ttlDays <= 0 {
|
||||
continue
|
||||
}
|
||||
cutoff := time.Now().AddDate(0, 0, -ttlDays)
|
||||
_, _ = store.PurgeBefore(cutoff)
|
||||
}
|
||||
}
|
||||
|
||||
func startOversizeJob(store *storage.Store, settingsRuntime *settings.Runtime) {
|
||||
ticker := time.NewTicker(10 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
settingsSnapshot := settingsRuntime.Get()
|
||||
if !settingsSnapshot.AutoPurgePayloads || settingsSnapshot.AutoPurgePayloadBytes <= 0 {
|
||||
continue
|
||||
}
|
||||
_, _ = store.PurgeOversize(settingsSnapshot.AutoPurgePayloadBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func startDBSizeJob(store *storage.Store, maxBytes int64) {
|
||||
if maxBytes <= 0 {
|
||||
return
|
||||
}
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
stats, err := store.Stats()
|
||||
if err != nil || stats.Bytes <= maxBytes {
|
||||
continue
|
||||
}
|
||||
if deleted, err := store.DeleteOldestFraction(0.25); err == nil && deleted > 0 {
|
||||
_ = store.Compact()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func startStatsJob(store *storage.Store, hub *ws.Hub) {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
stats, err := store.Stats()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
hub.Broadcast(ws.Event{Type: "stats", Data: stats})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user