Files
software-station/internal/stats/stats.go
2025-12-27 02:57:25 -06:00

155 lines
3.9 KiB
Go

package stats
import (
"encoding/json"
"log"
"net/http"
"os"
"sync"
"sync/atomic"
"time"
"software-station/internal/models"
"golang.org/x/time/rate"
)
type Service struct {
HashesFile string
KnownHashes struct {
sync.RWMutex
Data map[string]*models.FingerprintData
}
GlobalStats struct {
sync.RWMutex
UniqueRequests map[string]bool
SuccessDownloads map[string]bool
BlockedRequests map[string]bool
LimitedRequests map[string]bool
WebRequests map[string]bool
TotalResponseTime time.Duration
TotalRequests int64
TotalBytes int64
StartTime time.Time
}
DownloadStats struct {
sync.RWMutex
Limiters map[string]*rate.Limiter
}
hashesDirty int32
stopChan chan struct{}
}
func NewService(hashesFile string) *Service {
s := &Service{
HashesFile: hashesFile,
stopChan: make(chan struct{}),
}
s.KnownHashes.Data = make(map[string]*models.FingerprintData)
s.GlobalStats.UniqueRequests = make(map[string]bool)
s.GlobalStats.SuccessDownloads = make(map[string]bool)
s.GlobalStats.BlockedRequests = make(map[string]bool)
s.GlobalStats.LimitedRequests = make(map[string]bool)
s.GlobalStats.WebRequests = make(map[string]bool)
s.GlobalStats.StartTime = time.Now()
s.DownloadStats.Limiters = make(map[string]*rate.Limiter)
return s
}
func (s *Service) Start() {
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if atomic.CompareAndSwapInt32(&s.hashesDirty, 1, 0) {
s.FlushHashes()
}
case <-s.stopChan:
s.FlushHashes()
return
}
}
}()
}
func (s *Service) Stop() {
close(s.stopChan)
}
func (s *Service) LoadHashes() {
data, err := os.ReadFile(s.HashesFile)
if err != nil {
if !os.IsNotExist(err) {
log.Printf("Error reading hashes file: %v", err)
}
return
}
s.KnownHashes.Lock()
defer s.KnownHashes.Unlock()
if err := json.Unmarshal(data, &s.KnownHashes.Data); err != nil {
log.Printf("Error unmarshaling hashes: %v", err)
return
}
var total int64
for _, d := range s.KnownHashes.Data {
total += atomic.LoadInt64(&d.TotalBytes)
}
atomic.StoreInt64(&s.GlobalStats.TotalBytes, total)
}
func (s *Service) SaveHashes() {
atomic.StoreInt32(&s.hashesDirty, 1)
}
func (s *Service) FlushHashes() {
s.KnownHashes.RLock()
data, err := json.MarshalIndent(s.KnownHashes.Data, "", " ")
s.KnownHashes.RUnlock()
if err != nil {
log.Printf("Error marshaling hashes: %v", err)
return
}
if err := os.WriteFile(s.HashesFile, data, 0600); err != nil {
log.Printf("Error writing hashes file: %v", err)
}
}
func (s *Service) APIStatsHandler(w http.ResponseWriter, r *http.Request) {
s.GlobalStats.RLock()
defer s.GlobalStats.RUnlock()
avgResponse := time.Duration(0)
if s.GlobalStats.TotalRequests > 0 {
avgResponse = s.GlobalStats.TotalResponseTime / time.Duration(s.GlobalStats.TotalRequests)
}
totalBytes := atomic.LoadInt64(&s.GlobalStats.TotalBytes)
uptime := time.Since(s.GlobalStats.StartTime)
avgSpeed := float64(totalBytes) / uptime.Seconds()
status := "healthy"
if s.GlobalStats.TotalRequests > 0 && float64(len(s.GlobalStats.BlockedRequests))/float64(s.GlobalStats.TotalRequests) > 0.5 {
status = "unhealthy"
}
data := map[string]interface{}{
"total_unique_download_requests": len(s.GlobalStats.UniqueRequests),
"total_unique_success_downloads": len(s.GlobalStats.SuccessDownloads),
"total_unique_blocked": len(s.GlobalStats.BlockedRequests),
"total_unique_limited": len(s.GlobalStats.LimitedRequests),
"total_unique_web_requests": len(s.GlobalStats.WebRequests),
"avg_speed_bps": avgSpeed,
"avg_response_time": avgResponse.String(),
"uptime": uptime.String(),
"status": status,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("Error encoding stats: %v", err)
}
}