initial commit
This commit is contained in:
72
internal/app/coordinator.go
Normal file
72
internal/app/coordinator.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/collector"
|
||||
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/device"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type Coordinator struct {
|
||||
deviceManager *device.Manager
|
||||
registry *collector.Registry
|
||||
}
|
||||
|
||||
func NewCoordinator(dm *device.Manager, reg *collector.Registry) *Coordinator {
|
||||
return &Coordinator{
|
||||
deviceManager: dm,
|
||||
registry: reg,
|
||||
}
|
||||
}
|
||||
|
||||
// Реализация интерфейса prometheus.Collector
|
||||
func (c *Coordinator) Describe(ch chan<- *prometheus.Desc) {
|
||||
collectors := c.registry.GetCollectors()
|
||||
for _, col := range collectors {
|
||||
col.Describe(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Coordinator) Collect(ch chan<- prometheus.Metric) {
|
||||
c.Scrape(ch)
|
||||
}
|
||||
|
||||
func (c *Coordinator) Scrape(ch chan<- prometheus.Metric) {
|
||||
devices := c.deviceManager.GetDevices()
|
||||
collectors := c.registry.GetCollectors()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, d := range devices {
|
||||
wg.Add(1)
|
||||
go func(dev *device.Device) {
|
||||
defer wg.Done()
|
||||
c.scrapeDevice(dev, collectors, ch)
|
||||
}(d)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (c *Coordinator) scrapeDevice(dev *device.Device, collectors []collector.Collector, ch chan<- prometheus.Metric) {
|
||||
for _, col := range collectors {
|
||||
if shouldSkipCollector(dev, col.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := col.Collect(dev, ch); err != nil {
|
||||
// TODO: handle error metric
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shouldSkipCollector(dev *device.Device, name string) bool {
|
||||
for _, skip := range dev.SkipCollectors {
|
||||
if skip == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
54
internal/app/prometheus.go
Normal file
54
internal/app/prometheus.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
type PrometheusServer struct {
|
||||
port string
|
||||
coordinator *Coordinator
|
||||
server *http.Server
|
||||
//registry *prometheus.Registry
|
||||
}
|
||||
|
||||
func NewPrometheusServer(port string, coordinator *Coordinator) *PrometheusServer {
|
||||
// Создаем собственный реестр метрик
|
||||
// registry := prometheus.NewRegistry()
|
||||
|
||||
return &PrometheusServer{
|
||||
port: port,
|
||||
coordinator: coordinator,
|
||||
//registry: registry,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PrometheusServer) Start(ctx context.Context) error {
|
||||
// Регистрируем coordinator как коллектор
|
||||
// s.registry.MustRegister(s.coordinator)
|
||||
prometheus.MustRegister(s.coordinator)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
// Используем наш реестр вместо глобального
|
||||
// mux.Handle("/metrics", promhttp.HandlerFor(s.registry, promhttp.HandlerOpts{}))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
})
|
||||
|
||||
s.server = &http.Server{
|
||||
Addr: ":" + s.port,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.server.Shutdown(context.Background())
|
||||
}()
|
||||
|
||||
return s.server.ListenAndServe()
|
||||
}
|
||||
92
internal/collector/hotspot.go
Normal file
92
internal/collector/hotspot.go
Normal 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
111
internal/collector/iface.go
Normal 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
|
||||
}
|
||||
123
internal/collector/ifacestats.go
Normal file
123
internal/collector/ifacestats.go
Normal 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
|
||||
}
|
||||
12
internal/collector/interface.go
Normal file
12
internal/collector/interface.go
Normal 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)
|
||||
}
|
||||
101
internal/collector/internet.go
Normal file
101
internal/collector/internet.go
Normal 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
|
||||
}
|
||||
96
internal/collector/process.go
Normal file
96
internal/collector/process.go
Normal 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
|
||||
}
|
||||
31
internal/collector/registry.go
Normal file
31
internal/collector/registry.go
Normal 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
|
||||
}
|
||||
112
internal/collector/system.go
Normal file
112
internal/collector/system.go
Normal 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
|
||||
}
|
||||
41
internal/config/config.go
Normal file
41
internal/config/config.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Devices []DeviceConfig `yaml:"devices"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string `yaml:"port"`
|
||||
}
|
||||
|
||||
type DeviceConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Address string `yaml:"address"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Timeout string `yaml:"timeout"`
|
||||
SkipCollectors []string `yaml:"skip_collectors"`
|
||||
UpdateInterval string `yaml:"update_interval"` // например, "30s"
|
||||
CacheTTL string `yaml:"cache_ttl"` // например, "1m"
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
216
internal/device/manager.go
Normal file
216
internal/device/manager.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package device
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/config"
|
||||
"gitea.sinav-lab.com/sinav/keenetic-exporter-v2/internal/keenetic"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
Name string
|
||||
Client *keenetic.Client
|
||||
SkipCollectors []string
|
||||
|
||||
UpdateInterval time.Duration
|
||||
CacheTTL time.Duration
|
||||
|
||||
Cache map[string]any
|
||||
CacheMutex sync.RWMutex
|
||||
LastUpdate time.Time
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
devices []*Device
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewManager(configs []config.DeviceConfig) *Manager {
|
||||
m := &Manager{}
|
||||
|
||||
for _, cfg := range configs {
|
||||
client, err := keenetic.NewClient(cfg.Address)
|
||||
if err != nil {
|
||||
log.Printf("Failed to create client for %s: %v", cfg.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
timeout, err := time.ParseDuration(cfg.Timeout)
|
||||
if err != nil {
|
||||
timeout = 10 * time.Second
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
err = client.Init(ctx, cfg.Username, cfg.Password)
|
||||
cancel()
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed to initialize client for %s: %v", cfg.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
updateInterval, _ := time.ParseDuration(cfg.UpdateInterval)
|
||||
cacheTTL, _ := time.ParseDuration(cfg.CacheTTL)
|
||||
|
||||
deviceCtx, deviceCancel := context.WithCancel(context.Background())
|
||||
device := &Device{
|
||||
Name: cfg.Name,
|
||||
Client: client,
|
||||
SkipCollectors: cfg.SkipCollectors,
|
||||
UpdateInterval: updateInterval,
|
||||
CacheTTL: cacheTTL,
|
||||
Cache: make(map[string]any),
|
||||
ctx: deviceCtx,
|
||||
cancel: deviceCancel,
|
||||
}
|
||||
|
||||
m.devices = append(m.devices, device)
|
||||
log.Printf("Successfully initialized device: %s (hostname: %s)", cfg.Name, client.Hostname)
|
||||
|
||||
// Запуск фонового обновления
|
||||
device.wg.Add(1)
|
||||
go device.startBackgroundUpdater()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) GetDevices() []*Device {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
result := make([]*Device, len(m.devices))
|
||||
copy(result, m.devices)
|
||||
return result
|
||||
}
|
||||
|
||||
func (m *Manager) GetDevice(name string) (*Device, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for i := range m.devices {
|
||||
if m.devices[i].Name == name {
|
||||
return m.devices[i], nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("device %s not found", name)
|
||||
}
|
||||
|
||||
func (m *Manager) Shutdown(ctx context.Context) error {
|
||||
m.mutex.RLock()
|
||||
devices := m.devices
|
||||
m.mutex.RUnlock()
|
||||
|
||||
log.Printf("shutting down %d device(s)...", len(devices))
|
||||
|
||||
// Cancel all device contexts
|
||||
for _, dev := range devices {
|
||||
if dev.cancel != nil {
|
||||
dev.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all background updaters to finish with timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for _, dev := range devices {
|
||||
dev.wg.Wait()
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
log.Println("all background updaters stopped successfully")
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("shutdown timeout exceeded")
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) startBackgroundUpdater() {
|
||||
defer d.wg.Done()
|
||||
|
||||
if d.UpdateInterval <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("starting background updater for device %s (interval: %s)", d.Name, d.UpdateInterval)
|
||||
|
||||
ticker := time.NewTicker(d.UpdateInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
d.updateCache()
|
||||
|
||||
case <-d.ctx.Done():
|
||||
log.Printf("stopping background updater for device %s", d.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) updateCache() {
|
||||
collectors := map[string]func(context.Context) (any, error){
|
||||
"system": func(ctx context.Context) (any, error) {
|
||||
return d.Client.GetSystemInfo(ctx)
|
||||
},
|
||||
"interface": func(ctx context.Context) (any, error) {
|
||||
return d.Client.GetInterfaceInfo(ctx)
|
||||
},
|
||||
"internet": func(ctx context.Context) (any, error) {
|
||||
return d.Client.GetInternetStatus(ctx)
|
||||
},
|
||||
"hotspot": func(ctx context.Context) (any, error) {
|
||||
return d.Client.GetHotspotClientInfo(ctx)
|
||||
},
|
||||
"interface_stats": func(ctx context.Context) (any, error) {
|
||||
return d.Client.GetConnectedInterfaceStats(ctx)
|
||||
},
|
||||
"process": func(ctx context.Context) (any, error) {
|
||||
return d.Client.GetProcessInfo(ctx)
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
updatedCount := 0
|
||||
for name, fetchFn := range collectors {
|
||||
if d.shouldSkipCollector(name) {
|
||||
continue
|
||||
}
|
||||
|
||||
data, err := fetchFn(ctx)
|
||||
if err != nil {
|
||||
log.Printf("background update failed for device %s, collector %s: %v", d.Name, name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
d.CacheMutex.Lock()
|
||||
d.Cache[name] = data
|
||||
d.LastUpdate = time.Now()
|
||||
d.CacheMutex.Unlock()
|
||||
|
||||
updatedCount++
|
||||
}
|
||||
|
||||
if updatedCount > 0 {
|
||||
log.Printf("background update completed for device %s: %d collector(s) updated", d.Name, updatedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) shouldSkipCollector(name string) bool {
|
||||
return slices.Contains(d.SkipCollectors, name)
|
||||
}
|
||||
364
internal/keenetic/client.go
Normal file
364
internal/keenetic/client.go
Normal file
@@ -0,0 +1,364 @@
|
||||
package keenetic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type InternetStatus struct {
|
||||
Checked string `json:"checked"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Reliable bool `json:"reliable"`
|
||||
GatewayAccessible bool `json:"gateway-accessible"`
|
||||
DNSAccessible bool `json:"dns-accessible"`
|
||||
CaptiveAccessible bool `json:"captive-accessible"`
|
||||
Internet bool `json:"internet"`
|
||||
Gateway struct {
|
||||
Interface string `json:"interface"`
|
||||
Address string `json:"address"`
|
||||
Failures float64 `json:"failures"`
|
||||
Accessible bool `json:"accessible"`
|
||||
Excluded bool `json:"excluded"`
|
||||
} `json:"gateway"`
|
||||
Captive struct {
|
||||
Host string `json:"host"`
|
||||
Response string `json:"response"`
|
||||
Location string `json:"location"`
|
||||
Failures float64 `json:"failures"`
|
||||
Resolved bool `json:"resolved"`
|
||||
Address string `json:"address"`
|
||||
} `json:"captive"`
|
||||
}
|
||||
|
||||
type SystemInfo struct {
|
||||
ConnFree float64 `json:"connfree"`
|
||||
ConnTotal float64 `json:"conntotal"`
|
||||
CpuLoad float64 `json:"cpuload"`
|
||||
MemBuffers float64 `json:"membuffers"`
|
||||
MemCache float64 `json:"memcache"`
|
||||
MemFree float64 `json:"memfree"`
|
||||
MemTotal float64 `json:"memtotal"`
|
||||
SwapFree float64 `json:"swapfree"`
|
||||
SwapTotal float64 `json:"swaptotal"`
|
||||
Hostname string `json:"hostname"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
type ProcessCPUStat struct {
|
||||
Cur float64 `json:"cur"`
|
||||
}
|
||||
|
||||
type ProcessStats struct {
|
||||
CPU ProcessCPUStat `json:"cpu"`
|
||||
}
|
||||
|
||||
type ProcessInfo struct {
|
||||
Comm string `json:"comm"`
|
||||
Pid string `json:"pid"`
|
||||
Threads string `json:"threads"`
|
||||
Fds float64 `json:"fds"`
|
||||
VMSize string `json:"vm-size"`
|
||||
VMRSS string `json:"vm-rss"`
|
||||
Statistics ProcessStats `json:"statistics"`
|
||||
}
|
||||
|
||||
type HotspotClientInfo struct {
|
||||
MAC string `json:"mac"`
|
||||
IP string `json:"ip"`
|
||||
Name string `json:"name"`
|
||||
SSID string `json:"ssid"`
|
||||
Registered bool `json:"registered"`
|
||||
RXBytes float64 `json:"rxbytes"`
|
||||
TXBytes float64 `json:"txbytes"`
|
||||
TXRate float64 `json:"txrate"`
|
||||
RSSI float64 `json:"rssi"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
}
|
||||
|
||||
type InterfaceInfo struct {
|
||||
ID string `json:"id"`
|
||||
InterfaceName string `json:"interface-name"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Link string `json:"link"`
|
||||
Connected string `json:"connected"`
|
||||
State string `json:"state"`
|
||||
MTU float64 `json:"mtu"`
|
||||
TxQueueLength float64 `json:"tx-queue-length"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
MAC string `json:"mac"`
|
||||
Channel float64 `json:"channel"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
}
|
||||
|
||||
type InterfaceStats struct {
|
||||
RxPackets float64 `json:"rxpackets"`
|
||||
RxMulticastPackets float64 `json:"rx-multicast-packets"`
|
||||
RxBroadcastPackets float64 `json:"rx-broadcast-packets"`
|
||||
RxBytes float64 `json:"rxbytes"`
|
||||
RxErrors float64 `json:"rxerrors"`
|
||||
RxDropped float64 `json:"rxdropped"`
|
||||
TxPackets float64 `json:"txpackets"`
|
||||
TxMulticastPackets float64 `json:"tx-multicast-packets"`
|
||||
TxBroadcastPackets float64 `json:"tx-broadcast-packets"`
|
||||
TxBytes float64 `json:"txbytes"`
|
||||
TxErrors float64 `json:"txerrors"`
|
||||
TxDropped float64 `json:"txdropped"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
LastOverflow string `json:"last-overflow"`
|
||||
RxSpeed float64 `json:"rxspeed"`
|
||||
TxSpeed float64 `json:"txspeed"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL *url.URL
|
||||
login string
|
||||
password string
|
||||
Hostname string
|
||||
}
|
||||
|
||||
func NewClient(rawurl string) (*Client, error) {
|
||||
parsed, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid base URL: %w", err)
|
||||
}
|
||||
jar, _ := cookiejar.New(nil)
|
||||
return &Client{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Jar: jar,
|
||||
},
|
||||
baseURL: parsed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type authRequest struct {
|
||||
Login string `json:"login"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (c *Client) Init(ctx context.Context, login, password string) error {
|
||||
if err := c.authenticate(ctx, login, password); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.fetchHostname(ctx)
|
||||
}
|
||||
|
||||
func (c *Client) authenticate(ctx context.Context, login, password string) error {
|
||||
c.login = login
|
||||
c.password = password
|
||||
|
||||
authURL := c.baseURL.ResolveReference(&url.URL{Path: "/auth"})
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authURL.String(), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating auth GET request: %w", err)
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auth GET failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil // already authenticated
|
||||
}
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
return fmt.Errorf("unexpected auth GET status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
realm := resp.Header.Get("X-NDM-Realm")
|
||||
challenge := resp.Header.Get("X-NDM-Challenge")
|
||||
if realm == "" || challenge == "" {
|
||||
return fmt.Errorf("missing challenge headers")
|
||||
}
|
||||
|
||||
// compute: sha256(challenge + md5(login:realm:password))
|
||||
h := md5.Sum([]byte(fmt.Sprintf("%s:%s:%s", login, realm, password)))
|
||||
md5hex := hex.EncodeToString(h[:])
|
||||
s := sha256.Sum256([]byte(challenge + md5hex))
|
||||
shahex := hex.EncodeToString(s[:])
|
||||
|
||||
reqBody := authRequest{Login: login, Password: shahex}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(buf).Encode(reqBody); err != nil {
|
||||
return fmt.Errorf("encode auth body: %w", err)
|
||||
}
|
||||
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, authURL.String(), buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating auth POST request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err = c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("auth POST failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("auth failed, status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) fetchHostname(ctx context.Context) error {
|
||||
sys, err := c.GetSystemInfo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Hostname = sys.Hostname
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetJSON(ctx context.Context, path string, out any) error {
|
||||
relURL, err := url.Parse(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing path %q: %w", path, err)
|
||||
}
|
||||
fullURL := c.baseURL.ResolveReference(relURL)
|
||||
|
||||
doRequest := func() (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating GET request: %w", err)
|
||||
}
|
||||
return c.httpClient.Do(req)
|
||||
}
|
||||
|
||||
resp, err := doRequest()
|
||||
if err != nil {
|
||||
return fmt.Errorf("GET request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
_ = resp.Body.Close() // закрываем перед повтором
|
||||
|
||||
if err := c.authenticate(ctx, c.login, c.password); err != nil {
|
||||
return fmt.Errorf("re-authentication failed: %w", err)
|
||||
}
|
||||
|
||||
resp, err = doRequest()
|
||||
if err != nil {
|
||||
return fmt.Errorf("GET retry failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
data, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("GET %s failed: status %d, body: %s", fullURL.String(), resp.StatusCode, data)
|
||||
}
|
||||
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
func (c *Client) GetInternetStatus(ctx context.Context) (*InternetStatus, error) {
|
||||
var status InternetStatus
|
||||
if err := c.GetJSON(ctx, "/rci/show/internet/status", &status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetSystemInfo(ctx context.Context) (*SystemInfo, error) {
|
||||
var sys SystemInfo
|
||||
if err := c.GetJSON(ctx, "/rci/show/system", &sys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sys, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetProcessInfo(ctx context.Context) ([]*ProcessInfo, error) {
|
||||
var raw struct {
|
||||
Process []json.RawMessage `json:"process"`
|
||||
}
|
||||
if err := c.GetJSON(ctx, "/rci/show/processes", &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result []*ProcessInfo
|
||||
for _, item := range raw.Process {
|
||||
var p ProcessInfo
|
||||
if err := json.Unmarshal(item, &p); err != nil {
|
||||
continue
|
||||
}
|
||||
if p.Pid == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, &p)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetHotspotClientInfo(ctx context.Context) ([]*HotspotClientInfo, error) {
|
||||
var raw struct {
|
||||
Host []*HotspotClientInfo `json:"host"`
|
||||
}
|
||||
if err := c.GetJSON(ctx, "/rci/show/ip/hotspot", &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return raw.Host, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetInterfaceInfo(ctx context.Context) (map[string]*InterfaceInfo, error) {
|
||||
var result map[string]*InterfaceInfo
|
||||
if err := c.GetJSON(ctx, "/rci/show/interface", &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetInterfaceStats(ctx context.Context, name string) (*InterfaceStats, error) {
|
||||
path := fmt.Sprintf("/rci/show/interface/stat?name=%s", url.QueryEscape(name))
|
||||
var stats InterfaceStats
|
||||
if err := c.GetJSON(ctx, path, &stats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetConnectedInterfaceStats(ctx context.Context) (map[string]*InterfaceStats, error) {
|
||||
interfaces, err := c.GetInterfaceInfo(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get interfaces: %w", err)
|
||||
}
|
||||
|
||||
statsMap := make(map[string]*InterfaceStats)
|
||||
|
||||
for _, iface := range interfaces {
|
||||
if iface.InterfaceName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if iface.Connected == "no" {
|
||||
continue
|
||||
}
|
||||
|
||||
stats, err := c.GetInterfaceStats(ctx, iface.InterfaceName)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
statsMap[iface.InterfaceName] = stats
|
||||
}
|
||||
|
||||
return statsMap, nil
|
||||
}
|
||||
58
internal/utils/utils.go
Normal file
58
internal/utils/utils.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseKB(s string) float64 {
|
||||
s = strings.TrimSpace(strings.TrimSuffix(s, "kB"))
|
||||
n, _ := strconv.ParseFloat(s, 64)
|
||||
return n
|
||||
}
|
||||
|
||||
func IntFrom(v any) int {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return int(val)
|
||||
case int:
|
||||
return val
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func StrFrom(v any) string {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func BoolToFloat(b bool) float64 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func FloatFrom(v any) float64 {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
return val
|
||||
case int:
|
||||
return float64(val)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func KbToInt(v any) int {
|
||||
if s, ok := v.(string); ok {
|
||||
var i int
|
||||
fmt.Sscanf(s, "%d", &i)
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user