258 lines
6.9 KiB
Go
258 lines
6.9 KiB
Go
package speedport
|
|
|
|
import (
|
|
"log/slog"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
)
|
|
|
|
const namespace = "speedport"
|
|
|
|
// Exporter collects Speedport metrics and exposes them as Prometheus metrics.
|
|
type Exporter struct {
|
|
client *Client
|
|
logger *slog.Logger
|
|
|
|
mu sync.Mutex
|
|
scrapeErrors prometheus.Counter
|
|
scrapeDuration prometheus.Gauge
|
|
up prometheus.Gauge
|
|
|
|
// DSL metrics
|
|
dslUpRate *prometheus.GaugeVec
|
|
dslDownRate *prometheus.GaugeVec
|
|
dslSNR *prometheus.GaugeVec
|
|
dslAttenuation *prometheus.GaugeVec
|
|
dslPower *prometheus.GaugeVec
|
|
dslErrors *prometheus.GaugeVec
|
|
dslUptime prometheus.Gauge
|
|
|
|
// Connection metrics
|
|
onlineStatus prometheus.Gauge
|
|
|
|
// Generic metric for all raw values
|
|
rawValues *prometheus.GaugeVec
|
|
rawInfo *prometheus.GaugeVec
|
|
}
|
|
|
|
// NewExporter creates a new Speedport Prometheus exporter.
|
|
func NewExporter(client *Client, logger *slog.Logger) *Exporter {
|
|
return &Exporter{
|
|
client: client,
|
|
logger: logger,
|
|
|
|
scrapeErrors: prometheus.NewCounter(prometheus.CounterOpts{
|
|
Namespace: namespace,
|
|
Name: "scrape_errors_total",
|
|
Help: "Total number of scrape errors.",
|
|
}),
|
|
scrapeDuration: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Name: "scrape_duration_seconds",
|
|
Help: "Duration of the last scrape in seconds.",
|
|
}),
|
|
up: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Name: "up",
|
|
Help: "Whether the Speedport is reachable (1=up, 0=down).",
|
|
}),
|
|
|
|
dslUpRate: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "dsl",
|
|
Name: "rate_kbps",
|
|
Help: "DSL line rate in kbit/s.",
|
|
}, []string{"direction", "type"}),
|
|
|
|
dslSNR: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "dsl",
|
|
Name: "snr_db",
|
|
Help: "DSL signal-to-noise ratio margin in dB.",
|
|
}, []string{"direction"}),
|
|
|
|
dslAttenuation: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "dsl",
|
|
Name: "attenuation_db",
|
|
Help: "DSL line attenuation in dB.",
|
|
}, []string{"direction"}),
|
|
|
|
dslPower: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "dsl",
|
|
Name: "power_dbm",
|
|
Help: "DSL signal power in dBm.",
|
|
}, []string{"direction"}),
|
|
|
|
dslErrors: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "dsl",
|
|
Name: "errors_total",
|
|
Help: "DSL error counters (CRC, FEC, HEC).",
|
|
}, []string{"direction", "type"}),
|
|
|
|
dslUptime: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Subsystem: "dsl",
|
|
Name: "uptime_seconds",
|
|
Help: "Internet connection uptime in seconds.",
|
|
}),
|
|
|
|
onlineStatus: prometheus.NewGauge(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Name: "online_status",
|
|
Help: "Online status (1=online, 0=offline).",
|
|
}),
|
|
|
|
rawValues: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Name: "raw_value",
|
|
Help: "Raw numeric values from the router status page.",
|
|
}, []string{"varid", "vartype"}),
|
|
|
|
rawInfo: prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
|
Namespace: namespace,
|
|
Name: "info",
|
|
Help: "Router information as labels (value is always 1).",
|
|
}, []string{"device_name", "firmware_version", "serial_number", "dsl_mode"}),
|
|
}
|
|
}
|
|
|
|
// Describe implements prometheus.Collector.
|
|
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
|
|
e.scrapeErrors.Describe(ch)
|
|
e.scrapeDuration.Describe(ch)
|
|
e.up.Describe(ch)
|
|
e.dslUpRate.Describe(ch)
|
|
e.dslSNR.Describe(ch)
|
|
e.dslAttenuation.Describe(ch)
|
|
e.dslPower.Describe(ch)
|
|
e.dslErrors.Describe(ch)
|
|
e.dslUptime.Describe(ch)
|
|
e.onlineStatus.Describe(ch)
|
|
e.rawValues.Describe(ch)
|
|
e.rawInfo.Describe(ch)
|
|
}
|
|
|
|
// Collect implements prometheus.Collector.
|
|
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
start := time.Now()
|
|
|
|
// Reset vector metrics to avoid stale entries
|
|
e.dslUpRate.Reset()
|
|
e.dslSNR.Reset()
|
|
e.dslAttenuation.Reset()
|
|
e.dslPower.Reset()
|
|
e.dslErrors.Reset()
|
|
e.rawValues.Reset()
|
|
e.rawInfo.Reset()
|
|
|
|
status, err := e.client.GetStatus()
|
|
if err != nil {
|
|
e.logger.Error("failed to fetch status", "error", err)
|
|
e.scrapeErrors.Inc()
|
|
e.up.Set(0)
|
|
} else {
|
|
e.up.Set(1)
|
|
e.processStatus(status)
|
|
}
|
|
|
|
e.scrapeDuration.Set(time.Since(start).Seconds())
|
|
|
|
// Collect all metrics
|
|
e.scrapeErrors.Collect(ch)
|
|
e.scrapeDuration.Collect(ch)
|
|
e.up.Collect(ch)
|
|
e.dslUpRate.Collect(ch)
|
|
e.dslSNR.Collect(ch)
|
|
e.dslAttenuation.Collect(ch)
|
|
e.dslPower.Collect(ch)
|
|
e.dslErrors.Collect(ch)
|
|
e.dslUptime.Collect(ch)
|
|
e.onlineStatus.Collect(ch)
|
|
e.rawValues.Collect(ch)
|
|
e.rawInfo.Collect(ch)
|
|
}
|
|
|
|
// processStatus extracts metrics from status data.
|
|
func (e *Exporter) processStatus(sd *StatusData) {
|
|
dsl := ExtractDSLInfo(sd)
|
|
|
|
// Online status
|
|
if strings.EqualFold(dsl.OnlineStatus, "online") {
|
|
e.onlineStatus.Set(1)
|
|
} else {
|
|
e.onlineStatus.Set(0)
|
|
}
|
|
|
|
// DSL rates
|
|
if dsl.UpstreamRate > 0 {
|
|
e.dslUpRate.WithLabelValues("upstream", "current").Set(dsl.UpstreamRate)
|
|
}
|
|
if dsl.DownstreamRate > 0 {
|
|
e.dslUpRate.WithLabelValues("downstream", "current").Set(dsl.DownstreamRate)
|
|
}
|
|
if dsl.UpstreamMaxRate > 0 {
|
|
e.dslUpRate.WithLabelValues("upstream", "max").Set(dsl.UpstreamMaxRate)
|
|
}
|
|
if dsl.DownstreamMaxRate > 0 {
|
|
e.dslUpRate.WithLabelValues("downstream", "max").Set(dsl.DownstreamMaxRate)
|
|
}
|
|
|
|
// SNR
|
|
if dsl.UpstreamNoise > 0 {
|
|
e.dslSNR.WithLabelValues("upstream").Set(dsl.UpstreamNoise)
|
|
}
|
|
if dsl.DownstreamNoise > 0 {
|
|
e.dslSNR.WithLabelValues("downstream").Set(dsl.DownstreamNoise)
|
|
}
|
|
|
|
// Attenuation
|
|
if dsl.UpstreamAttn > 0 {
|
|
e.dslAttenuation.WithLabelValues("upstream").Set(dsl.UpstreamAttn)
|
|
}
|
|
if dsl.DownstreamAttn > 0 {
|
|
e.dslAttenuation.WithLabelValues("downstream").Set(dsl.DownstreamAttn)
|
|
}
|
|
|
|
// Power
|
|
e.dslPower.WithLabelValues("upstream").Set(dsl.UpstreamPower)
|
|
e.dslPower.WithLabelValues("downstream").Set(dsl.DownstreamPower)
|
|
|
|
// Error counters
|
|
e.dslErrors.WithLabelValues("upstream", "crc").Set(dsl.UpstreamCRC)
|
|
e.dslErrors.WithLabelValues("downstream", "crc").Set(dsl.DownstreamCRC)
|
|
e.dslErrors.WithLabelValues("upstream", "fec").Set(dsl.UpstreamFEC)
|
|
e.dslErrors.WithLabelValues("downstream", "fec").Set(dsl.DownstreamFEC)
|
|
e.dslErrors.WithLabelValues("upstream", "hec").Set(dsl.UpstreamHEC)
|
|
e.dslErrors.WithLabelValues("downstream", "hec").Set(dsl.DownstreamHEC)
|
|
|
|
// Uptime
|
|
if dsl.Uptime > 0 {
|
|
e.dslUptime.Set(dsl.Uptime)
|
|
}
|
|
|
|
// Info metric
|
|
e.rawInfo.WithLabelValues(
|
|
sd.Get("device_name"),
|
|
sd.Get("firmware_version"),
|
|
sd.Get("serial_number"),
|
|
dsl.DSLOperatingMode,
|
|
).Set(1)
|
|
|
|
// Export all numeric raw values
|
|
for _, entry := range sd.Raw {
|
|
if v, err := strconv.ParseFloat(strings.TrimSpace(entry.VarValue), 64); err == nil {
|
|
e.rawValues.WithLabelValues(entry.VarID, entry.VarType).Set(v)
|
|
}
|
|
}
|
|
}
|