speedporter/main.go

241 lines
5.1 KiB
Go
Raw Permalink Normal View History

2024-10-19 09:43:07 -04:00
package main
import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
2024-10-21 16:56:04 -04:00
bandwidthScale = 125000.0
defaultListenPort = 9090
defaultListenAddr = "0.0.0.0"
defaultTestPeriod = 5
2024-10-19 09:43:07 -04:00
defaultSpeedtestExec = "/usr/bin/speedtest"
2024-10-21 16:56:04 -04:00
listenPortEnvVar = "SPEEDTEST_METRICS_LISTEN_PORT"
listenAddrEnvVar = "SPEEDTEST_METRICS_LISTEN_ADDR"
testPeriodEnvVar = "SPEEDTEST_PERIOD_MINS"
speedtestExecEnvVar = "SPEEDTEST_EXEC"
2024-10-19 09:43:07 -04:00
)
var (
labels = []string{
"interface_name",
"interface_mac",
"test_host",
}
uploadSpeed = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "isp_upload_speed",
Help: "The upload speed of the network",
},
labels,
)
downloadSpeed = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "isp_download_speed",
Help: "The download speed of the network",
},
labels,
)
pingLatency = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "isp_ping_latency",
Help: "The ping latency of the network",
},
labels,
)
packetLoss = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "isp_packet_loss",
Help: "The packet loss of the network",
},
labels,
)
2024-10-21 16:56:04 -04:00
listenPort uint16
listenAddr string
2024-10-19 09:43:07 -04:00
testPeriodMins uint
2024-10-21 16:56:04 -04:00
speedtestExec string
2024-10-19 09:43:07 -04:00
)
type SpeedTestResult struct {
2024-10-21 16:56:04 -04:00
Type string
2024-10-19 09:43:07 -04:00
Timestamp time.Time
2024-10-21 16:56:04 -04:00
Ping struct {
Jitter float64
2024-10-19 09:43:07 -04:00
Latency float64
2024-10-21 16:56:04 -04:00
Low float64
High float64
2024-10-19 09:43:07 -04:00
}
Download struct {
Bandwidth int
2024-10-21 16:56:04 -04:00
Bytes int
Elapsed int
Latency struct {
IQM float64
Low float64
High float64
2024-10-19 09:43:07 -04:00
Jitter float64
}
}
Upload struct {
Bandwidth int
2024-10-21 16:56:04 -04:00
Bytes int
Elapsed int
Latency struct {
IQM float64
Low float64
High float64
2024-10-19 09:43:07 -04:00
Jitter float64
}
}
PacketLoss float64
2024-10-21 16:56:04 -04:00
ISP string
Interface struct {
2024-10-19 09:43:07 -04:00
InternalIP string
2024-10-21 16:56:04 -04:00
Name string
MACAddr string
IsVPN bool
2024-10-19 09:43:07 -04:00
ExternalIP string
}
Server struct {
2024-10-21 16:56:04 -04:00
ID int
Host string
Port int16
Name string
2024-10-19 09:43:07 -04:00
Location string
2024-10-21 16:56:04 -04:00
Country string
IP string
2024-10-19 09:43:07 -04:00
}
Result struct {
2024-10-21 16:56:04 -04:00
ID string
URL string
2024-10-19 09:43:07 -04:00
Persisted bool
}
}
func init() {
prometheus.MustRegister(uploadSpeed)
prometheus.MustRegister(downloadSpeed)
prometheus.MustRegister(pingLatency)
prometheus.MustRegister(packetLoss)
2024-10-19 09:43:07 -04:00
if lpEV, ok := os.LookupEnv(listenPortEnvVar); !ok {
listenPort = defaultListenPort
} else {
val, err := strconv.Atoi(lpEV)
if err != nil {
panic(fmt.Sprintf(
"Invalid Listen Port Specified in Environment Variable: %s",
listenPortEnvVar,
))
}
listenPort = uint16(val)
if int(listenPort) != val {
panic(fmt.Sprintf(
"Invalid Listen Port Specified in Environment Variable: %s=%d",
listenPortEnvVar,
val,
))
}
}
if tpEV, ok := os.LookupEnv(testPeriodEnvVar); !ok {
testPeriodMins = defaultTestPeriod
} else {
val, err := strconv.Atoi(tpEV)
if err != nil {
panic(fmt.Sprintf(
"Invalid Test Period Specified in Environment Variable: %s",
testPeriodEnvVar,
))
}
testPeriodMins = uint(val)
if int(testPeriodMins) != val {
panic(fmt.Sprintf(
"Invalid Test Period Specified in Environment Variable: %s=%d",
testPeriodEnvVar,
val,
))
}
}
if laEV, ok := os.LookupEnv(listenAddrEnvVar); !ok {
listenAddr = defaultListenAddr
} else if net.ParseIP(laEV) == nil {
panic(fmt.Sprintf(
"Invalid Listen Address Specified in Environment Variable: %s=%s",
listenAddrEnvVar,
laEV,
))
} else {
listenAddr = laEV
}
if seEV, ok := os.LookupEnv(speedtestExecEnvVar); !ok {
speedtestExec = defaultSpeedtestExec
} else {
speedtestExec = seEV
}
cmd := exec.Command(speedtestExec, "-h")
_, err := cmd.Output()
if err != nil {
panic(fmt.Sprintf("Error executing command: %v", err))
}
}
func runTest() {
cmd := exec.Command(speedtestExec, "-f", "json")
output, err := cmd.Output()
if err != nil {
log.Printf("Error executing command: %v", err)
return
}
var result SpeedTestResult
err = json.Unmarshal(output, &result)
if err != nil {
log.Printf("Error parsing JSON: %v\n", err)
return
} else if result.Type != "result" {
log.Printf("Did not receive a result: %s\n", result.Type)
return
}
uploadBandwidth := float64(result.Upload.Bandwidth) / bandwidthScale
downloadBandwidth := float64(result.Download.Bandwidth) / bandwidthScale
labelVals := []string{
result.Interface.Name,
result.Interface.MACAddr,
result.Server.Host,
}
uploadSpeed.WithLabelValues(labelVals...).Set(uploadBandwidth)
downloadSpeed.WithLabelValues(labelVals...).Set(downloadBandwidth)
pingLatency.WithLabelValues(labelVals...).Set(result.Ping.Latency)
packetLoss.WithLabelValues(labelVals...).Set(result.PacketLoss)
2024-10-19 09:43:07 -04:00
}
func main() {
go func() {
for {
runTest()
time.Sleep(time.Duration(testPeriodMins) * time.Minute)
}
}()
http.Handle("/metrics", promhttp.Handler())
fmt.Printf("Starting server on %s:%d", listenAddr, listenPort)
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", listenAddr, listenPort), nil))
}