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.
294 lines
7.9 KiB
294 lines
7.9 KiB
|
2 months ago
|
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
|
||
|
|
}
|