diff --git a/backend/cmd/services/logging/createLog.go b/backend/cmd/services/logging/createLog.go index 4b44852..e2f3d93 100644 --- a/backend/cmd/services/logging/createLog.go +++ b/backend/cmd/services/logging/createLog.go @@ -1,4 +1,4 @@ -package logging +package loggingx import ( "encoding/json" diff --git a/backend/cmd/services/logging/logger.go b/backend/cmd/services/logging/logger.go index 05b731e..2c835dd 100644 --- a/backend/cmd/services/logging/logger.go +++ b/backend/cmd/services/logging/logger.go @@ -1,4 +1,4 @@ -package logging +package loggingx import ( "github.com/gin-gonic/gin" diff --git a/backend/cmd/services/logging/logs_get_route.go b/backend/cmd/services/logging/logs_get_route.go index 7476437..7393f23 100644 --- a/backend/cmd/services/logging/logs_get_route.go +++ b/backend/cmd/services/logging/logs_get_route.go @@ -1,4 +1,4 @@ -package logging +package loggingx import ( "fmt" @@ -20,7 +20,7 @@ var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { - fmt.Println("Origin:", r.Header.Get("Origin")) + //fmt.Println("Origin:", r.Header.Get("Origin")) return true }, } @@ -65,6 +65,17 @@ func handleSSE(c *gin.Context) { "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("Cache-Control", "no-cache") c.Header("Connection", "keep-alive") @@ -92,6 +103,11 @@ func handleSSE(c *gin.Context) { func handleWebSocket(c *gin.Context) { 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) if err != nil { log.Error("WebSocket upgrade failed", "logger", map[string]interface{}{ diff --git a/backend/cmd/services/system/config/config.go b/backend/cmd/services/system/config/config.go new file mode 100644 index 0000000..0ae7ee3 --- /dev/null +++ b/backend/cmd/services/system/config/config.go @@ -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}) + +} diff --git a/backend/cmd/services/system/servers/servers.go b/backend/cmd/services/system/servers/servers.go new file mode 100644 index 0000000..9b140a3 --- /dev/null +++ b/backend/cmd/services/system/servers/servers.go @@ -0,0 +1 @@ +package system diff --git a/backend/cmd/services/system/system.go b/backend/cmd/services/system/system.go new file mode 100644 index 0000000..9b140a3 --- /dev/null +++ b/backend/cmd/services/system/system.go @@ -0,0 +1 @@ +package system diff --git a/backend/cmd/services/websocket/channelMGT/allLogs.go b/backend/cmd/services/websocket/channelMGT/allLogs.go new file mode 100644 index 0000000..eb2e107 --- /dev/null +++ b/backend/cmd/services/websocket/channelMGT/allLogs.go @@ -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() + }() + } + } +} diff --git a/backend/cmd/services/websocket/client.go b/backend/cmd/services/websocket/client.go new file mode 100644 index 0000000..5525296 --- /dev/null +++ b/backend/cmd/services/websocket/client.go @@ -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) +} diff --git a/backend/cmd/services/websocket/handler.go b/backend/cmd/services/websocket/handler.go new file mode 100644 index 0000000..c10b2e5 --- /dev/null +++ b/backend/cmd/services/websocket/handler.go @@ -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) + } +} diff --git a/backend/cmd/services/websocket/routes.go b/backend/cmd/services/websocket/routes.go new file mode 100644 index 0000000..b2e2890 --- /dev/null +++ b/backend/cmd/services/websocket/routes.go @@ -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) +} diff --git a/backend/go.mod b/backend/go.mod index 6e3f2b5..d32324a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -28,6 +28,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // 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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // 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/klauspost/cpuid/v2 v2.3.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/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/backend/main.go b/backend/main.go index 26c2f71..bedb9ac 100644 --- a/backend/main.go +++ b/backend/main.go @@ -24,10 +24,11 @@ import ( "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/joho/godotenv" - "lst.net/cmd/services/logging" "lst.net/cmd/services/system/config" + socketio "lst.net/cmd/services/websocket" _ "lst.net/docs" "lst.net/utils/db" + logging "lst.net/utils/logger" ) func main() { @@ -110,7 +111,8 @@ func main() { c.JSON(200, gin.H{"message": "pong"}) }) - logging.RegisterLoggerRoutes(r, basePath) + //logging.RegisterLoggerRoutes(r, basePath) + socketio.RegisterSocketRoutes(r) config.RegisterConfigRoutes(r, basePath) r.Any(basePath+"/api", errorApiLoc) diff --git a/backend/utils/db/config.go b/backend/utils/db/config.go new file mode 100644 index 0000000..e6a68ac --- /dev/null +++ b/backend/utils/db/config.go @@ -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 + +} diff --git a/backend/utils/db/db.go b/backend/utils/db/db.go index 3f5cdd7..b52f774 100644 --- a/backend/utils/db/db.go +++ b/backend/utils/db/db.go @@ -10,6 +10,8 @@ import ( var DB *gorm.DB +type JSONB map[string]interface{} + func InitDB() error { dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s", os.Getenv("DB_HOST"), @@ -25,8 +27,17 @@ func InitDB() error { return fmt.Errorf("failed to connect to database: %v", err) } - // Auto-migrate all models - DB.AutoMigrate(&Log{}) // Add other models here + fmt.Println("✅ Connected to database") + + // 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 } diff --git a/backend/utils/db/logs.go b/backend/utils/db/logs.go index fcabd09..d9139c2 100644 --- a/backend/utils/db/logs.go +++ b/backend/utils/db/logs.go @@ -1,19 +1,26 @@ package db -import "time" +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) 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. Message string `gorm:"not null"` - Service string `gorm:"size:50"` // Optional: service name - Metadata JSONB `gorm:"type:jsonb"` // Structured fields (e.g., {"user_id": 123}) - CreatedAt time.Time `gorm:"index"` // Auto-set by GORM + Service string `gorm:"size:50"` + Metadata JSONB `gorm:"type:jsonb"` // fields (e.g., {"user_id": 123}) + CreatedAt time.Time `gorm:"index"` Checked bool `gorm:"type:boolean;default:false"` + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` } // JSONB is a helper type for PostgreSQL JSONB fields. -type JSONB map[string]interface{} +//type JSONB map[string]interface{} // --- CRUD Operations --- @@ -29,13 +36,13 @@ func CreateLog(level, message, service string, metadata JSONB) error { } // 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 - 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 } -// DeleteOldLogs removes logs older than `days`. -func DeleteOldLogs(days int) error { - return DB.Where("created_at < ?", time.Now().AddDate(0, 0, -days)).Delete(&Log{}).Error +// DeleteOldLogs removes logs older than `days` and by level. +func DeleteOldLogs(days int, level string) error { + return DB.Where("created_at < ? and level = ?", time.Now().AddDate(0, 0, -days), level).Delete(&Log{}).Error } diff --git a/backend/utils/db/ws_clients.go b/backend/utils/db/ws_clients.go new file mode 100644 index 0000000..f8a389f --- /dev/null +++ b/backend/utils/db/ws_clients.go @@ -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"` +} diff --git a/backend/utils/logger/logger.go b/backend/utils/logger/logger.go new file mode 100644 index 0000000..1391f43 --- /dev/null +++ b/backend/utils/logger/logger.go @@ -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) +}