add statistics retrieve method

This commit is contained in:
Fran Jurmanović
2025-05-30 13:31:28 +02:00
parent 26c6bc5496
commit 9c9e28350f
7 changed files with 555 additions and 58 deletions

View File

@@ -11,6 +11,7 @@ import (
"log"
"os"
"path/filepath"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
@@ -63,13 +64,19 @@ func mustDecode[T any](fileName, path string) (T, error) {
type ConfigService struct {
repository *repository.ConfigRepository
serverRepository *repository.ServerRepository
serverService *ServerService
serverService *ServerService
configCache *model.ServerConfigCache
}
func NewConfigService(repository *repository.ConfigRepository, serverRepository *repository.ServerRepository) *ConfigService {
return &ConfigService{
repository: repository,
serverRepository: serverRepository,
configCache: model.NewServerConfigCache(model.CacheConfig{
ExpirationTime: 5 * time.Minute, // Cache configs for 5 minutes
ThrottleTime: 1 * time.Second, // Prevent rapid re-reads
DefaultStatus: model.StatusUnknown,
}),
}
}
@@ -148,6 +155,9 @@ func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{
return nil, err
}
// Invalidate all configs for this server since configs can be interdependent
as.configCache.InvalidateServerCache(strconv.Itoa(serverID))
as.serverService.StartAccServerRuntime(server)
// Log change
@@ -170,71 +180,145 @@ func (as ConfigService) UpdateConfig(ctx *fiber.Ctx, body *map[string]interface{
func (as ConfigService) GetConfig(ctx *fiber.Ctx) (interface{}, error) {
serverID, _ := ctx.ParamsInt("id")
configFile := ctx.Params("file")
serverIDStr := strconv.Itoa(serverID)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
if err != nil {
log.Print("Server not found")
return nil, fiber.NewError(404, "Server not found")
}
// Try to get from cache based on config file type
switch configFile {
case ConfigurationJson:
if cached, ok := as.configCache.GetConfiguration(serverIDStr); ok {
return cached, nil
}
case AssistRulesJson:
if cached, ok := as.configCache.GetAssistRules(serverIDStr); ok {
return cached, nil
}
case EventJson:
if cached, ok := as.configCache.GetEvent(serverIDStr); ok {
return cached, nil
}
case EventRulesJson:
if cached, ok := as.configCache.GetEventRules(serverIDStr); ok {
return cached, nil
}
case SettingsJson:
if cached, ok := as.configCache.GetSettings(serverIDStr); ok {
return cached, nil
}
}
decoded, err := DecodeFileName(configFile)(server.ConfigPath)
if err != nil {
return nil, err
}
// Cache the result based on config file type
switch configFile {
case ConfigurationJson:
if config, ok := decoded.(model.Configuration); ok {
as.configCache.UpdateConfiguration(serverIDStr, config)
}
case AssistRulesJson:
if rules, ok := decoded.(model.AssistRules); ok {
as.configCache.UpdateAssistRules(serverIDStr, rules)
}
case EventJson:
if event, ok := decoded.(model.EventConfig); ok {
as.configCache.UpdateEvent(serverIDStr, event)
}
case EventRulesJson:
if rules, ok := decoded.(model.EventRules); ok {
as.configCache.UpdateEventRules(serverIDStr, rules)
}
case SettingsJson:
if settings, ok := decoded.(model.ServerSettings); ok {
as.configCache.UpdateSettings(serverIDStr, settings)
}
}
return decoded, nil
}
// GetConfigs
// Gets physical config file and caches it in database.
//
// Args:
// context.Context: Application context
// Returns:
// string: Application version
// Gets all configurations for a server, using cache when possible.
func (as ConfigService) GetConfigs(ctx *fiber.Ctx) (*model.Configurations, error) {
serverID, _ := ctx.ParamsInt("id")
serverIDStr := strconv.Itoa(serverID)
server, err := as.serverRepository.GetByID(ctx.UserContext(), serverID)
if err != nil {
log.Print("Server not found")
return nil, fiber.NewError(404, "Server not found")
}
decodedconfiguration, err := mustDecode[model.Configuration](ConfigurationJson, server.ConfigPath)
if err != nil {
return nil, err
configs := &model.Configurations{}
// Load configuration
if cached, ok := as.configCache.GetConfiguration(serverIDStr); ok {
configs.Configuration = *cached
} else {
config, err := mustDecode[model.Configuration](ConfigurationJson, server.ConfigPath)
if err != nil {
return nil, err
}
configs.Configuration = config
as.configCache.UpdateConfiguration(serverIDStr, config)
}
decodedAssistRules, err := mustDecode[model.AssistRules](AssistRulesJson, server.ConfigPath)
if err != nil {
return nil, err
// Load assist rules
if cached, ok := as.configCache.GetAssistRules(serverIDStr); ok {
configs.AssistRules = *cached
} else {
rules, err := mustDecode[model.AssistRules](AssistRulesJson, server.ConfigPath)
if err != nil {
return nil, err
}
configs.AssistRules = rules
as.configCache.UpdateAssistRules(serverIDStr, rules)
}
decodedevent, err := mustDecode[model.EventConfig](EventJson, server.ConfigPath)
if err != nil {
return nil, err
// Load event config
if cached, ok := as.configCache.GetEvent(serverIDStr); ok {
configs.Event = *cached
} else {
event, err := mustDecode[model.EventConfig](EventJson, server.ConfigPath)
if err != nil {
return nil, err
}
configs.Event = event
as.configCache.UpdateEvent(serverIDStr, event)
}
decodedeventRules, err := mustDecode[model.EventRules](EventRulesJson, server.ConfigPath)
if err != nil {
return nil, err
// Load event rules
if cached, ok := as.configCache.GetEventRules(serverIDStr); ok {
configs.EventRules = *cached
} else {
rules, err := mustDecode[model.EventRules](EventRulesJson, server.ConfigPath)
if err != nil {
return nil, err
}
configs.EventRules = rules
as.configCache.UpdateEventRules(serverIDStr, rules)
}
decodedsettings, err := mustDecode[model.ServerSettings](SettingsJson, server.ConfigPath)
if err != nil {
return nil, err
// Load settings
if cached, ok := as.configCache.GetSettings(serverIDStr); ok {
configs.Settings = *cached
} else {
settings, err := mustDecode[model.ServerSettings](SettingsJson, server.ConfigPath)
if err != nil {
return nil, err
}
configs.Settings = settings
as.configCache.UpdateSettings(serverIDStr, settings)
}
return &model.Configurations{
Configuration: decodedconfiguration,
Event: decodedevent,
EventRules: decodedeventRules,
Settings: decodedsettings,
AssistRules: decodedAssistRules,
}, nil
return configs, nil
}
func readAndDecode[T interface{}](path string, configFile string) (T, error) {

View File

@@ -8,6 +8,7 @@ import (
"context"
"log"
"path/filepath"
"strconv"
"sync"
"time"
@@ -23,7 +24,7 @@ type ServerService struct {
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
sessionIDs sync.Map // Track current session ID per server
}
type pendingState struct {
@@ -91,42 +92,94 @@ func (s *ServerService) shouldInsertStateHistory(serverID uint) bool {
return false
}
func (s *ServerService) getNextSessionID(serverID uint) uint {
currentID, _ := s.sessionIDs.LoadOrStore(serverID, uint(0))
nextID := currentID.(uint) + 1
s.sessionIDs.Store(serverID, nextID)
return nextID
}
func (s *ServerService) insertStateHistory(serverID uint, state *model.ServerState) {
// Get or create session ID when session changes
currentSessionInterface, exists := s.instances.Load(serverID)
var sessionID uint
if !exists {
sessionID = s.getNextSessionID(serverID)
} else {
serverInstance := currentSessionInterface.(*tracking.AccServerInstance)
if serverInstance.State == nil || serverInstance.State.Session != state.Session {
sessionID = s.getNextSessionID(serverID)
} else {
sessionIDInterface, exists := s.sessionIDs.Load(serverID)
if !exists {
sessionID = s.getNextSessionID(serverID)
} else {
sessionID = sessionIDInterface.(uint)
}
}
}
s.stateHistoryRepo.Insert(context.Background(), &model.StateHistory{
ServerID: serverID,
Session: state.Session,
Track: state.Track,
PlayerCount: state.PlayerCount,
DateCreated: time.Now().UTC(),
SessionStart: state.SessionStart,
SessionDurationMinutes: state.SessionDurationMinutes,
SessionID: sessionID,
})
}
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)
serverIDStr := strconv.FormatUint(uint64(server.ID), 10)
// Get event config from cache or load it
var event model.EventConfig
if cached, ok := s.configService.configCache.GetEvent(serverIDStr); ok {
event = *cached
} else {
event, err := mustDecode[model.EventConfig](EventJson, server.ConfigPath)
if err != nil {
logging.Error("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
s.configService.configCache.UpdateEvent(serverIDStr, event)
}
sessions := sessionsInterface.([]model.Session)
if sessionType == "" && len(sessions) > 0 {
sessionType = sessions[0].SessionType
var configuration model.Configuration
if cached, ok := s.configService.configCache.GetConfiguration(serverIDStr); ok {
configuration = *cached
} else {
configuration, err := mustDecode[model.Configuration](ConfigurationJson, server.ConfigPath)
if err != nil {
logging.Error("Failed to load configuration config for server %d: %v", server.ID, err)
return
}
s.configService.configCache.UpdateConfiguration(serverIDStr, configuration)
}
for _, session := range sessions {
if session.SessionType == sessionType {
if instance, ok := s.instances.Load(server.ID); ok {
serverInstance := instance.(*tracking.AccServerInstance)
if instance, ok := s.instances.Load(server.ID); ok {
serverInstance := instance.(*tracking.AccServerInstance)
serverInstance.State.Track = event.Track
serverInstance.State.MaxConnections = configuration.MaxConnections.ToInt()
// Check if session type has changed
if serverInstance.State.Session != sessionType {
// Get new session ID for the new session
sessionID := s.getNextSessionID(server.ID)
s.sessionIDs.Store(server.ID, sessionID)
}
if sessionType == "" && len(event.Sessions) > 0 {
sessionType = event.Sessions[0].SessionType
}
for _, session := range event.Sessions {
if session.SessionType == sessionType {
serverInstance.State.SessionDurationMinutes = session.SessionDurationMinutes.ToInt()
serverInstance.State.Session = sessionType
break
}
break
}
}
}
@@ -177,17 +230,10 @@ func (s *ServerService) StartAccServerRuntime(server *model.Server) {
instance = instanceInterface.(*tracking.AccServerInstance)
}
config, _ := DecodeFileName(ConfigurationJson)(server.ConfigPath)
cfg := config.(model.Configuration)
event, _ := DecodeFileName(EventJson)(server.ConfigPath)
evt := event.(model.EventConfig)
// Invalidate config cache for this server before loading new configs
serverIDStr := strconv.FormatUint(uint64(server.ID), 10)
s.configService.configCache.InvalidateServerCache(serverIDStr)
instance.State.MaxConnections = cfg.MaxConnections.ToInt()
instance.State.Track = evt.Track
// 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)

View File

@@ -4,6 +4,8 @@ import (
"acc-server-manager/local/model"
"acc-server-manager/local/repository"
"log"
"sort"
"time"
"github.com/gofiber/fiber/v2"
)
@@ -40,4 +42,152 @@ func (s *StateHistoryService) Insert(ctx *fiber.Ctx, model *model.StateHistory)
return err
}
return nil
}
func (s *StateHistoryService) GetStatistics(ctx *fiber.Ctx, filter *model.StateHistoryFilter) (*model.StateHistoryStats, error) {
// Get all state history entries based on filter
entries, err := s.repository.GetAll(ctx.UserContext(), filter)
if err != nil {
log.Printf("Error getting state history for statistics: %v", err)
return nil, err
}
stats := &model.StateHistoryStats{
PlayerCountOverTime: make([]model.PlayerCountPoint, 0),
SessionTypes: make([]model.SessionCount, 0),
DailyActivity: make([]model.DailyActivity, 0),
RecentSessions: make([]model.RecentSession, 0),
}
if len(*entries) == 0 {
return stats, nil
}
// Maps to track unique sessions and their details
sessionMap := make(map[uint]*struct {
StartTime time.Time
EndTime time.Time
Session string
Track string
MaxPlayers int
})
// Maps for aggregating statistics
dailySessionCount := make(map[string]int)
sessionTypeCount := make(map[string]int)
totalPlayers := 0
peakPlayers := 0
// Process each state history entry
for _, entry := range *entries {
// Track player count over time
stats.PlayerCountOverTime = append(stats.PlayerCountOverTime, model.PlayerCountPoint{
Timestamp: entry.DateCreated,
Count: entry.PlayerCount,
})
// Update peak players
if entry.PlayerCount > peakPlayers {
peakPlayers = entry.PlayerCount
}
totalPlayers += entry.PlayerCount
// Process session information using SessionID
if _, exists := sessionMap[entry.SessionID]; !exists {
sessionMap[entry.SessionID] = &struct {
StartTime time.Time
EndTime time.Time
Session string
Track string
MaxPlayers int
}{
StartTime: entry.DateCreated,
Session: entry.Session,
Track: entry.Track,
MaxPlayers: entry.PlayerCount,
}
// Count session types
sessionTypeCount[entry.Session]++
// Count daily sessions
dateStr := entry.DateCreated.Format("2006-01-02")
dailySessionCount[dateStr]++
} else {
session := sessionMap[entry.SessionID]
session.EndTime = entry.DateCreated
if entry.PlayerCount > session.MaxPlayers {
session.MaxPlayers = entry.PlayerCount
}
}
}
// Calculate statistics
stats.PeakPlayers = peakPlayers
stats.TotalSessions = len(sessionMap)
if len(*entries) > 0 {
stats.AveragePlayers = float64(totalPlayers) / float64(len(*entries))
}
// Process session types
for sessionType, count := range sessionTypeCount {
stats.SessionTypes = append(stats.SessionTypes, model.SessionCount{
Name: sessionType,
Count: count,
})
}
// Process daily activity
for dateStr, count := range dailySessionCount {
date, _ := time.Parse("2006-01-02", dateStr)
stats.DailyActivity = append(stats.DailyActivity, model.DailyActivity{
Date: date,
SessionsCount: count,
})
}
// Calculate total playtime and prepare recent sessions
var recentSessions []model.RecentSession
totalPlaytime := 0
for sessionID, session := range sessionMap {
if !session.EndTime.IsZero() {
duration := int(session.EndTime.Sub(session.StartTime).Minutes())
totalPlaytime += duration
recentSessions = append(recentSessions, model.RecentSession{
ID: sessionID,
Date: session.StartTime,
Type: session.Session,
Track: session.Track,
Duration: duration,
Players: session.MaxPlayers,
})
}
}
stats.TotalPlaytime = totalPlaytime
// Sort recent sessions by date (newest first) and limit to last 10
sort.Slice(recentSessions, func(i, j int) bool {
return recentSessions[i].Date.After(recentSessions[j].Date)
})
if len(recentSessions) > 10 {
recentSessions = recentSessions[:10]
}
stats.RecentSessions = recentSessions
// Sort daily activity by date
sort.Slice(stats.DailyActivity, func(i, j int) bool {
return stats.DailyActivity[i].Date.Before(stats.DailyActivity[j].Date)
})
// Sort player count over time by timestamp
sort.Slice(stats.PlayerCountOverTime, func(i, j int) bool {
return stats.PlayerCountOverTime[i].Timestamp.Before(stats.PlayerCountOverTime[j].Timestamp)
})
return stats, nil
}