Files
OpenFlare/openflare_relay/internal/observability/collector.go
T

326 lines
7.8 KiB
Go

package observability
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"openflare-relay/internal/config"
"openflare-relay/internal/frps"
"openflare-relay/internal/state"
"openflare/service"
)
func BuildProfile(cfg *config.Config, stateStore *state.Store) *service.AgentNodeSystemProfile {
profile := collectProfile(cfg)
if profile == nil || stateStore == nil {
return profile
}
fingerprint := fingerprintProfile(profile)
snapshot, err := stateStore.Load()
if err != nil {
return profile
}
if snapshot.LastProfileFingerprint == fingerprint {
return nil
}
snapshot.LastProfileFingerprint = fingerprint
if err = stateStore.Save(snapshot); err != nil {
return profile
}
return profile
}
func BuildSnapshot(cfg *config.Config, stateStore *state.Store) *service.AgentNodeMetricSnapshot {
now := time.Now().UTC()
metric := &service.AgentNodeMetricSnapshot{CapturedAtUnix: now.Unix()}
metric.MemoryTotalBytes, metric.MemoryUsedBytes = readMemInfo()
metric.StorageTotalBytes, metric.StorageUsedBytes = statFilesystem(cfg.DataDir)
metric.NetworkRxBytes, metric.NetworkTxBytes = readLinuxNetworkTotals()
metric.DiskReadBytes, metric.DiskWriteBytes = readLinuxDiskTotals()
if stateStore == nil {
return metric
}
totalCPU, idleCPU := readLinuxCPUStat()
snapshot, err := stateStore.Load()
if err != nil {
return metric
}
if snapshot.LastCPUStatTotal > 0 && totalCPU > snapshot.LastCPUStatTotal && idleCPU >= snapshot.LastCPUStatIdle {
deltaTotal := totalCPU - snapshot.LastCPUStatTotal
deltaIdle := idleCPU - snapshot.LastCPUStatIdle
if deltaTotal > 0 && deltaIdle <= deltaTotal {
metric.CPUUsagePercent = float64(deltaTotal-deltaIdle) / float64(deltaTotal) * 100
}
}
snapshot.LastCPUStatTotal = totalCPU
snapshot.LastCPUStatIdle = idleCPU
snapshot.LastMetricAtUnix = now.Unix()
_ = stateStore.Save(snapshot)
return metric
}
func BuildHealthEvents(status frps.RuntimeStatus) []service.AgentNodeHealthEvent {
if strings.TrimSpace(status.Status) == "healthy" {
return []service.AgentNodeHealthEvent{}
}
message := strings.TrimSpace(status.LastError)
if message == "" {
message = "frps runtime is not healthy"
}
return []service.AgentNodeHealthEvent{{
EventType: "frps_unhealthy",
Severity: "critical",
Message: message,
TriggeredAtUnix: time.Now().UTC().Unix(),
}}
}
func collectProfile(cfg *config.Config) *service.AgentNodeSystemProfile {
hostname, _ := os.Hostname()
osName, osVersion := readLinuxOSRelease()
totalMemory, _ := readMemInfo()
totalDisk, _ := statFilesystem(cfg.DataDir)
return &service.AgentNodeSystemProfile{
Hostname: strings.TrimSpace(hostname),
OSName: osName,
OSVersion: osVersion,
KernelVersion: readFirstLine("/proc/sys/kernel/osrelease"),
Architecture: runtime.GOARCH,
CPUModel: readLinuxCPUModel(),
CPUCores: runtime.NumCPU(),
TotalMemoryBytes: totalMemory,
TotalDiskBytes: totalDisk,
UptimeSeconds: readLinuxUptimeSeconds(),
ReportedAtUnix: time.Now().UTC().Unix(),
}
}
func fingerprintProfile(profile *service.AgentNodeSystemProfile) string {
raw, err := json.Marshal(profile)
if err != nil {
return ""
}
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
func readLinuxOSRelease() (string, string) {
file, err := os.Open("/etc/os-release")
if err != nil {
return runtime.GOOS, ""
}
defer file.Close()
values := make(map[string]string)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
key, value, ok := strings.Cut(strings.TrimSpace(scanner.Text()), "=")
if !ok {
continue
}
values[key] = strings.Trim(value, `"`)
}
if pretty := strings.TrimSpace(values["PRETTY_NAME"]); pretty != "" {
return pretty, strings.TrimSpace(values["VERSION_ID"])
}
if name := strings.TrimSpace(values["NAME"]); name != "" {
return name, strings.TrimSpace(values["VERSION_ID"])
}
return runtime.GOOS, ""
}
func readLinuxCPUModel() string {
file, err := os.Open("/proc/cpuinfo")
if err != nil {
return ""
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(strings.ToLower(line), "model name") {
_, value, ok := strings.Cut(line, ":")
if ok {
return strings.TrimSpace(value)
}
}
}
return ""
}
func readMemInfo() (int64, int64) {
file, err := os.Open("/proc/meminfo")
if err != nil {
return 0, 0
}
defer file.Close()
var totalKB, availableKB int64
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "MemTotal:") {
totalKB = parseMemInfoValue(line)
}
if strings.HasPrefix(line, "MemAvailable:") {
availableKB = parseMemInfoValue(line)
}
}
total := totalKB * 1024
used := total - availableKB*1024
if used < 0 {
used = 0
}
return total, used
}
func parseMemInfoValue(line string) int64 {
fields := strings.Fields(line)
if len(fields) < 2 {
return 0
}
value, err := strconv.ParseInt(fields[1], 10, 64)
if err != nil {
return 0
}
return value
}
func readLinuxUptimeSeconds() int64 {
content, err := os.ReadFile("/proc/uptime")
if err != nil {
return 0
}
fields := strings.Fields(string(content))
if len(fields) == 0 {
return 0
}
value, err := strconv.ParseFloat(fields[0], 64)
if err != nil {
return 0
}
return int64(value)
}
func readLinuxCPUStat() (uint64, uint64) {
content, err := os.ReadFile("/proc/stat")
if err != nil {
return 0, 0
}
for _, line := range strings.Split(string(content), "\n") {
if !strings.HasPrefix(line, "cpu ") {
continue
}
fields := strings.Fields(line)
if len(fields) < 5 {
return 0, 0
}
var total uint64
for index := 1; index < len(fields); index++ {
value, err := strconv.ParseUint(fields[index], 10, 64)
if err != nil {
return 0, 0
}
total += value
}
idle, err := strconv.ParseUint(fields[4], 10, 64)
if err != nil {
return 0, 0
}
return total, idle
}
return 0, 0
}
func readLinuxNetworkTotals() (int64, int64) {
file, err := os.Open("/proc/net/dev")
if err != nil {
return 0, 0
}
defer file.Close()
var rx, tx int64
scanner := bufio.NewScanner(file)
for scanner.Scan() {
name, data, ok := strings.Cut(strings.TrimSpace(scanner.Text()), ":")
if !ok || strings.TrimSpace(name) == "lo" {
continue
}
fields := strings.Fields(data)
if len(fields) < 16 {
continue
}
if value, err := strconv.ParseInt(fields[0], 10, 64); err == nil {
rx += value
}
if value, err := strconv.ParseInt(fields[8], 10, 64); err == nil {
tx += value
}
}
return rx, tx
}
func readLinuxDiskTotals() (int64, int64) {
file, err := os.Open("/proc/diskstats")
if err != nil {
return 0, 0
}
defer file.Close()
var readBytes, writeBytes int64
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 14 || shouldSkipDiskDevice(fields[2]) {
continue
}
if value, err := strconv.ParseInt(fields[5], 10, 64); err == nil {
readBytes += value * 512
}
if value, err := strconv.ParseInt(fields[9], 10, 64); err == nil {
writeBytes += value * 512
}
}
return readBytes, writeBytes
}
func shouldSkipDiskDevice(device string) bool {
return device == "" || strings.HasPrefix(device, "loop") || strings.HasPrefix(device, "ram") || strings.HasPrefix(device, "dm-")
}
func statFilesystem(path string) (int64, int64) {
if strings.TrimSpace(path) == "" {
path = string(os.PathSeparator)
}
var stat syscall.Statfs_t
if err := syscall.Statfs(filepath.Clean(path), &stat); err != nil {
return 0, 0
}
total := int64(stat.Blocks) * int64(stat.Bsize)
used := total - int64(stat.Bavail)*int64(stat.Bsize)
if used < 0 {
used = 0
}
return total, used
}
func readFirstLine(path string) string {
content, err := os.ReadFile(path)
if err != nil {
return ""
}
return strings.TrimSpace(string(content))
}