350 lines
9.4 KiB
Go
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(×tamp, &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
|
|
} |