From 1f41f6003bf19504e854d4cd6decc5a3354ab76d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Thu, 29 May 2025 00:21:32 +0200 Subject: [PATCH] update logging --- local/model/api.go | 17 +++++- local/model/server.go | 11 ++-- local/model/stateHistory.go | 1 + local/service/config.go | 24 ++++++-- local/service/server.go | 98 ++++++++++++++++++++++++++------ local/utl/common/common.go | 65 ++++++++++++--------- local/utl/tracking/log_tailer.go | 78 +++++++++++++++++++++++++ 7 files changed, 238 insertions(+), 56 deletions(-) create mode 100644 local/utl/tracking/log_tailer.go diff --git a/local/model/api.go b/local/model/api.go index 1629aed..26a4eda 100644 --- a/local/model/api.go +++ b/local/model/api.go @@ -3,6 +3,7 @@ package model import ( "database/sql/driver" "fmt" + "strconv" ) type ServiceStatus int @@ -54,14 +55,24 @@ func ParseServiceStatus(s string) ServiceStatus { // MarshalJSON implements json.Marshaler interface func (s ServiceStatus) MarshalJSON() ([]byte, error) { - return []byte(`"` + s.String() + `"`), nil + // Return the numeric value instead of string + return []byte(strconv.Itoa(int(s))), nil } // UnmarshalJSON implements json.Unmarshaler interface func (s *ServiceStatus) UnmarshalJSON(data []byte) error { + // Try to parse as number first + if i, err := strconv.Atoi(string(data)); err == nil { + *s = ServiceStatus(i) + return nil + } + + // Fallback to string parsing for backward compatibility str := string(data) - // Remove quotes - str = str[1 : len(str)-1] + if len(str) >= 2 { + // Remove quotes if present + str = str[1 : len(str)-1] + } *s = ParseServiceStatus(str) return nil } diff --git a/local/model/server.go b/local/model/server.go index 198b4a9..ef591cd 100644 --- a/local/model/server.go +++ b/local/model/server.go @@ -41,11 +41,12 @@ type State struct { type ServerState struct { sync.RWMutex - Session string `json:"session"` - SessionStart time.Time `json:"sessionStart"` - PlayerCount int `json:"playerCount"` - Track string `json:"track"` - MaxConnections int `json:"maxConnections"` + Session string `json:"session"` + SessionStart time.Time `json:"sessionStart"` + PlayerCount int `json:"playerCount"` + Track string `json:"track"` + MaxConnections int `json:"maxConnections"` + SessionDurationMinutes int `json:"sessionDurationMinutes"` // Players map[int]*PlayerState // etc. } \ No newline at end of file diff --git a/local/model/stateHistory.go b/local/model/stateHistory.go index ee008ff..a244b9b 100644 --- a/local/model/stateHistory.go +++ b/local/model/stateHistory.go @@ -56,4 +56,5 @@ type StateHistory struct { Session string `json:"session"` PlayerCount int `json:"playerCount"` DateCreated time.Time `json:"dateCreated"` + SessionDurationMinutes int `json:"sessionDurationMinutes"` } \ No newline at end of file diff --git a/local/service/config.go b/local/service/config.go index 28edc94..9dd5807 100644 --- a/local/service/config.go +++ b/local/service/config.go @@ -87,19 +87,32 @@ func (as *ConfigService) SetServerService(serverService *ServerService) { func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) { serverID := ctx.Locals("serverId").(int) configFile := ctx.Params("file") - override := ctx.QueryBool("override") + override := ctx.QueryBool("override", false) server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID) - if err != nil { - return nil, err + log.Print("Server not found") + return nil, fiber.NewError(404, "Server not found") } // Read existing config configPath := filepath.Join(server.ConfigPath, "\\server\\cfg", configFile) oldData, err := os.ReadFile(configPath) if err != nil { - return nil, err + if os.IsNotExist(err) { + // Create directory if it doesn't exist + dir := filepath.Dir(configPath) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + // Create empty JSON file + if err := os.WriteFile(configPath, []byte("{}"), 0644); err != nil { + return nil, err + } + oldData = []byte("{}") + } else { + return nil, err + } } oldDataUTF8, err := DecodeUTF16LEBOM(oldData) @@ -124,12 +137,11 @@ func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{ return nil, err } - newDataUTF16, err := EncodeUTF16LEBOM(newData) if err != nil { return nil, err } - + context := ctx.UserContext() if err := os.WriteFile(configPath, newDataUTF16, 0644); err != nil { diff --git a/local/service/server.go b/local/service/server.go index 1f1c8cd..8cdd9e0 100644 --- a/local/service/server.go +++ b/local/service/server.go @@ -21,6 +21,8 @@ type ServerService struct { configService *ConfigService lastInsertTimes sync.Map // Track last insert time per server debouncers sync.Map // Track debounce timers per server + logTailers sync.Map // Track log tailers per server + sessionCache sync.Map // Cache of server event sessions } type pendingState struct { @@ -28,26 +30,43 @@ type pendingState struct { state *model.ServerState } +func (s *ServerService) ensureLogTailing(server *model.Server, instance *tracking.AccServerInstance) { + // Check if we already have a tailer + if _, exists := s.logTailers.Load(server.ID); exists { + return + } + + // Start tailing in a goroutine that handles file creation/deletion + go func() { + logPath := filepath.Join(server.ConfigPath, "\\server\\log\\server.log") + tailer := tracking.NewLogTailer(logPath, instance.HandleLogLine) + s.logTailers.Store(server.ID, tailer) + + // Start tailing and automatically handle file changes + tailer.Start() + }() +} + func NewServerService(repository *repository.ServerRepository, stateHistoryRepo *repository.StateHistoryRepository, apiService *ApiService, configService *ConfigService) *ServerService { service := &ServerService{ - repository: repository, - apiService: apiService, - configService: configService, + repository: repository, + apiService: apiService, + configService: configService, stateHistoryRepo: stateHistoryRepo, } + + // Initialize instances for all servers servers, err := repository.GetAll(context.Background(), &model.ServerFilter{}) if err != nil { log.Print(err.Error()) + return service } + for _, server := range *servers { - status, err := service.apiService.StatusServer(server.ServiceName) - if err != nil { - log.Print(err.Error()) - } - if (status == string(model.StatusRunning)) { - service.StartAccServerRuntime(&server) - } + // Initialize instance regardless of status + service.StartAccServerRuntime(&server) } + return service } @@ -77,10 +96,44 @@ func (s *ServerService) insertStateHistory(serverID uint, state *model.ServerSta Session: state.Session, PlayerCount: state.PlayerCount, DateCreated: time.Now().UTC(), + SessionDurationMinutes: state.SessionDurationMinutes, }) } +func (s *ServerService) updateSessionDuration(server *model.Server, sessionType string) { + sessionsInterface, exists := s.sessionCache.Load(server.ID) + if !exists { + // 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) + return + } + evt := event.(model.EventConfig) + s.sessionCache.Store(server.ID, evt.Sessions) + sessionsInterface = evt.Sessions + } + + sessions := sessionsInterface.([]model.Session) + if (sessionType == "" && len(sessions) > 0) { + sessionType = sessions[0].SessionType + } + for _, session := range sessions { + if session.SessionType == sessionType { + if instance, ok := s.instances.Load(server.ID); ok { + serverInstance := instance.(*tracking.AccServerInstance) + serverInstance.State.SessionDurationMinutes = session.SessionDurationMinutes.ToInt() + serverInstance.State.Session = sessionType + } + break + } + } +} + func (s *ServerService) handleStateChange(server *model.Server, state *model.ServerState) { + // Update session duration when session changes + s.updateSessionDuration(server, state.Session) + // Cancel existing timer if any if debouncer, exists := s.debouncers.Load(server.ID); exists { pending := debouncer.(*pendingState) @@ -111,10 +164,18 @@ func (s *ServerService) handleStateChange(server *model.Server, state *model.Ser } func (s *ServerService) StartAccServerRuntime(server *model.Server) { - s.instances.Delete(server.ID) - instance := tracking.NewAccServerInstance(server, func(state *model.ServerState, states ...tracking.StateChange) { - s.handleStateChange(server, state) - }) + // Get or create instance + instanceInterface, exists := s.instances.Load(server.ID) + var instance *tracking.AccServerInstance + if !exists { + instance = tracking.NewAccServerInstance(server, func(state *model.ServerState, states ...tracking.StateChange) { + s.handleStateChange(server, state) + }) + s.instances.Store(server.ID, instance) + } else { + instance = instanceInterface.(*tracking.AccServerInstance) + } + config, _ := DecodeFileName(ConfigurationJson)(server.ConfigPath) cfg := config.(model.Configuration) event, _ := DecodeFileName(EventJson)(server.ConfigPath) @@ -123,8 +184,13 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) { instance.State.MaxConnections = cfg.MaxConnections.ToInt() instance.State.Track = evt.Track - go tracking.TailLogFile(filepath.Join(server.ConfigPath, "\\server\\log\\server.log"), instance.HandleLogLine) - s.instances.Store(server.ID, instance) + // Cache sessions for duration lookup + s.sessionCache.Store(server.ID, evt.Sessions) + + s.updateSessionDuration(server, instance.State.Session) + + // Ensure log tailing is running (regardless of server status) + s.ensureLogTailing(server, instance) } // GetAll diff --git a/local/utl/common/common.go b/local/utl/common/common.go index 626f6bd..e969459 100644 --- a/local/utl/common/common.go +++ b/local/utl/common/common.go @@ -101,40 +101,53 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error { elem := val.Elem() typ := elem.Type() - for i := 0; i < elem.NumField(); i++ { - field := elem.Field(i) - fieldType := typ.Field(i) + // Process all fields including embedded structs + var processFields func(reflect.Value, reflect.Type) error + processFields = func(val reflect.Value, typ reflect.Type) error { + for i := 0; i < val.NumField(); i++ { + field := val.Field(i) + fieldType := typ.Field(i) - // Skip if field cannot be set - if !field.CanSet() { - continue - } - - // Check for param tag first (path parameters) - if paramName := fieldType.Tag.Get("param"); paramName != "" { - if err := parsePathParam(c, field, paramName); err != nil { - return fmt.Errorf("error parsing path parameter %s: %v", paramName, err) + // Handle embedded structs recursively + if fieldType.Anonymous { + if err := processFields(field, fieldType.Type); err != nil { + return err + } + continue } - continue - } - // Then check for query tag - queryName := fieldType.Tag.Get("query") - if queryName == "" { - queryName = ToSnakeCase(fieldType.Name) // Default to snake_case of field name - } + // Skip if field cannot be set + if !field.CanSet() { + continue + } - queryVal := c.Query(queryName) - if queryVal == "" { - continue // Skip empty values - } + // Check for param tag first (path parameters) + if paramName := fieldType.Tag.Get("param"); paramName != "" { + if err := parsePathParam(c, field, paramName); err != nil { + return fmt.Errorf("error parsing path parameter %s: %v", paramName, err) + } + continue + } - if err := parseValue(field, queryVal, fieldType.Tag); err != nil { - return fmt.Errorf("error parsing query parameter %s: %v", queryName, err) + // Then check for query tag + queryName := fieldType.Tag.Get("query") + if queryName == "" { + queryName = ToSnakeCase(fieldType.Name) // Default to snake_case of field name + } + + queryVal := c.Query(queryName) + if queryVal == "" { + continue // Skip empty values + } + + if err := parseValue(field, queryVal, fieldType.Tag); err != nil { + return fmt.Errorf("error parsing query parameter %s: %v", queryName, err) + } } + return nil } - return nil + return processFields(elem, typ) } func parsePathParam(c *fiber.Ctx, field reflect.Value, paramName string) error { diff --git a/local/utl/tracking/log_tailer.go b/local/utl/tracking/log_tailer.go new file mode 100644 index 0000000..422119c --- /dev/null +++ b/local/utl/tracking/log_tailer.go @@ -0,0 +1,78 @@ +package tracking + +import ( + "bufio" + "os" + "time" +) + +type LogTailer struct { + filePath string + handleLine func(string) + stopChan chan struct{} + isRunning bool +} + +func NewLogTailer(filePath string, handleLine func(string)) *LogTailer { + return &LogTailer{ + filePath: filePath, + handleLine: handleLine, + stopChan: make(chan struct{}), + } +} + +func (t *LogTailer) Start() { + if t.isRunning { + return + } + t.isRunning = true + + go func() { + var lastSize int64 = 0 + for { + select { + case <-t.stopChan: + t.isRunning = false + return + default: + // Try to open and read the file + if file, err := os.Open(t.filePath); err == nil { + stat, err := file.Stat() + if err != nil { + file.Close() + time.Sleep(time.Second) + continue + } + + // If file was truncated, start from beginning + if stat.Size() < lastSize { + lastSize = 0 + } + + // Seek to last read position + if lastSize > 0 { + file.Seek(lastSize, 0) + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + t.handleLine(scanner.Text()) + lastSize, _ = file.Seek(0, 1) // Get current position + } + + file.Close() + } + + // Wait before next attempt + time.Sleep(time.Second) + } + } + }() +} + +func (t *LogTailer) Stop() { + if !t.isRunning { + return + } + close(t.stopChan) +} \ No newline at end of file