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