diff --git a/local/api/api.go b/local/api/api.go index 0af6f11..0b2ccca 100644 --- a/local/api/api.go +++ b/local/api/api.go @@ -4,6 +4,7 @@ import ( "acc-server-manager/local/controller" "acc-server-manager/local/utl/common" "acc-server-manager/local/utl/configs" + "acc-server-manager/local/utl/logging" "os" "github.com/gofiber/fiber/v2" @@ -40,13 +41,13 @@ func Init(di *dig.Container, app *fiber.App) { return routeGroups }) if err != nil { - panic("unable to bind routes") + logging.Panic("unable to bind routes") } err = di.Provide(func() *dig.Container { return di }) if err != nil { - panic("unable to bind dig") + logging.Panic("unable to bind dig") } controller.InitializeControllers(di) diff --git a/local/model/filter.go b/local/model/filter.go index 5255edc..ffa55f9 100644 --- a/local/model/filter.go +++ b/local/model/filter.go @@ -1,6 +1,8 @@ package model -import "time" +import ( + "time" +) // BaseFilter contains common filter fields that can be embedded in other filters type BaseFilter struct { @@ -21,15 +23,6 @@ type ServerBasedFilter struct { ServerID int `param:"id"` } -// ServerFilter defines filtering options for Server queries -type ServerFilter struct { - BaseFilter - ServerBasedFilter - Name string `query:"name"` - ServiceName string `query:"service_name"` - Status string `query:"status"` -} - // ConfigFilter defines filtering options for Config queries type ConfigFilter struct { BaseFilter diff --git a/local/model/server.go b/local/model/server.go index ef591cd..8029a34 100644 --- a/local/model/server.go +++ b/local/model/server.go @@ -3,6 +3,8 @@ package model import ( "sync" "time" + + "gorm.io/gorm" ) // Server represents an ACC server instance @@ -49,4 +51,23 @@ type ServerState struct { SessionDurationMinutes int `json:"sessionDurationMinutes"` // Players map[int]*PlayerState // etc. +} + +// ServerFilter defines filtering options for Server queries +type ServerFilter struct { + BaseFilter + ServerBasedFilter + Name string `query:"name"` + ServiceName string `query:"service_name"` + Status string `query:"status"` +} + +// ApplyFilter implements the Filterable interface +func (f *ServerFilter) ApplyFilter(query *gorm.DB) *gorm.DB { + // Apply server filter + if f.ServerID != 0 { + query = query.Where("id = ?", f.ServerID) + } + + return query } \ No newline at end of file diff --git a/local/model/stateHistory.go b/local/model/stateHistory.go index a244b9b..667a53f 100644 --- a/local/model/stateHistory.go +++ b/local/model/stateHistory.go @@ -8,7 +8,6 @@ import ( // StateHistoryFilter combines common filter capabilities type StateHistoryFilter struct { - BaseFilter // Adds pagination and sorting ServerBasedFilter // Adds server ID from path parameter DateRangeFilter // Adds date range filtering diff --git a/local/service/server.go b/local/service/server.go index 8cdd9e0..b81b46c 100644 --- a/local/service/server.go +++ b/local/service/server.go @@ -3,6 +3,7 @@ package service import ( "acc-server-manager/local/model" "acc-server-manager/local/repository" + "acc-server-manager/local/utl/logging" "acc-server-manager/local/utl/tracking" "context" "log" @@ -58,7 +59,7 @@ func NewServerService(repository *repository.ServerRepository, stateHistoryRepo // Initialize instances for all servers servers, err := repository.GetAll(context.Background(), &model.ServerFilter{}) if err != nil { - log.Print(err.Error()) + logging.Error("Failed to get servers: %v", err) return service } @@ -106,7 +107,7 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType // Try to load sessions from config event, err := DecodeFileName(EventJson)(server.ConfigPath) if err != nil { - log.Printf("Failed to load event config for server %d: %v", server.ID, err) + logging.Error("Failed to load event config for server %d: %v", server.ID, err) return } evt := event.(model.EventConfig) @@ -115,7 +116,7 @@ func (s *ServerService) updateSessionDuration(server *model.Server, sessionType } sessions := sessionsInterface.([]model.Session) - if (sessionType == "" && len(sessions) > 0) { + if sessionType == "" && len(sessions) > 0 { sessionType = sessions[0].SessionType } for _, session := range sessions { @@ -200,24 +201,25 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) { // context.Context: Application context // Returns: // string: Application version -func (as ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]model.Server, error) { - servers, err := as.repository.GetAll(ctx.UserContext(), filter) +func (s ServerService) GetAll(ctx *fiber.Ctx, filter *model.ServerFilter) (*[]model.Server, error) { + servers, err := s.repository.GetAll(ctx.UserContext(), filter) if err != nil { + logging.Error("Failed to get servers: %v", err) return nil, err } for i, server := range *servers { - status, err := as.apiService.StatusServer(server.ServiceName) + status, err := s.apiService.StatusServer(server.ServiceName) if err != nil { - log.Print(err.Error()) + logging.Error("Failed to get status for server %s: %v", server.ServiceName, err) } (*servers)[i].Status = model.ParseServiceStatus(status) - instance, ok := as.instances.Load(server.ID) + instance, ok := s.instances.Load(server.ID) if !ok { - log.Print("Unable to retrieve instance for server of ID: ", server.ID) + logging.Warn("No instance found for server ID: %d", server.ID) } else { serverInstance := instance.(*tracking.AccServerInstance) - if (serverInstance.State != nil) { + if serverInstance.State != nil { (*servers)[i].State = *serverInstance.State } } diff --git a/local/utl/common/common.go b/local/utl/common/common.go index e969459..39b5b04 100644 --- a/local/utl/common/common.go +++ b/local/utl/common/common.go @@ -155,12 +155,18 @@ func parsePathParam(c *fiber.Ctx, field reflect.Value, paramName string) error { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: val, err := c.ParamsInt(paramName) if err != nil { + if strings.Contains(err.Error(), "strconv.Atoi: parsing \"\": invalid syntax") { + return nil + } return err } field.SetInt(int64(val)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: val, err := c.ParamsInt(paramName) if err != nil { + if strings.Contains(err.Error(), "strconv.Atoi: parsing \"\": invalid syntax") { + return nil + } return err } field.SetUint(uint64(val)) diff --git a/local/utl/db/db.go b/local/utl/db/db.go index 994b07b..6e34275 100644 --- a/local/utl/db/db.go +++ b/local/utl/db/db.go @@ -2,6 +2,7 @@ package db import ( "acc-server-manager/local/model" + "acc-server-manager/local/utl/logging" "go.uber.org/dig" "gorm.io/driver/sqlite" @@ -11,13 +12,13 @@ import ( func Start(di *dig.Container) { db, err := gorm.Open(sqlite.Open("acc.db"), &gorm.Config{}) if err != nil { - panic("failed to connect database") + logging.Panic("failed to connect database") } err = di.Provide(func() *gorm.DB { return db }) if err != nil { - panic("failed to bind database") + logging.Panic("failed to bind database") } Migrate(db) } @@ -25,39 +26,39 @@ func Start(di *dig.Container) { func Migrate(db *gorm.DB) { err := db.AutoMigrate(&model.ApiModel{}) if err != nil { - panic("failed to migrate model.ApiModel") + logging.Panic("failed to migrate model.ApiModel") } err = db.AutoMigrate(&model.Server{}) if err != nil { - panic("failed to migrate model.Server") + logging.Panic("failed to migrate model.Server") } err = db.AutoMigrate(&model.Config{}) if err != nil { - panic("failed to migrate model.Config") + logging.Panic("failed to migrate model.Config") } err = db.AutoMigrate(&model.Track{}) if err != nil { - panic("failed to migrate model.Track") + logging.Panic("failed to migrate model.Track") } err = db.AutoMigrate(&model.CarModel{}) if err != nil { - panic("failed to migrate model.CarModel") + logging.Panic("failed to migrate model.CarModel") } err = db.AutoMigrate(&model.CupCategory{}) if err != nil { - panic("failed to migrate model.CupCategory") + logging.Panic("failed to migrate model.CupCategory") } err = db.AutoMigrate(&model.DriverCategory{}) if err != nil { - panic("failed to migrate model.DriverCategory") + logging.Panic("failed to migrate model.DriverCategory") } err = db.AutoMigrate(&model.SessionType{}) if err != nil { - panic("failed to migrate model.SessionType") + logging.Panic("failed to migrate model.SessionType") } err = db.AutoMigrate(&model.StateHistory{}) if err != nil { - panic("failed to migrate model.StateHistory") + logging.Panic("failed to migrate model.StateHistory") } db.FirstOrCreate(&model.ApiModel{Api: "Works"}) diff --git a/local/utl/logging/logger.go b/local/utl/logging/logger.go new file mode 100644 index 0000000..245cde8 --- /dev/null +++ b/local/utl/logging/logger.go @@ -0,0 +1,149 @@ +package logging + +import ( + "fmt" + "io" + "log" + "os" + "path/filepath" + "runtime" + "sync" + "time" +) + +var ( + logger *Logger + once sync.Once + timeFormat = "2006-01-02 15:04:05.000" +) + +type Logger struct { + file *os.File + logger *log.Logger +} + +// Initialize creates or gets the singleton logger instance +func Initialize() (*Logger, error) { + var err error + once.Do(func() { + logger, err = newLogger() + }) + return logger, err +} + +func newLogger() (*Logger, error) { + // Ensure logs directory exists + if err := os.MkdirAll("logs", 0755); err != nil { + return nil, fmt.Errorf("failed to create logs directory: %v", err) + } + + // Open log file with date in name + logPath := filepath.Join("logs", fmt.Sprintf("acc-server-%s.log", time.Now().Format("2006-01-02"))) + file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return nil, fmt.Errorf("failed to open log file: %v", err) + } + + // Create multi-writer for both file and console + multiWriter := io.MultiWriter(file, os.Stdout) + + // Create logger with custom prefix + logger := &Logger{ + file: file, + logger: log.New(multiWriter, "", 0), + } + + return logger, nil +} + +func (l *Logger) Close() error { + if l.file != nil { + return l.file.Close() + } + return nil +} + +func (l *Logger) log(level, format string, v ...interface{}) { + // Get caller info + _, file, line, _ := runtime.Caller(2) + file = filepath.Base(file) + + // Format message + msg := fmt.Sprintf(format, v...) + + // Format final log line + logLine := fmt.Sprintf("[%s] [%s] [%s:%d] %s", + time.Now().Format(timeFormat), + level, + file, + line, + msg, + ) + + l.logger.Println(logLine) +} + +func (l *Logger) Info(format string, v ...interface{}) { + l.log("INFO", format, v...) +} + +func (l *Logger) Error(format string, v ...interface{}) { + l.log("ERROR", format, v...) +} + +func (l *Logger) Warn(format string, v ...interface{}) { + l.log("WARN", format, v...) +} + +func (l *Logger) Debug(format string, v ...interface{}) { + l.log("DEBUG", format, v...) +} + +func (l *Logger) Panic(format string) { + l.Panic("PANIC " + format) +} + +// Global convenience functions +func Info(format string, v ...interface{}) { + if logger != nil { + logger.Info(format, v...) + } +} + +func Error(format string, v ...interface{}) { + if logger != nil { + logger.Error(format, v...) + } +} + +func Warn(format string, v ...interface{}) { + if logger != nil { + logger.Warn(format, v...) + } +} + +func Debug(format string, v ...interface{}) { + if logger != nil { + logger.Debug(format, v...) + } +} + +func Panic(format string) { + if logger != nil { + logger.Panic(format) + } +} + +// RecoverAndLog recovers from panics and logs them +func RecoverAndLog() { + if r := recover(); r != nil { + if logger != nil { + // Get stack trace + buf := make([]byte, 4096) + n := runtime.Stack(buf, false) + stackTrace := string(buf[:n]) + + logger.log("PANIC", "Recovered from panic: %v\nStack Trace:\n%s", r, stackTrace) + } + } +} \ No newline at end of file diff --git a/local/utl/server/server.go b/local/utl/server/server.go index 7316215..45339f6 100644 --- a/local/utl/server/server.go +++ b/local/utl/server/server.go @@ -2,9 +2,8 @@ package server import ( "acc-server-manager/local/api" - "acc-server-manager/local/utl/common" + "acc-server-manager/local/utl/logging" "fmt" - "log" "os" "github.com/gofiber/fiber/v2" @@ -14,6 +13,17 @@ import ( ) func Start(di *dig.Container) *fiber.App { + // Initialize logger + logger, err := logging.Initialize() + if err != nil { + fmt.Printf("Failed to initialize logger: %v\n", err) + os.Exit(1) + } + defer logger.Close() + + // Set up panic recovery + defer logging.RecoverAndLog() + app := fiber.New(fiber.Config{ EnablePrintRoutes: true, }) @@ -22,22 +32,22 @@ func Start(di *dig.Container) *fiber.App { app.Get("/swagger/*", swagger.HandlerDefault) - file, err := os.OpenFile("logs.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) - if err != nil { - log.Print("Cannot open file logs.log") - } - log.SetOutput(file) - api.Init(di, app) app.Get("/ping", func(c *fiber.Ctx) error { return c.SendString("pong") }) + port := os.Getenv("PORT") - err = app.Listen(":" + port) - if err != nil { - msg := fmt.Sprintf("Running on %s:%s", common.GetIP(), port) - println(msg) + if port == "" { + port = "3000" // Default port } + + logging.Info("Starting server on port %s", port) + if err := app.Listen(":" + port); err != nil { + logging.Error("Failed to start server: %v", err) + os.Exit(1) + } + return app }