You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

293 lines
7.9 KiB

package systemmetrics
import (
"context"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"tera-clone/services/tera-api/internal/domain"
)
type Reader interface {
Read(context.Context, []domain.NetworkInterface) domain.SystemRuntimeMetrics
}
type UnavailableReader struct{}
func (UnavailableReader) Read(context.Context, []domain.NetworkInterface) domain.SystemRuntimeMetrics {
return domain.SystemRuntimeMetrics{}
}
type LinuxReaderOptions struct {
ProcRoot string
SysRoot string
EtcRoot string
Clock func() time.Time
StoragePath string
StorageReader func(string) (*domain.StorageMetrics, error)
}
type LinuxReader struct {
procRoot string
sysRoot string
etcRoot string
clock func() time.Time
mu sync.Mutex
previous cpuCounters
storagePath string
storageReader func(string) (*domain.StorageMetrics, error)
}
type cpuCounters struct {
total uint64
idle uint64
valid bool
}
func NewLinuxReader(options LinuxReaderOptions) *LinuxReader {
procRoot := options.ProcRoot
if procRoot == "" {
procRoot = "/proc"
}
sysRoot := options.SysRoot
if sysRoot == "" {
sysRoot = "/sys"
}
etcRoot := options.EtcRoot
if etcRoot == "" {
etcRoot = "/etc"
}
clock := options.Clock
if clock == nil {
clock = time.Now
}
storagePath := options.StoragePath
if storagePath == "" {
storagePath = "/userdata"
}
storageReader := options.StorageReader
if storageReader == nil {
storageReader = readStorage
}
return &LinuxReader{procRoot: procRoot, sysRoot: sysRoot, etcRoot: etcRoot, clock: clock, storagePath: storagePath, storageReader: storageReader}
}
func (r *LinuxReader) Read(ctx context.Context, interfaces []domain.NetworkInterface) domain.SystemRuntimeMetrics {
metrics := domain.SystemRuntimeMetrics{SystemTime: r.clock().UTC(), LoadAverages: []float64{}, NetworkInterfaces: []domain.SystemNetworkInterface{}}
if ctx.Err() != nil {
return metrics
}
metrics.BuildNumber = firstNonEmptyFile(filepath.Join(r.etcRoot, "tera", "build-number"), filepath.Join(r.etcRoot, "tera-release"))
metrics.DeviceID = firstNonEmptyFile(filepath.Join(r.etcRoot, "machine-id"))
metrics.UptimeSeconds = readUptime(filepath.Join(r.procRoot, "uptime"))
metrics.LoadAverages = readLoadAverages(filepath.Join(r.procRoot, "loadavg"))
metrics.TemperatureCelsius = readTemperature(r.sysRoot)
metrics.CPUUsagePercent = r.readCPUUsage(filepath.Join(r.procRoot, "stat"))
metrics.MemoryTotalBytes, metrics.MemoryAvailableBytes, metrics.MemoryUsedBytes, metrics.MemoryUsagePercent = readMemory(filepath.Join(r.procRoot, "meminfo"))
if storage, err := r.storageReader(r.storagePath); err == nil {
metrics.Storage = storage
}
for _, item := range interfaces {
if ctx.Err() != nil {
break
}
metrics.NetworkInterfaces = append(metrics.NetworkInterfaces, r.readInterface(item))
}
return metrics
}
func (r *LinuxReader) readInterface(item domain.NetworkInterface) domain.SystemNetworkInterface {
result := domain.SystemNetworkInterface{Name: item.Name, Device: item.Device, Link: strings.ToLower(strings.TrimSpace(item.Link)), Addresses: []string{}}
if item.Address != "" {
result.Addresses = append(result.Addresses, item.Address)
}
if item.IPv6Address != "" {
result.Addresses = append(result.Addresses, item.IPv6Address)
}
if !validDeviceName(item.Device) {
return result
}
root := filepath.Join(r.sysRoot, "class", "net", item.Device)
result.MACAddress = readTrimmed(filepath.Join(root, "address"))
if link := readTrimmed(filepath.Join(root, "operstate")); link != "" {
result.Link = strings.ToLower(link)
}
if speed, err := strconv.Atoi(readTrimmed(filepath.Join(root, "speed"))); err == nil && speed > 0 {
result.SpeedMbps = speed
}
result.RXBytes = readUint(filepath.Join(root, "statistics", "rx_bytes"))
result.TXBytes = readUint(filepath.Join(root, "statistics", "tx_bytes"))
return result
}
func (r *LinuxReader) readCPUUsage(path string) *float64 {
content, err := os.ReadFile(path)
if err != nil {
return nil
}
line := strings.SplitN(string(content), "\n", 2)[0]
fields := strings.Fields(line)
if len(fields) < 5 || fields[0] != "cpu" {
return nil
}
values := make([]uint64, 0, len(fields)-1)
for _, field := range fields[1:] {
value, err := strconv.ParseUint(field, 10, 64)
if err != nil {
return nil
}
values = append(values, value)
}
var total uint64
for _, value := range values {
total += value
}
idle := values[3]
if len(values) > 4 {
idle += values[4]
}
r.mu.Lock()
previous := r.previous
r.previous = cpuCounters{total: total, idle: idle, valid: true}
r.mu.Unlock()
if !previous.valid || total <= previous.total {
return nil
}
deltaTotal := total - previous.total
deltaIdle := idle - previous.idle
value := 100 * (1 - float64(deltaIdle)/float64(deltaTotal))
value = math.Round(math.Max(0, math.Min(100, value))*10) / 10
return &value
}
func readUptime(path string) int64 {
fields := strings.Fields(readTrimmed(path))
if len(fields) == 0 {
return 0
}
value, err := strconv.ParseFloat(fields[0], 64)
if err != nil || value < 0 {
return 0
}
return int64(value)
}
func readLoadAverages(path string) []float64 {
fields := strings.Fields(readTrimmed(path))
if len(fields) < 3 {
return []float64{}
}
result := make([]float64, 0, 3)
for _, field := range fields[:3] {
value, err := strconv.ParseFloat(field, 64)
if err != nil {
return []float64{}
}
result = append(result, value)
}
return result
}
func readTemperature(sysRoot string) *float64 {
paths, _ := filepath.Glob(filepath.Join(sysRoot, "class", "thermal", "thermal_zone*", "temp"))
for _, path := range paths {
value, err := strconv.ParseFloat(readTrimmed(path), 64)
if err != nil {
continue
}
if math.Abs(value) > 1000 {
value /= 1000
}
if value >= -50 && value <= 200 {
return &value
}
}
return nil
}
func readMemory(path string) (totalBytes, availableBytes, usedBytes uint64, usagePercent *float64) {
values := make(map[string]uint64)
for _, line := range strings.Split(readTrimmed(path), "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
key := strings.TrimSuffix(fields[0], ":")
value, err := strconv.ParseUint(fields[1], 10, 64)
if err != nil {
continue
}
if len(fields) >= 3 && !strings.EqualFold(fields[2], "kB") {
continue
}
if value > ^uint64(0)/1024 {
continue
}
values[key] = value * 1024
}
totalBytes = values["MemTotal"]
if totalBytes == 0 {
return 0, 0, 0, nil
}
var availableKnown bool
availableBytes, availableKnown = values["MemAvailable"]
if !availableKnown {
availableBytes = values["MemFree"] + values["Buffers"] + values["Cached"] + values["SReclaimable"]
if shmem := values["Shmem"]; shmem < availableBytes {
availableBytes -= shmem
}
}
if availableBytes > totalBytes {
availableBytes = totalBytes
}
usedBytes = totalBytes - availableBytes
percent := math.Round((float64(usedBytes)/float64(totalBytes))*1000) / 10
return totalBytes, availableBytes, usedBytes, &percent
}
func firstNonEmptyFile(paths ...string) string {
for _, path := range paths {
if value := readTrimmed(path); value != "" {
if newline := strings.IndexByte(value, '\n'); newline >= 0 {
value = strings.TrimSpace(value[:newline])
}
if len(value) > 128 {
value = value[:128]
}
return value
}
}
return ""
}
func readTrimmed(path string) string {
content, err := os.ReadFile(path)
if err != nil || len(content) > 4096 {
return ""
}
return strings.TrimSpace(strings.ToValidUTF8(string(content), ""))
}
func readUint(path string) uint64 {
value, _ := strconv.ParseUint(readTrimmed(path), 10, 64)
return value
}
func validDeviceName(value string) bool {
if value == "" || len(value) > 32 {
return false
}
for _, char := range value {
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '-' || char == '_' || char == '.' {
continue
}
return false
}
return true
}