feat(ws server): added in a websocket on port system to help with better logging
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
package logging
|
package loggingx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package logging
|
package loggingx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package logging
|
package loggingx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -20,7 +20,7 @@ var upgrader = websocket.Upgrader{
|
|||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 1024,
|
WriteBufferSize: 1024,
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
fmt.Println("Origin:", r.Header.Get("Origin"))
|
//fmt.Println("Origin:", r.Header.Get("Origin"))
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,17 @@ func handleSSE(c *gin.Context) {
|
|||||||
"user_agent": c.Request.UserAgent(),
|
"user_agent": c.Request.UserAgent(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
c.Header("Access-Control-Allow-Credentials", "true")
|
||||||
|
c.Header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
c.Header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||||
|
|
||||||
|
// Handle preflight requests
|
||||||
|
if c.Request.Method == "OPTIONS" {
|
||||||
|
c.AbortWithStatus(204)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Header("Content-Type", "text/event-stream")
|
c.Header("Content-Type", "text/event-stream")
|
||||||
c.Header("Cache-Control", "no-cache")
|
c.Header("Cache-Control", "no-cache")
|
||||||
c.Header("Connection", "keep-alive")
|
c.Header("Connection", "keep-alive")
|
||||||
@@ -92,6 +103,11 @@ func handleSSE(c *gin.Context) {
|
|||||||
|
|
||||||
func handleWebSocket(c *gin.Context) {
|
func handleWebSocket(c *gin.Context) {
|
||||||
log := New()
|
log := New()
|
||||||
|
log.Info("WebSocket connection established", "logger", map[string]interface{}{
|
||||||
|
"endpoint": "/api/logger/logs",
|
||||||
|
"client_ip": c.ClientIP(),
|
||||||
|
"user_agent": c.Request.UserAgent(),
|
||||||
|
})
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("WebSocket upgrade failed", "logger", map[string]interface{}{
|
log.Error("WebSocket upgrade failed", "logger", map[string]interface{}{
|
||||||
|
|||||||
53
backend/cmd/services/system/config/config.go
Normal file
53
backend/cmd/services/system/config/config.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"lst.net/utils/db"
|
||||||
|
logging "lst.net/utils/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigUpdateInput struct {
|
||||||
|
Description *string `json:"description"`
|
||||||
|
Value *string `json:"value"`
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
|
AppService *string `json:"app_service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterConfigRoutes(l *gin.Engine, baseUrl string) {
|
||||||
|
// seed the db on start up
|
||||||
|
db.SeedConfigs(db.DB)
|
||||||
|
|
||||||
|
configGroup := l.Group(baseUrl + "/api/config")
|
||||||
|
configGroup.GET("/configs", getconfigs)
|
||||||
|
configGroup.POST("/configs", newConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getconfigs(c *gin.Context) {
|
||||||
|
log := logging.New()
|
||||||
|
configs, err := db.GetAllConfigs(db.DB)
|
||||||
|
log.Info("Current Configs", "system", map[string]interface{}{
|
||||||
|
"endpoint": "/api/config/configs",
|
||||||
|
"client_ip": c.ClientIP(),
|
||||||
|
"user_agent": c.Request.UserAgent(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"message": "There was an error getting the configs", "error": err})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{"message": "Current configs", "data": configs})
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConfig(c *gin.Context) {
|
||||||
|
|
||||||
|
var config ConfigUpdateInput
|
||||||
|
|
||||||
|
err := c.ShouldBindBodyWithJSON(&config)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(500, gin.H{"message": "Internal Server Error"})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, gin.H{"message": "New config was just added", "data": config})
|
||||||
|
|
||||||
|
}
|
||||||
1
backend/cmd/services/system/servers/servers.go
Normal file
1
backend/cmd/services/system/servers/servers.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package system
|
||||||
1
backend/cmd/services/system/system.go
Normal file
1
backend/cmd/services/system/system.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package system
|
||||||
83
backend/cmd/services/websocket/channelMGT/allLogs.go
Normal file
83
backend/cmd/services/websocket/channelMGT/allLogs.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
package channelmgt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
logging "lst.net/utils/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setup the notifiyer
|
||||||
|
|
||||||
|
// -- Only needs to be run once in DB
|
||||||
|
// CREATE OR REPLACE FUNCTION notify_new_log() RETURNS trigger AS $$
|
||||||
|
// BEGIN
|
||||||
|
// PERFORM pg_notify('new_log', row_to_json(NEW)::text);
|
||||||
|
// RETURN NEW;
|
||||||
|
// END;
|
||||||
|
// $$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
// DROP TRIGGER IF EXISTS new_log_trigger ON logs;
|
||||||
|
|
||||||
|
// CREATE TRIGGER new_log_trigger
|
||||||
|
// AFTER INSERT ON logs
|
||||||
|
// FOR EACH ROW EXECUTE FUNCTION notify_new_log();
|
||||||
|
|
||||||
|
func AllLogs(db *sql.DB, broadcaster chan logging.Message) {
|
||||||
|
fmt.Println("[AllLogs] started")
|
||||||
|
log := logging.New()
|
||||||
|
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
os.Getenv("DB_HOST"),
|
||||||
|
os.Getenv("DB_PORT"),
|
||||||
|
os.Getenv("DB_USER"),
|
||||||
|
os.Getenv("DB_PASSWORD"),
|
||||||
|
os.Getenv("DB_NAME"),
|
||||||
|
)
|
||||||
|
|
||||||
|
listener := pq.NewListener(dsn, 10*time.Second, time.Minute, nil)
|
||||||
|
err := listener.Listen("new_log")
|
||||||
|
if err != nil {
|
||||||
|
log.Panic("Failed to LISTEN on new_log", "logger", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Listening for new logs...")
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case notify := <-listener.Notify:
|
||||||
|
if notify != nil {
|
||||||
|
fmt.Println("New log notification received")
|
||||||
|
|
||||||
|
// Unmarshal the JSON payload of the inserted row
|
||||||
|
var logData map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(notify.Extra), &logData); err != nil {
|
||||||
|
log.Error("Failed to unmarshal notification payload", "logger", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build message to broadcast
|
||||||
|
msg := logging.Message{
|
||||||
|
Channel: "logs", // This matches your logs channel name
|
||||||
|
Data: logData,
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcaster <- msg
|
||||||
|
//fmt.Printf("[Broadcasting] sending: %+v\n", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(90 * time.Second):
|
||||||
|
go func() {
|
||||||
|
log.Debug("Re-pinging Postgres LISTEN", "logger", map[string]interface{}{})
|
||||||
|
listener.Ping()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
backend/cmd/services/websocket/client.go
Normal file
93
backend/cmd/services/websocket/client.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package socketio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
logging "lst.net/utils/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
ClientID uuid.UUID
|
||||||
|
Conn *websocket.Conn
|
||||||
|
APIKey string
|
||||||
|
IPAddress string
|
||||||
|
UserAgent string
|
||||||
|
Send chan []byte
|
||||||
|
Channels map[string]bool // e.g., {"logs": true, "labels": true}
|
||||||
|
}
|
||||||
|
|
||||||
|
var clients = make(map[*Client]bool)
|
||||||
|
|
||||||
|
var clientsLock sync.Mutex
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
var broadcast = make(chan string)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
msg := <-broadcast
|
||||||
|
|
||||||
|
clientsLock.Lock()
|
||||||
|
for client := range clients {
|
||||||
|
if client.Channels["logs"] {
|
||||||
|
err := client.Conn.WriteMessage(websocket.TextMessage, []byte(msg))
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Write error:", err)
|
||||||
|
client.Conn.Close()
|
||||||
|
//client.MarkDisconnected()
|
||||||
|
delete(clients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientsLock.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartBroadcasting(broadcaster chan logging.Message) {
|
||||||
|
go func() {
|
||||||
|
log.Println("StartBroadcasting goroutine started")
|
||||||
|
for {
|
||||||
|
msg := <-broadcaster
|
||||||
|
//log.Printf("Received msg on broadcaster: %+v\n", msg)
|
||||||
|
clientsLock.Lock()
|
||||||
|
for client := range clients {
|
||||||
|
if client.Channels[msg.Channel] {
|
||||||
|
log.Println("Sending message to client")
|
||||||
|
err := client.Conn.WriteJSON(msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Write error:", err)
|
||||||
|
client.Conn.Close()
|
||||||
|
client.MarkDisconnected()
|
||||||
|
delete(clients, client)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Println("Skipping client, channel mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientsLock.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (c *Client) JoinChannel(name string) {
|
||||||
|
// ch := GetOrCreateChannel(name)
|
||||||
|
// c.Channels[name] = ch
|
||||||
|
// ch.Register <- c
|
||||||
|
// }
|
||||||
|
|
||||||
|
// func (c *Client) LeaveChannel(name string) {
|
||||||
|
// if ch, ok := c.Channels[name]; ok {
|
||||||
|
// ch.Unregister <- c
|
||||||
|
// delete(c.Channels, name)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (c *Client) Disconnect() {
|
||||||
|
// for _, ch := range c.Channels {
|
||||||
|
// ch.Unregister <- c
|
||||||
|
// }
|
||||||
|
close(c.Send)
|
||||||
|
}
|
||||||
163
backend/cmd/services/websocket/handler.go
Normal file
163
backend/cmd/services/websocket/handler.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package socketio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"lst.net/utils/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JoinPayload struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Services []string `json:"services,omitempty"`
|
||||||
|
APIKey string `json:"apiKey"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// type Channel struct {
|
||||||
|
// Name string
|
||||||
|
// Clients map[*Client]bool
|
||||||
|
// Register chan *Client
|
||||||
|
// Unregister chan *Client
|
||||||
|
// Broadcast chan Message
|
||||||
|
// }
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true }, // allow all origins; customize for prod
|
||||||
|
}
|
||||||
|
|
||||||
|
func SocketHandler(c *gin.Context) {
|
||||||
|
// Upgrade HTTP to websocket
|
||||||
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Failed to upgrade:", err)
|
||||||
|
c.AbortWithStatus(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Create client struct
|
||||||
|
client := &Client{
|
||||||
|
Conn: conn,
|
||||||
|
IPAddress: c.ClientIP(),
|
||||||
|
UserAgent: c.Request.UserAgent(),
|
||||||
|
Channels: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
clientsLock.Lock()
|
||||||
|
clients[client] = true
|
||||||
|
clientsLock.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
clientsLock.Lock()
|
||||||
|
delete(clients, client)
|
||||||
|
clientsLock.Unlock()
|
||||||
|
client.MarkDisconnected()
|
||||||
|
client.Disconnect()
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
// Read message from client
|
||||||
|
_, msg, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Read error:", err)
|
||||||
|
clientsLock.Lock()
|
||||||
|
delete(clients, client)
|
||||||
|
clientsLock.Unlock()
|
||||||
|
client.MarkDisconnected()
|
||||||
|
client.Disconnect()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload JoinPayload
|
||||||
|
err = json.Unmarshal(msg, &payload)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Invalid JSON payload:", err)
|
||||||
|
clientsLock.Lock()
|
||||||
|
delete(clients, client)
|
||||||
|
clientsLock.Unlock()
|
||||||
|
client.MarkDisconnected()
|
||||||
|
client.Disconnect()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple API key check (replace with real auth)
|
||||||
|
if payload.APIKey == "" {
|
||||||
|
conn.WriteMessage(websocket.TextMessage, []byte("Missing API Key"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
client.APIKey = payload.APIKey
|
||||||
|
|
||||||
|
// Handle channel subscription, add more here as we get more in.
|
||||||
|
switch payload.Channel {
|
||||||
|
case "logs":
|
||||||
|
client.Channels["logs"] = true
|
||||||
|
case "logServices":
|
||||||
|
for _, svc := range payload.Services {
|
||||||
|
client.Channels["logServices:"+svc] = true
|
||||||
|
}
|
||||||
|
case "labels":
|
||||||
|
client.Channels["labels"] = true
|
||||||
|
default:
|
||||||
|
conn.WriteMessage(websocket.TextMessage, []byte("Unknown channel"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save client info in DB
|
||||||
|
client.SaveToDB()
|
||||||
|
|
||||||
|
// Confirm subscription
|
||||||
|
resp := map[string]string{
|
||||||
|
"status": "subscribed",
|
||||||
|
"channel": payload.Channel,
|
||||||
|
}
|
||||||
|
respJSON, _ := json.Marshal(resp)
|
||||||
|
conn.WriteMessage(websocket.TextMessage, respJSON)
|
||||||
|
|
||||||
|
// You could now start pushing messages to client or keep connection open
|
||||||
|
// For demo, just wait and keep connection alive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SaveToDB() {
|
||||||
|
// Convert c.Channels (map[string]bool) to map[string]interface{} for JSONB
|
||||||
|
channels := make(map[string]interface{})
|
||||||
|
for ch := range c.Channels {
|
||||||
|
channels[ch] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
clientRecord := &db.ClientRecord{
|
||||||
|
APIKey: c.APIKey,
|
||||||
|
IPAddress: c.IPAddress,
|
||||||
|
UserAgent: c.UserAgent,
|
||||||
|
Channels: db.JSONB(channels),
|
||||||
|
ConnectedAt: time.Now(),
|
||||||
|
LastHeartbeat: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.DB.Create(&clientRecord).Error; err != nil {
|
||||||
|
log.Println("❌ Error saving client:", err)
|
||||||
|
} else {
|
||||||
|
c.ClientID = clientRecord.ClientID // ✅ Assign the generated UUID back to the client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) MarkDisconnected() {
|
||||||
|
now := time.Now()
|
||||||
|
res := db.DB.Model(&db.ClientRecord{}).
|
||||||
|
Where("client_id = ?", c.ClientID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"disconnected_at": &now,
|
||||||
|
})
|
||||||
|
|
||||||
|
if res.RowsAffected == 0 {
|
||||||
|
log.Println("⚠️ No rows updated for client_id:", c.ClientID)
|
||||||
|
}
|
||||||
|
if res.Error != nil {
|
||||||
|
log.Println("❌ Error updating disconnected_at:", res.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
backend/cmd/services/websocket/routes.go
Normal file
25
backend/cmd/services/websocket/routes.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package socketio
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
channelmgt "lst.net/cmd/services/websocket/channelMGT"
|
||||||
|
"lst.net/utils/db"
|
||||||
|
logging "lst.net/utils/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var broadcaster = make(chan logging.Message) // define broadcaster here so it’s accessible
|
||||||
|
|
||||||
|
func RegisterSocketRoutes(r *gin.Engine) {
|
||||||
|
sqlDB, err := db.DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// channels
|
||||||
|
go channelmgt.AllLogs(sqlDB, broadcaster)
|
||||||
|
go StartBroadcasting(broadcaster)
|
||||||
|
|
||||||
|
wsGroup := r.Group("/ws")
|
||||||
|
wsGroup.GET("/connect", SocketHandler)
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||||
@@ -38,6 +39,7 @@ require (
|
|||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
github.com/mailru/easyjson v0.9.0 // indirect
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ import (
|
|||||||
"github.com/gin-contrib/cors"
|
"github.com/gin-contrib/cors"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"lst.net/cmd/services/logging"
|
|
||||||
"lst.net/cmd/services/system/config"
|
"lst.net/cmd/services/system/config"
|
||||||
|
socketio "lst.net/cmd/services/websocket"
|
||||||
_ "lst.net/docs"
|
_ "lst.net/docs"
|
||||||
"lst.net/utils/db"
|
"lst.net/utils/db"
|
||||||
|
logging "lst.net/utils/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -110,7 +111,8 @@ func main() {
|
|||||||
c.JSON(200, gin.H{"message": "pong"})
|
c.JSON(200, gin.H{"message": "pong"})
|
||||||
})
|
})
|
||||||
|
|
||||||
logging.RegisterLoggerRoutes(r, basePath)
|
//logging.RegisterLoggerRoutes(r, basePath)
|
||||||
|
socketio.RegisterSocketRoutes(r)
|
||||||
config.RegisterConfigRoutes(r, basePath)
|
config.RegisterConfigRoutes(r, basePath)
|
||||||
|
|
||||||
r.Any(basePath+"/api", errorApiLoc)
|
r.Any(basePath+"/api", errorApiLoc)
|
||||||
|
|||||||
67
backend/utils/db/config.go
Normal file
67
backend/utils/db/config.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
gorm.Model
|
||||||
|
ID uint `gorm:"primaryKey;autoIncrement"`
|
||||||
|
Name string `gorm:"uniqueIndex;not null"`
|
||||||
|
Description string `gorm:"type:text"`
|
||||||
|
Value string `gorm:"not null"`
|
||||||
|
Enabled bool `gorm:"default:true"`
|
||||||
|
AppService string `gorm:"default:system"`
|
||||||
|
CreatedAt time.Time `gorm:"index"`
|
||||||
|
UpdatedAt time.Time `gorm:"index"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var seedConfigData = []Config{
|
||||||
|
{Name: "serverPort", Description: "The port the server will listen on if not running in docker", Value: "4000", Enabled: true},
|
||||||
|
{Name: "server", Description: "The server we will use when connecting to the alplaprod sql", Value: "usmcd1vms006", Enabled: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
func SeedConfigs(db *gorm.DB) error {
|
||||||
|
|
||||||
|
for _, cfg := range seedConfigData {
|
||||||
|
var existing Config
|
||||||
|
// Try to find config by unique name
|
||||||
|
result := db.Where("name =?", cfg.Name).First(&existing)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
if result.Error == gorm.ErrRecordNotFound {
|
||||||
|
// not here lets add it
|
||||||
|
if err := db.Create(&cfg).Error; err != nil {
|
||||||
|
log.Printf("Failed to seed config %s: %v", cfg.Name, err)
|
||||||
|
}
|
||||||
|
log.Printf("Seeded new config: %s", cfg.Name)
|
||||||
|
} else {
|
||||||
|
// Some other error
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// only update the fields we want to update.
|
||||||
|
existing.Description = cfg.Description
|
||||||
|
if err := db.Save(&existing).Error; err != nil {
|
||||||
|
log.Printf("Failed to update config %s: %v", cfg.Name, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Updated existing config: %s", cfg.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllConfigs(db *gorm.DB) ([]Config, error) {
|
||||||
|
var configs []Config
|
||||||
|
|
||||||
|
result := db.Find(&configs)
|
||||||
|
|
||||||
|
return configs, result.Error
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
|
|
||||||
var DB *gorm.DB
|
var DB *gorm.DB
|
||||||
|
|
||||||
|
type JSONB map[string]interface{}
|
||||||
|
|
||||||
func InitDB() error {
|
func InitDB() error {
|
||||||
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s",
|
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s",
|
||||||
os.Getenv("DB_HOST"),
|
os.Getenv("DB_HOST"),
|
||||||
@@ -25,8 +27,17 @@ func InitDB() error {
|
|||||||
return fmt.Errorf("failed to connect to database: %v", err)
|
return fmt.Errorf("failed to connect to database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-migrate all models
|
fmt.Println("✅ Connected to database")
|
||||||
DB.AutoMigrate(&Log{}) // Add other models here
|
|
||||||
|
// ensures we have the uuid stuff setup properly
|
||||||
|
DB.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`)
|
||||||
|
|
||||||
|
err = DB.AutoMigrate(&Log{}, &Config{}, &ClientRecord{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to auto-migrate models: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✅ Database migration completed successfully")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,26 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type Log struct {
|
type Log struct {
|
||||||
ID uint `gorm:"primaryKey"`
|
LogID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
|
||||||
Level string `gorm:"size:10;not null"` // "info", "error", etc.
|
Level string `gorm:"size:10;not null"` // "info", "error", etc.
|
||||||
Message string `gorm:"not null"`
|
Message string `gorm:"not null"`
|
||||||
Service string `gorm:"size:50"` // Optional: service name
|
Service string `gorm:"size:50"`
|
||||||
Metadata JSONB `gorm:"type:jsonb"` // Structured fields (e.g., {"user_id": 123})
|
Metadata JSONB `gorm:"type:jsonb"` // fields (e.g., {"user_id": 123})
|
||||||
CreatedAt time.Time `gorm:"index"` // Auto-set by GORM
|
CreatedAt time.Time `gorm:"index"`
|
||||||
Checked bool `gorm:"type:boolean;default:false"`
|
Checked bool `gorm:"type:boolean;default:false"`
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONB is a helper type for PostgreSQL JSONB fields.
|
// JSONB is a helper type for PostgreSQL JSONB fields.
|
||||||
type JSONB map[string]interface{}
|
//type JSONB map[string]interface{}
|
||||||
|
|
||||||
// --- CRUD Operations ---
|
// --- CRUD Operations ---
|
||||||
|
|
||||||
@@ -29,13 +36,13 @@ func CreateLog(level, message, service string, metadata JSONB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLogsByLevel fetches logs filtered by severity.
|
// GetLogsByLevel fetches logs filtered by severity.
|
||||||
func GetLogsByLevel(level string, limit int) ([]Log, error) {
|
func GetLogs(level string, limit int, service string) ([]Log, error) {
|
||||||
var logs []Log
|
var logs []Log
|
||||||
err := DB.Where("level = ?", level).Limit(limit).Find(&logs).Error
|
err := DB.Where("level = ? and service = ?", level, service).Limit(limit).Find(&logs).Error
|
||||||
return logs, err
|
return logs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteOldLogs removes logs older than `days`.
|
// DeleteOldLogs removes logs older than `days` and by level.
|
||||||
func DeleteOldLogs(days int) error {
|
func DeleteOldLogs(days int, level string) error {
|
||||||
return DB.Where("created_at < ?", time.Now().AddDate(0, 0, -days)).Delete(&Log{}).Error
|
return DB.Where("created_at < ? and level = ?", time.Now().AddDate(0, 0, -days), level).Delete(&Log{}).Error
|
||||||
}
|
}
|
||||||
|
|||||||
20
backend/utils/db/ws_clients.go
Normal file
20
backend/utils/db/ws_clients.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientRecord struct {
|
||||||
|
ClientID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
|
||||||
|
APIKey string `gorm:"not null"`
|
||||||
|
IPAddress string `gorm:"not null"`
|
||||||
|
UserAgent string `gorm:"size:255"`
|
||||||
|
ConnectedAt time.Time `gorm:"index"`
|
||||||
|
LastHeartbeat time.Time `gorm:"index"`
|
||||||
|
Channels JSONB `gorm:"type:jsonb"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DisconnectedAt *time.Time `gorm:"column:disconnected_at"`
|
||||||
|
}
|
||||||
116
backend/utils/logger/logger.go
Normal file
116
backend/utils/logger/logger.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"lst.net/utils/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomLogger struct {
|
||||||
|
consoleLogger zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a configured CustomLogger.
|
||||||
|
func New() *CustomLogger {
|
||||||
|
// Colorized console output
|
||||||
|
consoleWriter := zerolog.ConsoleWriter{
|
||||||
|
Out: os.Stderr,
|
||||||
|
TimeFormat: "2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CustomLogger{
|
||||||
|
consoleLogger: zerolog.New(consoleWriter).
|
||||||
|
With().
|
||||||
|
Timestamp().
|
||||||
|
Logger(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PrettyFormat(level, message string, metadata map[string]interface{}) string {
|
||||||
|
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
base := fmt.Sprintf("[%s] %s| Message: %s", strings.ToUpper(level), timestamp, message)
|
||||||
|
|
||||||
|
if len(metadata) > 0 {
|
||||||
|
metaJSON, _ := json.Marshal(metadata)
|
||||||
|
return fmt.Sprintf("%s | Metadata: %s", base, string(metaJSON))
|
||||||
|
}
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) logToPostgres(level, message, service string, metadata map[string]interface{}) {
|
||||||
|
err := db.CreateLog(level, message, service, metadata)
|
||||||
|
if err != nil {
|
||||||
|
// Fallback to console if DB fails
|
||||||
|
log.Error().Err(err).Msg("Failed to write log to PostgreSQL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Level-Specific Methods ---
|
||||||
|
|
||||||
|
func (l *CustomLogger) Info(message, service string, fields map[string]interface{}) {
|
||||||
|
l.consoleLogger.Info().Fields(fields).Msg(message)
|
||||||
|
l.logToPostgres("info", message, service, fields)
|
||||||
|
|
||||||
|
//PostLog(PrettyFormat("info", message, fields)) // Broadcast pretty message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) Warn(message, service string, fields map[string]interface{}) {
|
||||||
|
l.consoleLogger.Error().Fields(fields).Msg(message)
|
||||||
|
l.logToPostgres("warn", message, service, fields)
|
||||||
|
|
||||||
|
//PostLog(PrettyFormat("warn", message, fields)) // Broadcast pretty message
|
||||||
|
|
||||||
|
// Custom logic for errors (e.g., alerting)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
l.consoleLogger.Warn().Msg("Additional error context captured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) Error(message, service string, fields map[string]interface{}) {
|
||||||
|
l.consoleLogger.Error().Fields(fields).Msg(message)
|
||||||
|
l.logToPostgres("error", message, service, fields)
|
||||||
|
|
||||||
|
//PostLog(PrettyFormat("error", message, fields)) // Broadcast pretty message
|
||||||
|
|
||||||
|
// Custom logic for errors (e.g., alerting)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
l.consoleLogger.Warn().Msg("Additional error context captured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) Panic(message, service string, fields map[string]interface{}) {
|
||||||
|
// Log to console (colored, with fields)
|
||||||
|
l.consoleLogger.Error().
|
||||||
|
Str("service", service).
|
||||||
|
Fields(fields).
|
||||||
|
Msg(message + " (PANIC)") // Explicitly mark as panic
|
||||||
|
|
||||||
|
// Log to PostgreSQL (sync to ensure it's saved before crashing)
|
||||||
|
err := db.CreateLog("panic", message, service, fields) // isCritical=true
|
||||||
|
if err != nil {
|
||||||
|
l.consoleLogger.Error().Err(err).Msg("Failed to save panic log to PostgreSQL")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional context (optional)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
l.consoleLogger.Warn().Msg("Additional panic context captured")
|
||||||
|
}
|
||||||
|
|
||||||
|
panic(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CustomLogger) Debug(message, service string, fields map[string]interface{}) {
|
||||||
|
l.consoleLogger.Debug().Fields(fields).Msg(message)
|
||||||
|
l.logToPostgres("debug", message, service, fields)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user