Files
nutcas3 afe4084ae2 refactor: Replace log.Printf with structured logging using logrus in api-server metrics collection
- Remove standard log import from server_metrics.go
- Add logrus import for structured logging
- Replace log.Printf with logrus.WithError for system metrics storage errors
- Replace log.Printf with logrus.WithError for subscriber count errors (total, active, suspended)
- Replace log.Printf with logrus.WithError for active sessions count errors
- Replace log.Printf with logrus.WithError for /proc/stat, /proc/meminfo, and /proc/net/dev read errors
2026-04-30 22:43:37 +03:00

197 lines
5.3 KiB
Go

package main
import (
"context"
"math"
"os"
"strconv"
"strings"
"time"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/database"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/metrics"
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
"github.com/sirupsen/logrus"
)
// startMetricsCollection periodically collects and stores metrics.
func startMetricsCollection(ctx context.Context, mc *metrics.MetricsCollectorWithTimeSeries, db *database.Database) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
collectMetrics(ctx, mc, db)
}
}
}
// collectMetrics gathers metrics from various sources.
func collectMetrics(ctx context.Context, mc *metrics.MetricsCollectorWithTimeSeries, db *database.Database) {
totalSubs, activeSubs, suspendedSubs := getSubscriberStats(db)
mc.UpdateSubscriberMetrics(totalSubs, activeSubs, suspendedSubs)
systemMetrics := metrics.SystemMetrics{
ActiveSessions: getActiveSessionsCount(db),
CPUUsage: getCPUUsage(),
MemoryUsage: getMemoryUsage(),
NetworkRX: getNetworkRX(),
NetworkTX: getNetworkTX(),
Timestamp: time.Now(),
}
if ts := mc.GetTimeSeriesStorage(); ts != nil {
if err := ts.StoreSystemMetrics(ctx, systemMetrics); err != nil {
logrus.WithError(err).Error("Failed to store system metrics")
}
}
}
// getSubscriberStats counts subscribers by status.
func getSubscriberStats(db *database.Database) (total, active, suspended int) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var totalCount, activeCount, suspendedCount int64
if err := db.DB.WithContext(ctx).Model(&models.Subscriber{}).Count(&totalCount).Error; err != nil {
logrus.WithError(err).Error("Error counting total subscribers")
return 0, 0, 0
}
if err := db.DB.WithContext(ctx).Model(&models.Subscriber{}).Where("status = ?", "active").Count(&activeCount).Error; err != nil {
logrus.WithError(err).Error("Error counting active subscribers")
return int(totalCount), 0, 0
}
if err := db.DB.WithContext(ctx).Model(&models.Subscriber{}).
Where("status IN ?", []string{"suspended", "deactivated", "blocked"}).
Count(&suspendedCount).Error; err != nil {
logrus.WithError(err).Error("Error counting suspended subscribers")
return int(totalCount), int(activeCount), 0
}
return int(totalCount), int(activeCount), int(suspendedCount)
}
// getActiveSessionsCount returns the number of currently active charging sessions.
func getActiveSessionsCount(db *database.Database) int {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var count int64
if err := db.DB.WithContext(ctx).Model(&models.Session{}).Where(
"status IN ? AND end_time IS NULL",
[]string{"active", "connected", "established"},
).Count(&count).Error; err != nil {
logrus.WithError(err).Error("Error counting active sessions")
return 0
}
return int(count)
}
// getCPUUsage reads /proc/stat and returns the non-idle CPU percentage.
func getCPUUsage() float64 {
data, err := os.ReadFile("/proc/stat")
if err != nil {
logrus.WithError(err).Error("Error reading /proc/stat")
return 0
}
lines := strings.Split(string(data), "\n")
if len(lines) == 0 {
return 0
}
fields := strings.Fields(lines[0])
if len(fields) < 8 || fields[0] != "cpu" {
return 0
}
var totalCPU, idleCPU float64
for i, field := range fields[1:8] {
val, err := strconv.ParseFloat(field, 64)
if err != nil {
continue
}
totalCPU += val
if i == 3 {
idleCPU = val
}
}
if totalCPU == 0 {
return 0
}
return math.Min(((totalCPU-idleCPU)/totalCPU)*100, 100.0)
}
// getMemoryUsage reads /proc/meminfo and returns used-memory percentage.
func getMemoryUsage() float64 {
data, err := os.ReadFile("/proc/meminfo")
if err != nil {
logrus.WithError(err).Error("Error reading /proc/meminfo")
return 0
}
lines := strings.Split(string(data), "\n")
var totalMem, availableMem float64
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
switch fields[0] {
case "MemTotal:":
if val, err := strconv.ParseFloat(fields[1], 64); err == nil {
totalMem = val * 1024
}
case "MemAvailable:":
if val, err := strconv.ParseFloat(fields[1], 64); err == nil {
availableMem = val * 1024
}
}
}
if totalMem == 0 {
return 0
}
return math.Min(((totalMem-availableMem)/totalMem)*100, 100.0)
}
// getNetworkRX returns total RX bytes across non-loopback interfaces.
func getNetworkRX() int64 { return readNetworkStat(1) }
// getNetworkTX returns total TX bytes across non-loopback interfaces.
func getNetworkTX() int64 { return readNetworkStat(9) }
// readNetworkStat parses /proc/net/dev for a given field index.
func readNetworkStat(fieldIdx int) int64 {
data, err := os.ReadFile("/proc/net/dev")
if err != nil {
logrus.WithError(err).Error("Error reading /proc/net/dev")
return 0
}
lines := strings.Split(string(data), "\n")
var total int64
for i, line := range lines {
if i < 2 {
continue
}
fields := strings.Fields(line)
if len(fields) < 10 {
continue
}
if strings.HasPrefix(fields[0], "lo:") {
continue
}
if v, err := strconv.ParseInt(fields[fieldIdx], 10, 64); err == nil {
total += v
}
}
return total
}