initial commit

This commit is contained in:
2025-10-17 09:29:37 +03:00
commit 9739ede62e
25 changed files with 2270 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
package collector
import (
"context"
"fmt"
"log"
"time"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/device"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/keenetic"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
type HotspotCollector struct {
registeredDesc *prometheus.Desc
rxBytesDesc *prometheus.Desc
txBytesDesc *prometheus.Desc
txRateDesc *prometheus.Desc
rssiDesc *prometheus.Desc
uptimeDesc *prometheus.Desc
}
func NewHotspotCollector() *HotspotCollector {
labels := []string{"device", "mac", "ip", "client", "ssid"}
return &HotspotCollector{
registeredDesc: prometheus.NewDesc("keenetic_hotspot_client_registered", "Whether the client is registered", labels, nil),
rxBytesDesc: prometheus.NewDesc("keenetic_hotspot_client_rxbytes", "Total number of bytes received by the client", labels, nil),
txBytesDesc: prometheus.NewDesc("keenetic_hotspot_client_txbytes", "Total number of bytes transmitted by the client", labels, nil),
txRateDesc: prometheus.NewDesc("keenetic_hotspot_client_txrate", "Current transmit rate", labels, nil),
rssiDesc: prometheus.NewDesc("keenetic_hotspot_client_rssi", "Received signal strength indicator (RSSI) in dBm", labels, nil),
uptimeDesc: prometheus.NewDesc("keenetic_hotspot_client_uptime", "Uptime of the client device in seconds", labels, nil),
}
}
func (c *HotspotCollector) Name() string {
return "hotspot"
}
func (c *HotspotCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.registeredDesc
ch <- c.rxBytesDesc
ch <- c.txBytesDesc
ch <- c.txRateDesc
ch <- c.rssiDesc
ch <- c.uptimeDesc
}
func (c *HotspotCollector) Collect(dev *device.Device, ch chan<- prometheus.Metric) error {
var hotspotInfo any
var ok bool
hostname := dev.Name
client := dev.Client
dev.CacheMutex.RLock()
hotspotInfo, ok = dev.Cache["hotspot"]
dev.CacheMutex.RUnlock()
if !ok {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
fresh, err := client.GetHotspotClientInfo(ctx)
cancel()
if err != nil {
log.Printf("failed to get hotspot client info from %s: %v", hostname, err)
return err
}
hotspotInfo = fresh
dev.CacheMutex.Lock()
dev.Cache["hotspot"] = fresh
dev.CacheMutex.Unlock()
}
data, ok := hotspotInfo.([]*keenetic.HotspotClientInfo)
if !ok {
log.Printf("invalid cache data type for hotspot info on %s", hostname)
return fmt.Errorf("invalid cache data type for hotspot info")
}
for _, hotspotClient := range data {
labels := []string{hostname, hotspotClient.MAC, hotspotClient.IP, hotspotClient.Name, hotspotClient.SSID}
ch <- prometheus.MustNewConstMetric(c.registeredDesc, prometheus.GaugeValue, utils.BoolToFloat(hotspotClient.Registered), labels...)
ch <- prometheus.MustNewConstMetric(c.rxBytesDesc, prometheus.CounterValue, hotspotClient.RXBytes, labels...)
ch <- prometheus.MustNewConstMetric(c.txBytesDesc, prometheus.CounterValue, hotspotClient.TXBytes, labels...)
ch <- prometheus.MustNewConstMetric(c.txRateDesc, prometheus.GaugeValue, hotspotClient.TXRate, labels...)
ch <- prometheus.MustNewConstMetric(c.rssiDesc, prometheus.GaugeValue, hotspotClient.RSSI, labels...)
ch <- prometheus.MustNewConstMetric(c.uptimeDesc, prometheus.GaugeValue, hotspotClient.Uptime, labels...)
}
return nil
}

111
internal/collector/iface.go Normal file
View File

@@ -0,0 +1,111 @@
package collector
import (
"context"
"fmt"
"log"
"time"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/device"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/keenetic"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
type InterfaceCollector struct {
linkDesc *prometheus.Desc
stateDesc *prometheus.Desc
mtuDesc *prometheus.Desc
txQueueLengthDesc *prometheus.Desc
uptimeDesc *prometheus.Desc
temperatureDesc *prometheus.Desc
channelDesc *prometheus.Desc
}
func NewInterfaceCollector() *InterfaceCollector {
labels := []string{"device", "id", "name", "type", "description", "mac", "connected"}
return &InterfaceCollector{
linkDesc: prometheus.NewDesc("keenetic_interface_link_up", "Link status (1=up, 0=down)", labels, nil),
stateDesc: prometheus.NewDesc("keenetic_interface_state_up", "Interface state (1=up, 0=down)", labels, nil),
mtuDesc: prometheus.NewDesc("keenetic_interface_mtu", "MTU size", labels, nil),
txQueueLengthDesc: prometheus.NewDesc("keenetic_interface_tx_queue_length", "TX queue length", labels, nil),
uptimeDesc: prometheus.NewDesc("keenetic_interface_uptime_seconds", "Interface uptime in seconds", labels, nil),
temperatureDesc: prometheus.NewDesc("keenetic_interface_temperature", "Interface temperature in celsius", labels, nil),
channelDesc: prometheus.NewDesc("keenetic_interface_channel", "Interface wifi channel", labels, nil),
}
}
func (c *InterfaceCollector) Name() string {
return "interface"
}
func (c *InterfaceCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.linkDesc
ch <- c.stateDesc
ch <- c.mtuDesc
ch <- c.txQueueLengthDesc
ch <- c.uptimeDesc
ch <- c.temperatureDesc
ch <- c.channelDesc
}
func (c *InterfaceCollector) Collect(dev *device.Device, ch chan<- prometheus.Metric) error {
var ifaceData any
var ok bool
hostname := dev.Name
client := dev.Client
// Check cache first
dev.CacheMutex.RLock()
ifaceData, ok = dev.Cache["interface"]
valid := time.Since(dev.LastUpdate) < dev.CacheTTL
dev.CacheMutex.RUnlock()
// Fetch fresh data if cache miss or expired
if !ok || !valid {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
fresh, err := client.GetInterfaceInfo(ctx)
cancel()
if err != nil {
log.Printf("failed to get interface info from %s: %v", hostname, err)
return err
}
ifaceData = fresh
dev.CacheMutex.Lock()
dev.Cache["interface"] = fresh
dev.LastUpdate = time.Now()
dev.CacheMutex.Unlock()
}
// Type assertion
data, ok := ifaceData.(map[string]*keenetic.InterfaceInfo)
if !ok || data == nil {
log.Printf("invalid cache data type for interface info on %s", hostname)
return fmt.Errorf("invalid cache data type for interface info")
}
// Emit metrics for each interface
for _, iface := range data {
labels := []string{
hostname,
iface.ID,
iface.InterfaceName,
iface.Type,
iface.Description,
iface.MAC,
iface.Connected,
}
ch <- prometheus.MustNewConstMetric(c.linkDesc, prometheus.GaugeValue, utils.BoolToFloat(iface.Link == "up"), labels...)
ch <- prometheus.MustNewConstMetric(c.stateDesc, prometheus.GaugeValue, utils.BoolToFloat(iface.State == "up"), labels...)
ch <- prometheus.MustNewConstMetric(c.mtuDesc, prometheus.GaugeValue, float64(iface.MTU), labels...)
ch <- prometheus.MustNewConstMetric(c.txQueueLengthDesc, prometheus.GaugeValue, float64(iface.TxQueueLength), labels...)
ch <- prometheus.MustNewConstMetric(c.uptimeDesc, prometheus.CounterValue, float64(iface.Uptime), labels...)
ch <- prometheus.MustNewConstMetric(c.temperatureDesc, prometheus.GaugeValue, float64(iface.Temperature), labels...)
ch <- prometheus.MustNewConstMetric(c.channelDesc, prometheus.GaugeValue, float64(iface.Channel), labels...)
}
return nil
}

View File

@@ -0,0 +1,123 @@
package collector
import (
"context"
"fmt"
"log"
"time"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/device"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/keenetic"
"github.com/prometheus/client_golang/prometheus"
)
type InterfaceStatsCollector struct {
rxBytesDesc *prometheus.Desc
txBytesDesc *prometheus.Desc
rxPacketsDesc *prometheus.Desc
txPacketsDesc *prometheus.Desc
rxErrorsDesc *prometheus.Desc
txErrorsDesc *prometheus.Desc
rxDroppedDesc *prometheus.Desc
txDroppedDesc *prometheus.Desc
rxBroadcastDesc *prometheus.Desc
txBroadcastDesc *prometheus.Desc
rxMulticastDesc *prometheus.Desc
txMulticastDesc *prometheus.Desc
rxSpeedBpsDesc *prometheus.Desc
txSpeedBpsDesc *prometheus.Desc
}
func NewInterfaceStatsCollector() *InterfaceStatsCollector {
labels := []string{"device", "interface"}
return &InterfaceStatsCollector{
rxBytesDesc: prometheus.NewDesc("keenetic_interface_rx_bytes", "Received bytes", labels, nil),
txBytesDesc: prometheus.NewDesc("keenetic_interface_tx_bytes", "Transmitted bytes", labels, nil),
rxPacketsDesc: prometheus.NewDesc("keenetic_interface_rx_packets", "Received packets", labels, nil),
txPacketsDesc: prometheus.NewDesc("keenetic_interface_tx_packets", "Transmitted packets", labels, nil),
rxErrorsDesc: prometheus.NewDesc("keenetic_interface_rx_errors", "Receive errors", labels, nil),
txErrorsDesc: prometheus.NewDesc("keenetic_interface_tx_errors", "Transmit errors", labels, nil),
rxDroppedDesc: prometheus.NewDesc("keenetic_interface_rx_dropped", "Dropped received packets", labels, nil),
txDroppedDesc: prometheus.NewDesc("keenetic_interface_tx_dropped", "Dropped transmitted packets", labels, nil),
rxBroadcastDesc: prometheus.NewDesc("keenetic_interface_rx_broadcast_packets", "Received broadcast packets", labels, nil),
txBroadcastDesc: prometheus.NewDesc("keenetic_interface_tx_broadcast_packets", "Transmitted broadcast packets", labels, nil),
rxMulticastDesc: prometheus.NewDesc("keenetic_interface_rx_multicast_packets", "Received multicast packets", labels, nil),
txMulticastDesc: prometheus.NewDesc("keenetic_interface_tx_multicast_packets", "Transmitted multicast packets", labels, nil),
rxSpeedBpsDesc: prometheus.NewDesc("keenetic_interface_rx_speed_bps", "Receive speed in bits per second", labels, nil),
txSpeedBpsDesc: prometheus.NewDesc("keenetic_interface_tx_speed_bps", "Transmit speed in bits per second", labels, nil),
}
}
func (c *InterfaceStatsCollector) Name() string {
return "interface_stats"
}
func (c *InterfaceStatsCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.rxBytesDesc
ch <- c.txBytesDesc
ch <- c.rxPacketsDesc
ch <- c.txPacketsDesc
ch <- c.rxErrorsDesc
ch <- c.txErrorsDesc
ch <- c.rxDroppedDesc
ch <- c.txDroppedDesc
ch <- c.rxBroadcastDesc
ch <- c.txBroadcastDesc
ch <- c.rxMulticastDesc
ch <- c.txMulticastDesc
ch <- c.rxSpeedBpsDesc
ch <- c.txSpeedBpsDesc
}
func (c *InterfaceStatsCollector) Collect(dev *device.Device, ch chan<- prometheus.Metric) error {
var statsInfo any
var ok bool
hostname := dev.Name
client := dev.Client
dev.CacheMutex.RLock()
statsInfo, ok = dev.Cache["interface_stats"]
dev.CacheMutex.RUnlock()
if !ok {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
fresh, err := client.GetConnectedInterfaceStats(ctx)
cancel()
if err != nil {
log.Printf("failed to get interface stats from %s: %v", hostname, err)
return err
}
statsInfo = fresh
dev.CacheMutex.Lock()
dev.Cache["interface_stats"] = fresh
dev.CacheMutex.Unlock()
}
stats, ok := statsInfo.(map[string]*keenetic.InterfaceStats)
if !ok {
log.Printf("invalid cache data type for interface stats on %s", hostname)
return fmt.Errorf("invalid cache data type for interface stats")
}
for name, s := range stats {
labels := []string{hostname, name}
ch <- prometheus.MustNewConstMetric(c.rxBytesDesc, prometheus.CounterValue, s.RxBytes, labels...)
ch <- prometheus.MustNewConstMetric(c.txBytesDesc, prometheus.CounterValue, s.TxBytes, labels...)
ch <- prometheus.MustNewConstMetric(c.rxPacketsDesc, prometheus.CounterValue, s.RxPackets, labels...)
ch <- prometheus.MustNewConstMetric(c.txPacketsDesc, prometheus.CounterValue, s.TxPackets, labels...)
ch <- prometheus.MustNewConstMetric(c.rxErrorsDesc, prometheus.CounterValue, s.RxErrors, labels...)
ch <- prometheus.MustNewConstMetric(c.txErrorsDesc, prometheus.CounterValue, s.TxErrors, labels...)
ch <- prometheus.MustNewConstMetric(c.rxDroppedDesc, prometheus.CounterValue, s.RxDropped, labels...)
ch <- prometheus.MustNewConstMetric(c.txDroppedDesc, prometheus.CounterValue, s.TxDropped, labels...)
ch <- prometheus.MustNewConstMetric(c.rxBroadcastDesc, prometheus.CounterValue, s.RxBroadcastPackets, labels...)
ch <- prometheus.MustNewConstMetric(c.txBroadcastDesc, prometheus.CounterValue, s.TxBroadcastPackets, labels...)
ch <- prometheus.MustNewConstMetric(c.rxMulticastDesc, prometheus.CounterValue, s.RxMulticastPackets, labels...)
ch <- prometheus.MustNewConstMetric(c.txMulticastDesc, prometheus.CounterValue, s.TxMulticastPackets, labels...)
ch <- prometheus.MustNewConstMetric(c.rxSpeedBpsDesc, prometheus.GaugeValue, s.RxSpeed, labels...)
ch <- prometheus.MustNewConstMetric(c.txSpeedBpsDesc, prometheus.GaugeValue, s.TxSpeed, labels...)
}
return nil
}

View File

@@ -0,0 +1,12 @@
package collector
import (
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/device"
"github.com/prometheus/client_golang/prometheus"
)
type Collector interface {
Name() string
Collect(dev *device.Device, ch chan<- prometheus.Metric) error
Describe(ch chan<- *prometheus.Desc)
}

View File

@@ -0,0 +1,101 @@
package collector
import (
"context"
"fmt"
"log"
"time"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/device"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/keenetic"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
type InternetCollector struct {
enabledDesc *prometheus.Desc
reliableDesc *prometheus.Desc
gatewayAccessibleDesc *prometheus.Desc
dnsAccessibleDesc *prometheus.Desc
captiveAccessibleDesc *prometheus.Desc
internetDesc *prometheus.Desc
gatewayFailuresDesc *prometheus.Desc
captiveFailuresDesc *prometheus.Desc
}
func NewInternetCollector() *InternetCollector {
labels := []string{"device", "interface"}
return &InternetCollector{
enabledDesc: prometheus.NewDesc("keenetic_internet_enabled", "Internet enabled", labels, nil),
reliableDesc: prometheus.NewDesc("keenetic_internet_reliable", "Internet reliable", labels, nil),
gatewayAccessibleDesc: prometheus.NewDesc("keenetic_gateway_accessible", "Gateway accessible", labels, nil),
dnsAccessibleDesc: prometheus.NewDesc("keenetic_dns_accessible", "DNS accessible", labels, nil),
captiveAccessibleDesc: prometheus.NewDesc("keenetic_captive_accessible", "Captive portal accessible", labels, nil),
internetDesc: prometheus.NewDesc("keenetic_internet_available", "Internet available", labels, nil),
gatewayFailuresDesc: prometheus.NewDesc("keenetic_gateway_failures", "Gateway access failure count", labels, nil),
captiveFailuresDesc: prometheus.NewDesc("keenetic_captive_failures", "Captive portal failure count", labels, nil),
}
}
func (c *InternetCollector) Name() string {
return "internet"
}
func (c *InternetCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.enabledDesc
ch <- c.reliableDesc
ch <- c.gatewayAccessibleDesc
ch <- c.dnsAccessibleDesc
ch <- c.captiveAccessibleDesc
ch <- c.internetDesc
ch <- c.gatewayFailuresDesc
ch <- c.captiveFailuresDesc
}
func (c *InternetCollector) Collect(dev *device.Device, ch chan<- prometheus.Metric) error {
var internetInfo any
var ok bool
hostname := dev.Name
client := dev.Client
dev.CacheMutex.RLock()
internetInfo, ok = dev.Cache["internet"]
valid := time.Since(dev.LastUpdate) < dev.CacheTTL
dev.CacheMutex.RUnlock()
if !ok || !valid {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
fresh, err := client.GetInternetStatus(ctx)
cancel()
if err != nil {
log.Printf("failed to get internet status from %s: %v", hostname, err)
return err
}
internetInfo = fresh
dev.CacheMutex.Lock()
dev.Cache["internet"] = fresh
dev.LastUpdate = time.Now()
dev.CacheMutex.Unlock()
}
status, ok := internetInfo.(*keenetic.InternetStatus)
if !ok || status == nil {
log.Printf("invalid cache data type for internet status on %s", hostname)
return fmt.Errorf("invalid cache data type for internet status")
}
iface := status.Gateway.Interface
labels := []string{hostname, iface}
ch <- prometheus.MustNewConstMetric(c.enabledDesc, prometheus.GaugeValue, utils.BoolToFloat(status.Enabled), labels...)
ch <- prometheus.MustNewConstMetric(c.reliableDesc, prometheus.GaugeValue, utils.BoolToFloat(status.Reliable), labels...)
ch <- prometheus.MustNewConstMetric(c.gatewayAccessibleDesc, prometheus.GaugeValue, utils.BoolToFloat(status.GatewayAccessible), labels...)
ch <- prometheus.MustNewConstMetric(c.dnsAccessibleDesc, prometheus.GaugeValue, utils.BoolToFloat(status.DNSAccessible), labels...)
ch <- prometheus.MustNewConstMetric(c.captiveAccessibleDesc, prometheus.GaugeValue, utils.BoolToFloat(status.CaptiveAccessible), labels...)
ch <- prometheus.MustNewConstMetric(c.internetDesc, prometheus.GaugeValue, utils.BoolToFloat(status.Internet), labels...)
ch <- prometheus.MustNewConstMetric(c.gatewayFailuresDesc, prometheus.GaugeValue, status.Gateway.Failures, labels...)
ch <- prometheus.MustNewConstMetric(c.captiveFailuresDesc, prometheus.GaugeValue, status.Captive.Failures, labels...)
return nil
}

View File

@@ -0,0 +1,96 @@
package collector
import (
"context"
"fmt"
"log"
"strconv"
"strings"
"time"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/device"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/keenetic"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/utils"
"github.com/prometheus/client_golang/prometheus"
)
type ProcessCollector struct {
cpuDesc *prometheus.Desc
vmSizeDesc *prometheus.Desc
vmRSSDesc *prometheus.Desc
threadsDesc *prometheus.Desc
fdsDesc *prometheus.Desc
}
func NewProcessCollector() *ProcessCollector {
labels := []string{"device", "comm", "pid"}
return &ProcessCollector{
cpuDesc: prometheus.NewDesc("keenetic_process_cpu_seconds", "CPU usage of the process", labels, nil),
vmSizeDesc: prometheus.NewDesc("keenetic_process_memory_virtual_bytes", "Virtual memory size in bytes", labels, nil),
vmRSSDesc: prometheus.NewDesc("keenetic_process_memory_resident_bytes", "Resident memory size in bytes", labels, nil),
threadsDesc: prometheus.NewDesc("keenetic_process_threads", "Number of threads", labels, nil),
fdsDesc: prometheus.NewDesc("keenetic_process_fds", "Number of open file descriptors", labels, nil),
}
}
func (c *ProcessCollector) Name() string {
return "process"
}
func (c *ProcessCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.cpuDesc
ch <- c.vmSizeDesc
ch <- c.vmRSSDesc
ch <- c.threadsDesc
ch <- c.fdsDesc
}
func (c *ProcessCollector) Collect(dev *device.Device, ch chan<- prometheus.Metric) error {
var processInfo any
var ok bool
hostname := dev.Name
client := dev.Client
dev.CacheMutex.RLock()
processInfo, ok = dev.Cache["process"]
valid := time.Since(dev.LastUpdate) < dev.CacheTTL
dev.CacheMutex.RUnlock()
if !ok || !valid {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
fresh, err := client.GetProcessInfo(ctx)
cancel()
if err != nil {
log.Printf("failed to get process info from %s: %v", hostname, err)
return err
}
processInfo = fresh
dev.CacheMutex.Lock()
dev.Cache["process"] = fresh
dev.LastUpdate = time.Now()
dev.CacheMutex.Unlock()
}
procs, ok := processInfo.([]*keenetic.ProcessInfo)
if !ok {
log.Printf("invalid cache data type for process info on %s", hostname)
return fmt.Errorf("invalid cache data type for process info")
}
for _, p := range procs {
labels := []string{hostname, p.Comm, p.Pid}
vmSize := utils.ParseKB(p.VMSize) * 1024
vmRSS := utils.ParseKB(p.VMRSS) * 1024
threads, _ := strconv.Atoi(strings.TrimSpace(p.Threads))
ch <- prometheus.MustNewConstMetric(c.cpuDesc, prometheus.CounterValue, p.Statistics.CPU.Cur, labels...)
ch <- prometheus.MustNewConstMetric(c.vmSizeDesc, prometheus.GaugeValue, vmSize, labels...)
ch <- prometheus.MustNewConstMetric(c.vmRSSDesc, prometheus.GaugeValue, vmRSS, labels...)
ch <- prometheus.MustNewConstMetric(c.threadsDesc, prometheus.GaugeValue, float64(threads), labels...)
ch <- prometheus.MustNewConstMetric(c.fdsDesc, prometheus.GaugeValue, p.Fds, labels...)
}
return nil
}

View File

@@ -0,0 +1,31 @@
package collector
import (
"sync"
)
type Registry struct {
collectors []Collector
mutex sync.RWMutex
}
func NewRegistry() *Registry {
return &Registry{
collectors: make([]Collector, 0),
}
}
func (r *Registry) Register(collector Collector) {
r.mutex.Lock()
defer r.mutex.Unlock()
r.collectors = append(r.collectors, collector)
}
func (r *Registry) GetCollectors() []Collector {
r.mutex.RLock()
defer r.mutex.RUnlock()
result := make([]Collector, len(r.collectors))
copy(result, r.collectors)
return result
}

View File

@@ -0,0 +1,112 @@
package collector
import (
"context"
"fmt"
"log"
"strconv"
"time"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/device"
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/keenetic"
"github.com/prometheus/client_golang/prometheus"
)
type SystemCollector struct {
cpuLoadDesc *prometheus.Desc
memTotalDesc *prometheus.Desc
memFreeDesc *prometheus.Desc
memCacheDesc *prometheus.Desc
memBuffersDesc *prometheus.Desc
swapTotalDesc *prometheus.Desc
swapFreeDesc *prometheus.Desc
connTotalDesc *prometheus.Desc
connFreeDesc *prometheus.Desc
uptimeDesc *prometheus.Desc
}
func NewSystemCollector() *SystemCollector {
labels := []string{"device"}
return &SystemCollector{
cpuLoadDesc: prometheus.NewDesc("keenetic_cpu_load", "Current CPU load", labels, nil),
memTotalDesc: prometheus.NewDesc("keenetic_memory_total_bytes", "Total memory in bytes", labels, nil),
memFreeDesc: prometheus.NewDesc("keenetic_memory_free_bytes", "Free memory in bytes", labels, nil),
memCacheDesc: prometheus.NewDesc("keenetic_memory_cache_bytes", "Cache memory in bytes", labels, nil),
memBuffersDesc: prometheus.NewDesc("keenetic_memory_buffers_bytes", "Buffer memory in bytes", labels, nil),
swapTotalDesc: prometheus.NewDesc("keenetic_swap_total_bytes", "Total swap in bytes", labels, nil),
swapFreeDesc: prometheus.NewDesc("keenetic_swap_free_bytes", "Free swap in bytes", labels, nil),
connTotalDesc: prometheus.NewDesc("keenetic_connections_total", "Total number of connections", labels, nil),
connFreeDesc: prometheus.NewDesc("keenetic_connections_free", "Number of free connections", labels, nil),
uptimeDesc: prometheus.NewDesc("keenetic_uptime_seconds", "Device uptime in seconds", labels, nil),
}
}
func (c *SystemCollector) Name() string {
return "system"
}
func (c *SystemCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- c.cpuLoadDesc
ch <- c.memTotalDesc
ch <- c.memFreeDesc
ch <- c.memCacheDesc
ch <- c.memBuffersDesc
ch <- c.swapTotalDesc
ch <- c.swapFreeDesc
ch <- c.connTotalDesc
ch <- c.connFreeDesc
ch <- c.uptimeDesc
}
func (c *SystemCollector) Collect(dev *device.Device, ch chan<- prometheus.Metric) error {
var sysInfo any
var ok bool
hostname := dev.Name
client := dev.Client
dev.CacheMutex.RLock()
sysInfo, ok = dev.Cache["system"]
valid := time.Since(dev.LastUpdate) < dev.CacheTTL
dev.CacheMutex.RUnlock()
if !ok || !valid {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
fresh, err := client.GetSystemInfo(ctx)
cancel()
if err != nil {
log.Printf("Failed to get system info from %s: %v", hostname, err)
return err
}
sysInfo = fresh
dev.CacheMutex.Lock()
dev.Cache["system"] = fresh
dev.LastUpdate = time.Now()
dev.CacheMutex.Unlock()
}
s, ok := sysInfo.(*keenetic.SystemInfo)
if !ok || s == nil {
log.Printf("invalid cache data type for system info on %s", hostname)
return fmt.Errorf("invalid cache data type for system info")
}
uptime, err := strconv.ParseFloat(s.Uptime, 64)
if err != nil {
log.Printf("failed to parse uptime for %s: %v", hostname, err)
return fmt.Errorf("failed to parse uptime: %w", err)
}
ch <- prometheus.MustNewConstMetric(c.cpuLoadDesc, prometheus.GaugeValue, s.CpuLoad, hostname)
ch <- prometheus.MustNewConstMetric(c.memTotalDesc, prometheus.GaugeValue, s.MemTotal, hostname)
ch <- prometheus.MustNewConstMetric(c.memFreeDesc, prometheus.GaugeValue, s.MemFree, hostname)
ch <- prometheus.MustNewConstMetric(c.memCacheDesc, prometheus.GaugeValue, s.MemCache, hostname)
ch <- prometheus.MustNewConstMetric(c.memBuffersDesc, prometheus.GaugeValue, s.MemBuffers, hostname)
ch <- prometheus.MustNewConstMetric(c.swapTotalDesc, prometheus.GaugeValue, s.SwapTotal, hostname)
ch <- prometheus.MustNewConstMetric(c.swapFreeDesc, prometheus.GaugeValue, s.SwapFree, hostname)
ch <- prometheus.MustNewConstMetric(c.connTotalDesc, prometheus.GaugeValue, s.ConnTotal, hostname)
ch <- prometheus.MustNewConstMetric(c.connFreeDesc, prometheus.GaugeValue, s.ConnFree, hostname)
ch <- prometheus.MustNewConstMetric(c.uptimeDesc, prometheus.CounterValue, uptime, hostname)
return nil
}