update logging

This commit is contained in:
Fran Jurmanović
2025-05-29 00:21:32 +02:00
parent 3dfbe77219
commit 1f41f6003b
7 changed files with 238 additions and 56 deletions

View File

@@ -3,6 +3,7 @@ package model
import ( import (
"database/sql/driver" "database/sql/driver"
"fmt" "fmt"
"strconv"
) )
type ServiceStatus int type ServiceStatus int
@@ -54,14 +55,24 @@ func ParseServiceStatus(s string) ServiceStatus {
// MarshalJSON implements json.Marshaler interface // MarshalJSON implements json.Marshaler interface
func (s ServiceStatus) MarshalJSON() ([]byte, error) { 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 // UnmarshalJSON implements json.Unmarshaler interface
func (s *ServiceStatus) UnmarshalJSON(data []byte) error { 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) str := string(data)
// Remove quotes if len(str) >= 2 {
str = str[1 : len(str)-1] // Remove quotes if present
str = str[1 : len(str)-1]
}
*s = ParseServiceStatus(str) *s = ParseServiceStatus(str)
return nil return nil
} }

View File

@@ -41,11 +41,12 @@ type State struct {
type ServerState struct { type ServerState struct {
sync.RWMutex sync.RWMutex
Session string `json:"session"` Session string `json:"session"`
SessionStart time.Time `json:"sessionStart"` SessionStart time.Time `json:"sessionStart"`
PlayerCount int `json:"playerCount"` PlayerCount int `json:"playerCount"`
Track string `json:"track"` Track string `json:"track"`
MaxConnections int `json:"maxConnections"` MaxConnections int `json:"maxConnections"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
// Players map[int]*PlayerState // Players map[int]*PlayerState
// etc. // etc.
} }

View File

@@ -56,4 +56,5 @@ type StateHistory struct {
Session string `json:"session"` Session string `json:"session"`
PlayerCount int `json:"playerCount"` PlayerCount int `json:"playerCount"`
DateCreated time.Time `json:"dateCreated"` DateCreated time.Time `json:"dateCreated"`
SessionDurationMinutes int `json:"sessionDurationMinutes"`
} }

View File

@@ -87,19 +87,32 @@ func (as *ConfigService) SetServerService(serverService *ServerService) {
func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) { func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{}) (*model.Config, error) {
serverID := ctx.Locals("serverId").(int) serverID := ctx.Locals("serverId").(int)
configFile := ctx.Params("file") configFile := ctx.Params("file")
override := ctx.QueryBool("override") override := ctx.QueryBool("override", false)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID) server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
if err != nil { if err != nil {
return nil, err log.Print("Server not found")
return nil, fiber.NewError(404, "Server not found")
} }
// Read existing config // Read existing config
configPath := filepath.Join(server.ConfigPath, "\\server\\cfg", configFile) configPath := filepath.Join(server.ConfigPath, "\\server\\cfg", configFile)
oldData, err := os.ReadFile(configPath) oldData, err := os.ReadFile(configPath)
if err != nil { 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) oldDataUTF8, err := DecodeUTF16LEBOM(oldData)
@@ -124,12 +137,11 @@ func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{
return nil, err return nil, err
} }
newDataUTF16, err := EncodeUTF16LEBOM(newData) newDataUTF16, err := EncodeUTF16LEBOM(newData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
context := ctx.UserContext() context := ctx.UserContext()
if err := os.WriteFile(configPath, newDataUTF16, 0644); err != nil { if err := os.WriteFile(configPath, newDataUTF16, 0644); err != nil {

View File

@@ -21,6 +21,8 @@ type ServerService struct {
configService *ConfigService configService *ConfigService
lastInsertTimes sync.Map // Track last insert time per server lastInsertTimes sync.Map // Track last insert time per server
debouncers sync.Map // Track debounce timers 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 { type pendingState struct {
@@ -28,26 +30,43 @@ type pendingState struct {
state *model.ServerState 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 { func NewServerService(repository *repository.ServerRepository, stateHistoryRepo *repository.StateHistoryRepository, apiService *ApiService, configService *ConfigService) *ServerService {
service := &ServerService{ service := &ServerService{
repository: repository, repository: repository,
apiService: apiService, apiService: apiService,
configService: configService, configService: configService,
stateHistoryRepo: stateHistoryRepo, stateHistoryRepo: stateHistoryRepo,
} }
// Initialize instances for all servers
servers, err := repository.GetAll(context.Background(), &model.ServerFilter{}) servers, err := repository.GetAll(context.Background(), &model.ServerFilter{})
if err != nil { if err != nil {
log.Print(err.Error()) log.Print(err.Error())
return service
} }
for _, server := range *servers { for _, server := range *servers {
status, err := service.apiService.StatusServer(server.ServiceName) // Initialize instance regardless of status
if err != nil { service.StartAccServerRuntime(&server)
log.Print(err.Error())
}
if (status == string(model.StatusRunning)) {
service.StartAccServerRuntime(&server)
}
} }
return service return service
} }
@@ -77,10 +96,44 @@ func (s *ServerService) insertStateHistory(serverID uint, state *model.ServerSta
Session: state.Session, Session: state.Session,
PlayerCount: state.PlayerCount, PlayerCount: state.PlayerCount,
DateCreated: time.Now().UTC(), 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) { 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 // Cancel existing timer if any
if debouncer, exists := s.debouncers.Load(server.ID); exists { if debouncer, exists := s.debouncers.Load(server.ID); exists {
pending := debouncer.(*pendingState) pending := debouncer.(*pendingState)
@@ -111,10 +164,18 @@ func (s *ServerService) handleStateChange(server *model.Server, state *model.Ser
} }
func (s *ServerService) StartAccServerRuntime(server *model.Server) { func (s *ServerService) StartAccServerRuntime(server *model.Server) {
s.instances.Delete(server.ID) // Get or create instance
instance := tracking.NewAccServerInstance(server, func(state *model.ServerState, states ...tracking.StateChange) { instanceInterface, exists := s.instances.Load(server.ID)
s.handleStateChange(server, state) 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) config, _ := DecodeFileName(ConfigurationJson)(server.ConfigPath)
cfg := config.(model.Configuration) cfg := config.(model.Configuration)
event, _ := DecodeFileName(EventJson)(server.ConfigPath) event, _ := DecodeFileName(EventJson)(server.ConfigPath)
@@ -123,8 +184,13 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) {
instance.State.MaxConnections = cfg.MaxConnections.ToInt() instance.State.MaxConnections = cfg.MaxConnections.ToInt()
instance.State.Track = evt.Track instance.State.Track = evt.Track
go tracking.TailLogFile(filepath.Join(server.ConfigPath, "\\server\\log\\server.log"), instance.HandleLogLine) // Cache sessions for duration lookup
s.instances.Store(server.ID, instance) 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 // GetAll

View File

@@ -101,40 +101,53 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
elem := val.Elem() elem := val.Elem()
typ := elem.Type() typ := elem.Type()
for i := 0; i < elem.NumField(); i++ { // Process all fields including embedded structs
field := elem.Field(i) var processFields func(reflect.Value, reflect.Type) error
fieldType := typ.Field(i) 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 // Handle embedded structs recursively
if !field.CanSet() { if fieldType.Anonymous {
continue if err := processFields(field, fieldType.Type); err != nil {
} return err
}
// Check for param tag first (path parameters) continue
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
}
// Then check for query tag // Skip if field cannot be set
queryName := fieldType.Tag.Get("query") if !field.CanSet() {
if queryName == "" { continue
queryName = ToSnakeCase(fieldType.Name) // Default to snake_case of field name }
}
queryVal := c.Query(queryName) // Check for param tag first (path parameters)
if queryVal == "" { if paramName := fieldType.Tag.Get("param"); paramName != "" {
continue // Skip empty values 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 { // Then check for query tag
return fmt.Errorf("error parsing query parameter %s: %v", queryName, err) 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 { func parsePathParam(c *fiber.Ctx, field reflect.Value, paramName string) error {

View File

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