update logging
This commit is contained in:
@@ -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 {
|
||||||
|
// Remove quotes if present
|
||||||
str = str[1 : len(str)-1]
|
str = str[1 : len(str)-1]
|
||||||
|
}
|
||||||
*s = ParseServiceStatus(str)
|
*s = ParseServiceStatus(str)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type ServerState struct {
|
|||||||
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.
|
||||||
}
|
}
|
||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
@@ -87,20 +87,33 @@ 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 {
|
||||||
|
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
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,7 +137,6 @@ 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
|
||||||
|
|||||||
@@ -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,6 +30,23 @@ 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,
|
||||||
@@ -35,19 +54,19 @@ func NewServerService(repository *repository.ServerRepository, stateHistoryRepo
|
|||||||
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 {
|
|
||||||
log.Print(err.Error())
|
|
||||||
}
|
|
||||||
if (status == string(model.StatusRunning)) {
|
|
||||||
service.StartAccServerRuntime(&server)
|
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)
|
||||||
|
var instance *tracking.AccServerInstance
|
||||||
|
if !exists {
|
||||||
|
instance = tracking.NewAccServerInstance(server, func(state *model.ServerState, states ...tracking.StateChange) {
|
||||||
s.handleStateChange(server, state)
|
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
|
||||||
|
|||||||
@@ -101,10 +101,21 @@ 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
|
||||||
|
processFields = func(val reflect.Value, typ reflect.Type) error {
|
||||||
|
for i := 0; i < val.NumField(); i++ {
|
||||||
|
field := val.Field(i)
|
||||||
fieldType := typ.Field(i)
|
fieldType := typ.Field(i)
|
||||||
|
|
||||||
|
// Handle embedded structs recursively
|
||||||
|
if fieldType.Anonymous {
|
||||||
|
if err := processFields(field, fieldType.Type); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if field cannot be set
|
// Skip if field cannot be set
|
||||||
if !field.CanSet() {
|
if !field.CanSet() {
|
||||||
continue
|
continue
|
||||||
@@ -133,10 +144,12 @@ func ParseQueryFilter(c *fiber.Ctx, filter interface{}) error {
|
|||||||
return fmt.Errorf("error parsing query parameter %s: %v", queryName, err)
|
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 {
|
||||||
switch field.Kind() {
|
switch field.Kind() {
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
|||||||
78
local/utl/tracking/log_tailer.go
Normal file
78
local/utl/tracking/log_tailer.go
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user