316 lines
10 KiB
Go
316 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
speedport "speedport-exporter/internal/speedport"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
)
|
|
|
|
var (
|
|
version = "dev"
|
|
commit = "none"
|
|
date = "unknown"
|
|
)
|
|
|
|
func main() {
|
|
// Global flags
|
|
host := flag.String("host", envOrDefault("SPEEDPORT_HOST", "speedport.ip"), "Speedport router address")
|
|
password := flag.String("password", envOrDefault("SPEEDPORT_PASSWORD", ""), "Router password (for authenticated endpoints)")
|
|
debug := flag.Bool("debug", false, "Enable debug logging")
|
|
versionFlag := flag.Bool("version", false, "Print version and exit")
|
|
|
|
// Subcommand detection
|
|
if len(os.Args) < 2 {
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
subcommand := os.Args[1]
|
|
|
|
// Handle version flag before subcommand parsing
|
|
if subcommand == "-version" || subcommand == "--version" || subcommand == "version" {
|
|
fmt.Printf("speedport-exporter %s (commit: %s, built: %s)\n", version, commit, date)
|
|
os.Exit(0)
|
|
}
|
|
|
|
// Parse flags after subcommand
|
|
flag.CommandLine.Parse(os.Args[2:])
|
|
|
|
if *versionFlag {
|
|
fmt.Printf("speedport-exporter %s (commit: %s, built: %s)\n", version, commit, date)
|
|
os.Exit(0)
|
|
}
|
|
|
|
// Setup logger
|
|
logLevel := slog.LevelInfo
|
|
if *debug {
|
|
logLevel = slog.LevelDebug
|
|
}
|
|
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))
|
|
|
|
// Create client
|
|
opts := []speedport.ClientOption{
|
|
speedport.WithLogger(logger),
|
|
}
|
|
if *password != "" {
|
|
opts = append(opts, speedport.WithPassword(*password))
|
|
}
|
|
|
|
client, err := speedport.NewClient(*host, opts...)
|
|
if err != nil {
|
|
logger.Error("failed to create client", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
switch subcommand {
|
|
case "status":
|
|
cmdStatus(client, logger)
|
|
case "raw":
|
|
cmdRaw(client, logger)
|
|
case "json":
|
|
cmdJSON(client, logger)
|
|
case "endpoints":
|
|
cmdEndpoints(client, logger)
|
|
case "serve":
|
|
cmdServe(client, logger)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", subcommand)
|
|
printUsage()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func printUsage() {
|
|
fmt.Fprintf(os.Stderr, `speedport-exporter - Speedport Smart 4 Status Reader & Prometheus Exporter
|
|
|
|
Usage:
|
|
speedport-exporter <command> [flags]
|
|
|
|
Commands:
|
|
status Show formatted DSL status overview
|
|
raw Show all raw key-value pairs from Status.json
|
|
json Output raw JSON from Status.json
|
|
endpoints Fetch and dump all known endpoints
|
|
serve Start Prometheus metrics exporter HTTP server
|
|
|
|
Flags:
|
|
-host Router address (default: speedport.ip, env: SPEEDPORT_HOST)
|
|
-password Router password for authenticated endpoints (env: SPEEDPORT_PASSWORD)
|
|
-debug Enable debug logging
|
|
|
|
Serve-specific flags:
|
|
-listen Listen address for HTTP server (default: :9810)
|
|
-interval Minimum scrape interval (default: 30s)
|
|
|
|
Environment variables:
|
|
SPEEDPORT_HOST Router address
|
|
SPEEDPORT_PASSWORD Router password
|
|
|
|
Examples:
|
|
speedport-exporter status
|
|
speedport-exporter status -host 192.168.2.1
|
|
speedport-exporter raw -host speedport.ip
|
|
speedport-exporter json | jq .
|
|
speedport-exporter serve -listen :9810
|
|
SPEEDPORT_PASSWORD=mypass speedport-exporter endpoints
|
|
`)
|
|
}
|
|
|
|
// cmdStatus shows a formatted DSL status overview.
|
|
func cmdStatus(client *speedport.Client, logger *slog.Logger) {
|
|
sd, err := client.GetStatus()
|
|
if err != nil {
|
|
logger.Error("failed to get status", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
dsl := speedport.ExtractDSLInfo(sd)
|
|
|
|
fmt.Println("╔══════════════════════════════════════════════════════╗")
|
|
fmt.Println("║ Speedport Smart 4 - DSL Status ║")
|
|
fmt.Println("╠══════════════════════════════════════════════════════╣")
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
|
|
fmt.Fprintf(w, "║ %-20s\t%s\n", "Device:", sd.Get("device_name"))
|
|
fmt.Fprintf(w, "║ %-20s\t%s\n", "Firmware:", sd.Get("firmware_version"))
|
|
fmt.Fprintf(w, "║ %-20s\t%s\n", "Online Status:", colorStatus(dsl.OnlineStatus))
|
|
fmt.Fprintf(w, "║ %-20s\t%s\n", "DSL Link:", colorStatus(dsl.DSLLinkStatus))
|
|
fmt.Fprintf(w, "║ %-20s\t%s\n", "DSL Mode:", dsl.DSLOperatingMode)
|
|
w.Flush()
|
|
|
|
fmt.Println("╠══════════════════════════════════════════════════════╣")
|
|
fmt.Println("║ Line Rates Upstream Downstream ║")
|
|
fmt.Println("╠══════════════════════════════════════════════════════╣")
|
|
|
|
fmt.Fprintf(w, "║ %-25s\t%8.0f\t%8.0f kbit/s\n", "Current Rate:", dsl.UpstreamRate, dsl.DownstreamRate)
|
|
fmt.Fprintf(w, "║ %-25s\t%8.0f\t%8.0f kbit/s\n", "Max Attainable Rate:", dsl.UpstreamMaxRate, dsl.DownstreamMaxRate)
|
|
w.Flush()
|
|
|
|
fmt.Println("╠══════════════════════════════════════════════════════╣")
|
|
fmt.Println("║ Line Quality Upstream Downstream ║")
|
|
fmt.Println("╠══════════════════════════════════════════════════════╣")
|
|
|
|
fmt.Fprintf(w, "║ %-25s\t%8.1f\t%8.1f dB\n", "SNR Margin:", dsl.UpstreamNoise, dsl.DownstreamNoise)
|
|
fmt.Fprintf(w, "║ %-25s\t%8.1f\t%8.1f dB\n", "Line Attenuation:", dsl.UpstreamAttn, dsl.DownstreamAttn)
|
|
fmt.Fprintf(w, "║ %-25s\t%8.1f\t%8.1f dBm\n", "Signal Power:", dsl.UpstreamPower, dsl.DownstreamPower)
|
|
w.Flush()
|
|
|
|
fmt.Println("╠══════════════════════════════════════════════════════╣")
|
|
fmt.Println("║ Error Counters Upstream Downstream ║")
|
|
fmt.Println("╠══════════════════════════════════════════════════════╣")
|
|
|
|
fmt.Fprintf(w, "║ %-25s\t%8.0f\t%8.0f\n", "CRC Errors:", dsl.UpstreamCRC, dsl.DownstreamCRC)
|
|
fmt.Fprintf(w, "║ %-25s\t%8.0f\t%8.0f\n", "FEC Errors:", dsl.UpstreamFEC, dsl.DownstreamFEC)
|
|
fmt.Fprintf(w, "║ %-25s\t%8.0f\t%8.0f\n", "HEC Errors:", dsl.UpstreamHEC, dsl.DownstreamHEC)
|
|
w.Flush()
|
|
|
|
if dsl.Uptime > 0 {
|
|
fmt.Println("╠══════════════════════════════════════════════════════╣")
|
|
fmt.Fprintf(w, "║ %-25s\t%s\n", "Connection Uptime:", formatDuration(dsl.Uptime))
|
|
w.Flush()
|
|
}
|
|
|
|
fmt.Println("╚══════════════════════════════════════════════════════╝")
|
|
}
|
|
|
|
// cmdRaw dumps all raw key-value pairs.
|
|
func cmdRaw(client *speedport.Client, logger *slog.Logger) {
|
|
sd, err := client.GetStatus()
|
|
if err != nil {
|
|
logger.Error("failed to get status", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintf(w, "TYPE\tID\tVALUE\n")
|
|
fmt.Fprintf(w, "----\t--\t-----\n")
|
|
for _, entry := range sd.Raw {
|
|
fmt.Fprintf(w, "%s\t%s\t%s\n", entry.VarType, entry.VarID, entry.VarValue)
|
|
}
|
|
w.Flush()
|
|
}
|
|
|
|
// cmdJSON outputs raw JSON.
|
|
func cmdJSON(client *speedport.Client, logger *slog.Logger) {
|
|
sd, err := client.GetStatus()
|
|
if err != nil {
|
|
logger.Error("failed to get status", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
|
|
// Output as a clean map
|
|
output := make(map[string]interface{})
|
|
for _, entry := range sd.Raw {
|
|
output[entry.VarID] = entry.VarValue
|
|
}
|
|
|
|
if err := enc.Encode(output); err != nil {
|
|
logger.Error("failed to encode JSON", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// cmdEndpoints fetches all known endpoints and dumps them.
|
|
func cmdEndpoints(client *speedport.Client, logger *slog.Logger) {
|
|
for _, ep := range speedport.AllEndpoints {
|
|
fmt.Printf("=== %s ===\n", ep)
|
|
sd, err := client.GetEndpoint(ep)
|
|
if err != nil {
|
|
fmt.Printf(" ERROR: %v\n\n", err)
|
|
continue
|
|
}
|
|
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
for _, entry := range sd.Raw {
|
|
fmt.Fprintf(w, " [%s] %s\t= %s\n", entry.VarType, entry.VarID, entry.VarValue)
|
|
}
|
|
w.Flush()
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
// cmdServe starts the Prometheus metrics HTTP server.
|
|
func cmdServe(client *speedport.Client, logger *slog.Logger) {
|
|
// Parse serve-specific flags
|
|
serveFlags := flag.NewFlagSet("serve", flag.ExitOnError)
|
|
listen := serveFlags.String("listen", envOrDefault("SPEEDPORT_LISTEN", ":9810"), "Listen address")
|
|
_ = serveFlags.Parse(flag.Args())
|
|
|
|
exporter := speedport.NewExporter(client, logger)
|
|
prometheus.MustRegister(exporter)
|
|
|
|
http.Handle("/metrics", promhttp.Handler())
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
fmt.Fprintf(w, `<!DOCTYPE html>
|
|
<html>
|
|
<head><title>Speedport Exporter</title></head>
|
|
<body>
|
|
<h1>Speedport Smart 4 Exporter</h1>
|
|
<p><a href="/metrics">Metrics</a></p>
|
|
<p><a href="/health">Health</a></p>
|
|
</body>
|
|
</html>`)
|
|
})
|
|
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
fmt.Fprintln(w, "ok")
|
|
})
|
|
|
|
logger.Info("starting Prometheus exporter", "listen", *listen)
|
|
if err := http.ListenAndServe(*listen, nil); err != nil {
|
|
logger.Error("server failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// --- Helpers ---
|
|
|
|
func envOrDefault(key, defaultVal string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return defaultVal
|
|
}
|
|
|
|
func colorStatus(s string) string {
|
|
switch strings.ToLower(s) {
|
|
case "online", "up":
|
|
return "✅ " + s
|
|
case "offline", "down", "":
|
|
return "❌ " + s
|
|
default:
|
|
return "⚠️ " + s
|
|
}
|
|
}
|
|
|
|
func formatDuration(seconds float64) string {
|
|
d := time.Duration(seconds) * time.Second
|
|
days := int(d.Hours()) / 24
|
|
hours := int(d.Hours()) % 24
|
|
mins := int(d.Minutes()) % 60
|
|
|
|
if days > 0 {
|
|
return fmt.Sprintf("%dd %dh %dm", days, hours, mins)
|
|
}
|
|
if hours > 0 {
|
|
return fmt.Sprintf("%dh %dm", hours, mins)
|
|
}
|
|
return fmt.Sprintf("%dm", mins)
|
|
}
|