5 Commits

Author SHA1 Message Date
6a631be909 docs(docker): docs about the custom network for the db is seperated 2025-07-25 12:14:51 -05:00
75c17d2065 test(iis): wrapper test for ws 2025-07-25 12:14:05 -05:00
63c053b38c docs(wss): more ws stuff 2025-07-25 12:13:47 -05:00
5bcbdaf3d0 feat(ws server): added in a websocket on port system to help with better logging 2025-07-25 12:13:19 -05:00
074032f20d refactor(app port): changed to have the port be dyncamic on the iis side
docker will default to 8080 and can be adjusted via the docker compose, or passing the same env over
it will change it as well.
2025-07-23 07:36:18 -05:00
24 changed files with 801 additions and 52 deletions

View File

@@ -1,6 +1,9 @@
# uncomment this out to run in productions # uncomment this out to run in productions
# APP_ENV=production # APP_ENV=production
# Server port that will allow vite to talk to the backend.
VITE_SERVER_PORT=4000
# lstv2 loc # lstv2 loc
LSTV2="C\drive\loc" LSTV2="C\drive\loc"

View File

@@ -20,6 +20,7 @@ app.Use(async (context, next) =>
if (context.WebSockets.IsWebSocketRequest && if (context.WebSockets.IsWebSocketRequest &&
context.Request.Path.StartsWithSegments("/lst/api/logger/logs")) context.Request.Path.StartsWithSegments("/lst/api/logger/logs"))
{ {
Console.WriteLine("WebSocket request received!");
try try
{ {
var backendUri = new UriBuilder("ws", "localhost", 8080) var backendUri = new UriBuilder("ws", "localhost", 8080)

View File

@@ -9,6 +9,10 @@
<!-- Proxy all requests starting with /lst/ to the .NET wrapper (port 4000) --> <!-- Proxy all requests starting with /lst/ to the .NET wrapper (port 4000) -->
<rule name="Proxy to Wrapper" stopProcessing="true"> <rule name="Proxy to Wrapper" stopProcessing="true">
<match url="^lst/(.*)" /> <match url="^lst/(.*)" />
<conditions>
<!-- Skip this rule if it's a WebSocket request -->
<add input="{HTTP_UPGRADE}" pattern="^WebSocket$" negate="true" />
</conditions>
<action type="Rewrite" url="http://localhost:8080/{R:1}" /> <action type="Rewrite" url="http://localhost:8080/{R:1}" />
</rule> </rule>
</rules> </rules>

View File

@@ -1,4 +1,4 @@
package logging package loggingx
import ( import (
"encoding/json" "encoding/json"

View File

@@ -1,4 +1,4 @@
package logging package loggingx
import ( import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"

View File

@@ -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{}{

View 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})
}

View File

@@ -0,0 +1 @@
package system

View File

@@ -0,0 +1 @@
package system

View 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()
}()
}
}
}

View 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)
}

View 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)
}
}

View 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 its 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)
}

View File

@@ -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

View File

@@ -24,9 +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"
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() {
@@ -109,11 +111,18 @@ 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)
r.Any(basePath+"/api", errorApiLoc) r.Any(basePath+"/api", errorApiLoc)
r.Run(":8080") // get the server port
port := "8080"
if os.Getenv("VITE_SERVER_PORT") != "" {
port = os.Getenv("VITE_SERVER_PORT")
}
r.Run(":" + port)
} }
// func serveViteApp(c *gin.Context) { // func serveViteApp(c *gin.Context) {

View 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
}

View File

@@ -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
} }

View File

@@ -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
} }

View 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"`
}

View 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)
}

View File

@@ -1,35 +1,41 @@
import { useState } from 'react' import { useState } from "react";
import reactLogo from './assets/react.svg' import reactLogo from "./assets/react.svg";
import viteLogo from '/vite.svg' import viteLogo from "/vite.svg";
import './App.css' import "./App.css";
import WebSocketViewer from "./WebSocketTest";
function App() { function App() {
const [count, setCount] = useState(0) const [count, setCount] = useState(0);
return ( return (
<> <>
<div> <div>
<a href="https://vite.dev" target="_blank"> <a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" /> <img src={viteLogo} className="logo" alt="Vite logo" />
</a> </a>
<a href="https://react.dev" target="_blank"> <a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" /> <img
</a> src={reactLogo}
</div> className="logo react"
<h1>Vite + React</h1> alt="React logo"
<div className="card"> />
<button onClick={() => setCount((count) => count + 1)}> </a>
count is {count} </div>
</button> <h1>Vite + React</h1>
<p> <div className="card">
Edit <code>src/App.tsx</code> and save to test HMR <button onClick={() => setCount((count) => count + 1)}>
</p> count is {count}
</div> </button>
<p className="read-the-docs"> <p>
Click on the Vite and React logos to learn more Edit <code>src/App.tsx</code> and save to test HMR
</p> </p>
</> </div>
) <p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
<WebSocketViewer />
</>
);
} }
export default App export default App;

