enki 7c92aa3ded
Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / E2E Tests (push) Blocked by required conditions
Major DHT and Torrent fixes.
2025-08-29 21:18:36 -07:00

350 lines
9.4 KiB
Go

package stats
import (
"database/sql"
"log"
)
// CreateStatsTables creates the necessary tables for stats collection
func CreateStatsTables(db *sql.DB) error {
// Time-series stats table
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS stats_timeseries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
component TEXT NOT NULL,
metric TEXT NOT NULL,
value REAL NOT NULL,
metadata TEXT,
INDEX idx_component_metric_time (component, metric, timestamp)
)
`)
if err != nil {
return err
}
// Performance metrics table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS performance_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
endpoint TEXT NOT NULL,
method TEXT NOT NULL,
response_time_ms REAL NOT NULL,
status_code INTEGER NOT NULL,
bytes_sent INTEGER DEFAULT 0,
bytes_received INTEGER DEFAULT 0,
user_agent TEXT,
ip_address TEXT,
INDEX idx_endpoint_time (endpoint, timestamp),
INDEX idx_status_time (status_code, timestamp)
)
`)
if err != nil {
return err
}
// Bandwidth tracking table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS bandwidth_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
torrent_hash TEXT NOT NULL,
bytes_served INTEGER NOT NULL,
bytes_from_peers INTEGER DEFAULT 0,
peer_count INTEGER DEFAULT 0,
source TEXT NOT NULL, -- 'webseed', 'tracker', 'dht'
INDEX idx_torrent_time (torrent_hash, timestamp),
INDEX idx_source_time (source, timestamp)
)
`)
if err != nil {
return err
}
// System metrics table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS system_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
cpu_percent REAL NOT NULL,
memory_bytes INTEGER NOT NULL,
goroutines INTEGER NOT NULL,
heap_objects INTEGER NOT NULL,
gc_cycles INTEGER NOT NULL,
disk_usage INTEGER DEFAULT 0
)
`)
if err != nil {
return err
}
// Component health history
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS component_health (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
component TEXT NOT NULL,
status TEXT NOT NULL, -- 'healthy', 'degraded', 'unhealthy'
error_message TEXT,
response_time_ms REAL,
INDEX idx_component_status_time (component, status, timestamp)
)
`)
return err
}
// StatsDB wraps database operations for stats
type StatsDB struct {
db *sql.DB
}
// NewStatsDB creates a new stats database wrapper
func NewStatsDB(db *sql.DB) *StatsDB {
return &StatsDB{db: db}
}
// RecordTimeSeries records a time-series data point
func (sdb *StatsDB) RecordTimeSeries(component, metric string, value float64, metadata string) error {
_, err := sdb.db.Exec(`
INSERT INTO stats_timeseries (component, metric, value, metadata)
VALUES (?, ?, ?, ?)
`, component, metric, value, metadata)
return err
}
// RecordPerformance records a performance metric
func (sdb *StatsDB) RecordPerformance(endpoint, method string, responseTime float64, statusCode int, bytesSent, bytesReceived int64, userAgent, ipAddress string) error {
_, err := sdb.db.Exec(`
INSERT INTO performance_metrics (endpoint, method, response_time_ms, status_code, bytes_sent, bytes_received, user_agent, ip_address)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, endpoint, method, responseTime, statusCode, bytesSent, bytesReceived, userAgent, ipAddress)
return err
}
// RecordBandwidth records bandwidth usage
func (sdb *StatsDB) RecordBandwidth(torrentHash string, bytesServed, bytesFromPeers int64, peerCount int, source string) error {
_, err := sdb.db.Exec(`
INSERT INTO bandwidth_stats (torrent_hash, bytes_served, bytes_from_peers, peer_count, source)
VALUES (?, ?, ?, ?, ?)
`, torrentHash, bytesServed, bytesFromPeers, peerCount, source)
return err
}
// RecordSystemMetrics records system performance metrics
func (sdb *StatsDB) RecordSystemMetrics(cpuPercent float64, memoryBytes int64, goroutines, heapObjects, gcCycles int, diskUsage int64) error {
_, err := sdb.db.Exec(`
INSERT INTO system_metrics (cpu_percent, memory_bytes, goroutines, heap_objects, gc_cycles, disk_usage)
VALUES (?, ?, ?, ?, ?, ?)
`, cpuPercent, memoryBytes, goroutines, heapObjects, gcCycles, diskUsage)
return err
}
// RecordComponentHealth records component health status
func (sdb *StatsDB) RecordComponentHealth(component, status, errorMessage string, responseTime float64) error {
_, err := sdb.db.Exec(`
INSERT INTO component_health (component, status, error_message, response_time_ms)
VALUES (?, ?, ?, ?)
`, component, status, errorMessage, responseTime)
return err
}
// GetAverageResponseTime gets average response time for last N minutes
func (sdb *StatsDB) GetAverageResponseTime(minutes int) (float64, error) {
var avg float64
err := sdb.db.QueryRow(`
SELECT COALESCE(AVG(response_time_ms), 0)
FROM performance_metrics
WHERE timestamp > datetime('now', '-' || ? || ' minutes')
`, minutes).Scan(&avg)
return avg, err
}
// GetRequestsPerSecond gets requests per second for last N minutes
func (sdb *StatsDB) GetRequestsPerSecond(minutes int) (float64, error) {
var count int64
err := sdb.db.QueryRow(`
SELECT COUNT(*)
FROM performance_metrics
WHERE timestamp > datetime('now', '-' || ? || ' minutes')
`, minutes).Scan(&count)
if err != nil {
return 0, err
}
return float64(count) / float64(minutes*60), nil
}
// GetErrorRate gets error rate percentage for last N minutes
func (sdb *StatsDB) GetErrorRate(minutes int) (float64, error) {
var totalRequests, errorRequests int64
err := sdb.db.QueryRow(`
SELECT COUNT(*) FROM performance_metrics
WHERE timestamp > datetime('now', '-' || ? || ' minutes')
`, minutes).Scan(&totalRequests)
if err != nil {
return 0, err
}
err = sdb.db.QueryRow(`
SELECT COUNT(*) FROM performance_metrics
WHERE timestamp > datetime('now', '-' || ? || ' minutes')
AND status_code >= 400
`, minutes).Scan(&errorRequests)
if err != nil {
return 0, err
}
if totalRequests == 0 {
return 0, nil
}
return float64(errorRequests) / float64(totalRequests) * 100, nil
}
// GetBandwidthStats gets bandwidth statistics for last N hours
func (sdb *StatsDB) GetBandwidthStats(hours int) (map[string]interface{}, error) {
rows, err := sdb.db.Query(`
SELECT
SUM(bytes_served) as total_served,
SUM(bytes_from_peers) as total_from_peers,
COUNT(DISTINCT torrent_hash) as active_torrents,
AVG(peer_count) as avg_peer_count
FROM bandwidth_stats
WHERE timestamp > datetime('now', '-' || ? || ' hours')
`, hours)
if err != nil {
return nil, err
}
defer rows.Close()
var totalServed, totalFromPeers, activeTorrents, avgPeerCount int64
if rows.Next() {
err = rows.Scan(&totalServed, &totalFromPeers, &activeTorrents, &avgPeerCount)
if err != nil {
return nil, err
}
}
var p2pOffload float64
if totalServed > 0 {
p2pOffload = float64(totalFromPeers) / float64(totalServed) * 100
}
return map[string]interface{}{
"total_served": totalServed,
"total_from_peers": totalFromPeers,
"p2p_offload_percent": p2pOffload,
"active_torrents": activeTorrents,
"avg_peer_count": avgPeerCount,
}, nil
}
// GetSystemHealthOverTime gets system health metrics over time
func (sdb *StatsDB) GetSystemHealthOverTime(hours int) ([]map[string]interface{}, error) {
rows, err := sdb.db.Query(`
SELECT
datetime(timestamp) as time,
cpu_percent,
memory_bytes,
goroutines
FROM system_metrics
WHERE timestamp > datetime('now', '-' || ? || ' hours')
ORDER BY timestamp DESC
LIMIT 100
`, hours)
if err != nil {
return nil, err
}
defer rows.Close()
var results []map[string]interface{}
for rows.Next() {
var timestamp string
var cpu float64
var memory int64
var goroutines int
err = rows.Scan(&timestamp, &cpu, &memory, &goroutines)
if err != nil {
continue
}
results = append(results, map[string]interface{}{
"timestamp": timestamp,
"cpu": cpu,
"memory": memory,
"goroutines": goroutines,
})
}
return results, nil
}
// CleanupOldStats removes old statistics data
func (sdb *StatsDB) CleanupOldStats(daysToKeep int) error {
tables := []string{
"stats_timeseries",
"performance_metrics",
"bandwidth_stats",
"system_metrics",
"component_health",
}
for _, table := range tables {
_, err := sdb.db.Exec(`
DELETE FROM `+table+`
WHERE timestamp < datetime('now', '-' || ? || ' days')
`, daysToKeep)
if err != nil {
log.Printf("Error cleaning up %s: %v", table, err)
}
}
return nil
}
// GetTopTorrentsByBandwidth gets most popular torrents by bandwidth
func (sdb *StatsDB) GetTopTorrentsByBandwidth(hours int, limit int) ([]map[string]interface{}, error) {
rows, err := sdb.db.Query(`
SELECT
torrent_hash,
SUM(bytes_served) as total_served,
AVG(peer_count) as avg_peers,
COUNT(*) as records
FROM bandwidth_stats
WHERE timestamp > datetime('now', '-' || ? || ' hours')
GROUP BY torrent_hash
ORDER BY total_served DESC
LIMIT ?
`, hours, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var results []map[string]interface{}
for rows.Next() {
var hash string
var served int64
var avgPeers float64
var records int
err = rows.Scan(&hash, &served, &avgPeers, &records)
if err != nil {
continue
}
results = append(results, map[string]interface{}{
"hash": hash,
"bytes_served": served,
"avg_peers": avgPeers,
"activity": records,
})
}
return results, nil
}