Files
speedport-exporter/internal/speedport/exporter.go
2026-03-07 16:31:30 +01:00

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)
}
}
}