View File

@@ -0,0 +1,41 @@
import { useEffect, useRef } from "react";
const WebSocketViewer = () => {
const ws = useRef<any>(null);
useEffect(() => {
// Connect to your Go backend WebSocket endpoint
ws.current = new WebSocket(
(window.location.protocol === "https:" ? "wss://" : "ws://") +
window.location.host +
"/lst/api/logger/logs"
);
ws.current.onopen = () => {
console.log("[WebSocket] Connected");
};
ws.current.onmessage = (event: any) => {
console.log("[WebSocket] Message received:", event.data);
};
ws.current.onerror = (error: any) => {
console.error("[WebSocket] Error:", error);
};
ws.current.onclose = () => {
console.log("[WebSocket] Disconnected");
};
// Cleanup on unmount
return () => {
if (ws.current) {
ws.current.close();
}
};
}, []);
return <div>Check the console for WebSocket messages</div>;
};
export default WebSocketViewer;

View File

@@ -94,9 +94,10 @@ function Update-BuildNumber {
Push-Location $rootDir/backend Push-Location $rootDir/backend
Write-Host "Building the app" Write-Host "Building the app"
go get go get
swag init -o swagger -g main.go # swag init -o swagger -g main.go
go build -ldflags "-X main.version=$($version)-$($initialBuildValue)" -o lst_app.exe ./main.go go build -ldflags "-X main.version=$($version)-$($initialBuildValue)" -o lst_app.exe ./main.go
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Warning "app build failed!" Write-Warning "app build failed!"
@@ -124,6 +125,22 @@ function Update-BuildNumber {
Write-Host "Building wrapper" Write-Host "Building wrapper"
Push-Location $rootDir/LstWrapper Push-Location $rootDir/LstWrapper
Write-Host "Changing the port to match the server port in the env file"
$port = $env:VITE_SERVER_PORT
if (-not $port) {
$port = "8080" # Default port if env var not set
}
$webConfigPath = "web.config"
$content = Get-Content -Path $webConfigPath -Raw
$newContent = $content -replace '(?<=Rewrite" url="http://localhost:)\d+(?=/\{R:1\}")', $port
$newContent | Set-Content -Path $webConfigPath -NoNewline
Write-Host "Updated web.config rewrite port to $port"
#remove the publish folder as we done need it #remove the publish folder as we done need it
if (-not (Test-Path "publish")) { if (-not (Test-Path "publish")) {
Write-Host "The publish folder is already deleted nothing else to do" Write-Host "The publish folder is already deleted nothing else to do"
@@ -133,6 +150,15 @@ function Update-BuildNumber {
dotnet publish -c Release -o ./publish dotnet publish -c Release -o ./publish
$webConfigPath = "web.config"
$content = Get-Content -Path $webConfigPath -Raw
$newContent = $content -replace '(?<=Rewrite" url="http://localhost:)\d+(?=/\{R:1\}")', "8080"
$newContent | Set-Content -Path $webConfigPath -NoNewline
Write-Host "Updated web.config rewrite port back to 8080"
Pop-Location Pop-Location
Write-Host "Building Docs" Write-Host "Building Docs"

View File

@@ -14,4 +14,5 @@ docker push git.tuffraid.net/cowch/logistics_support_tool:latest
Write-Host "Pull the new images to our docker system" Write-Host "Pull the new images to our docker system"
docker compose -f ./docker-compose.yml up -d --force-recreate docker compose -f ./docker-compose.yml up -d --force-recreate
# in case we get logged out docker login git.tuffraid.net # in case we get logged out docker login git.tuffraid.net
# create a docker network so we have this for us docker network create -d bridge my-bridge-network