155 lines
3.9 KiB
Go
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)
|
|
}
|
|
}
|